- Beacon detail endpoints (LiveView + REST API) now hide unapproved beacons from anonymous and unauthorized viewers; only the submitter and admins can see pending records before approval. Adds Beacons.get_visible_beacon/2 with scope-aware checks. - API contact pagination now honors per_page end-to-end. Radio.list_contacts/1 accepts :per_page and clamps to 200. - API rate limiter: ETS table is now owned by a long-lived Sweeper GenServer (won't die with a request task); Sweeper periodically prunes expired-window rows to bound memory; init_table/0 race is rescued. - /scores/cells and /weather/cells: add per-IP rate limiting and a shared GridBounds clamp/413 guard so global / oversized viewports no longer drive unbounded binary responses. - NEXRAD PNG unfilter (sub/up/average/paeth): replace acc++[byte] + Enum.at(acc, idx-bpp) with O(n) binary recursion. Decode time for the 12200x5400 n0q frame goes from quadratic to linear. - LiveTableFooter.parse_page and ScoresFile.fetch_bound: switch String.to_integer/String.to_float to Integer.parse/Float.parse, fall back to defaults instead of raising. - PathLive and MapLive band-event handlers: replace String.to_integer(params["band"]) with parse_int / parse_band_param so a non-numeric band parameter no longer crashes the LiveView.
116 lines
3.7 KiB
Elixir
116 lines
3.7 KiB
Elixir
defmodule MicrowavepropWeb.Api.V1.BeaconControllerTest do
|
|
use MicrowavepropWeb.ConnCase, async: true
|
|
|
|
alias Microwaveprop.Accounts
|
|
alias Microwaveprop.AccountsFixtures
|
|
alias Microwaveprop.Beacons
|
|
alias MicrowavepropWeb.Api.RateLimiter
|
|
|
|
@valid %{
|
|
"frequency_mhz" => 10_368.1,
|
|
"callsign" => "W5HN",
|
|
"lat" => 32.897,
|
|
"lon" => -97.038,
|
|
"power_mw" => 1000.0,
|
|
"height_ft" => 100,
|
|
"keying" => "on_off"
|
|
}
|
|
|
|
setup %{conn: conn} do
|
|
RateLimiter.reset()
|
|
user = AccountsFixtures.user_fixture()
|
|
{:ok, {plaintext, _}} = Accounts.create_api_token(user, %{name: "t"})
|
|
%{conn: conn, user: user, plaintext: plaintext}
|
|
end
|
|
|
|
describe "GET /api/v1/beacons" do
|
|
test "returns approved beacons only", %{conn: conn, user: user} do
|
|
{:ok, approved} = Beacons.create_beacon(user, @valid)
|
|
{:ok, _} = Beacons.approve_beacon(approved)
|
|
{:ok, _pending} = Beacons.create_beacon(user, Map.put(@valid, "callsign", "K5XX"))
|
|
|
|
body = conn |> get(~p"/api/v1/beacons") |> json_response(200)
|
|
|
|
assert Enum.any?(body["data"], &(&1["callsign"] == "W5HN"))
|
|
refute Enum.any?(body["data"], &(&1["callsign"] == "K5XX"))
|
|
end
|
|
end
|
|
|
|
describe "GET /api/v1/beacons/:id" do
|
|
test "returns approved beacons to anonymous viewers", %{conn: conn, user: user} do
|
|
{:ok, b} = Beacons.create_beacon(user, @valid)
|
|
{:ok, b} = Beacons.approve_beacon(b)
|
|
body = conn |> get(~p"/api/v1/beacons/#{b.id}") |> json_response(200)
|
|
assert body["data"]["id"] == b.id
|
|
end
|
|
|
|
test "404 for anonymous viewers on unapproved beacons", %{conn: conn, user: user} do
|
|
{:ok, b} = Beacons.create_beacon(user, @valid)
|
|
conn = get(conn, ~p"/api/v1/beacons/#{b.id}")
|
|
assert json_response(conn, 404)
|
|
end
|
|
|
|
test "submitter can view their own unapproved beacon", %{conn: conn, user: user, plaintext: pt} do
|
|
{:ok, b} = Beacons.create_beacon(user, @valid)
|
|
|
|
body =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer " <> pt)
|
|
|> get(~p"/api/v1/beacons/#{b.id}")
|
|
|> json_response(200)
|
|
|
|
assert body["data"]["id"] == b.id
|
|
assert body["data"]["approved"] == false
|
|
end
|
|
|
|
test "non-owner non-admin gets 404 on another user's unapproved beacon", %{conn: conn, user: user} do
|
|
{:ok, b} = Beacons.create_beacon(user, @valid)
|
|
other = AccountsFixtures.user_fixture()
|
|
{:ok, {other_pt, _}} = Accounts.create_api_token(other, %{name: "t"})
|
|
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer " <> other_pt)
|
|
|> get(~p"/api/v1/beacons/#{b.id}")
|
|
|
|
assert json_response(conn, 404)
|
|
end
|
|
|
|
test "404 on unknown id", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/beacons/00000000-0000-0000-0000-000000000000")
|
|
assert json_response(conn, 404)
|
|
end
|
|
|
|
test "404 on malformed id", %{conn: conn} do
|
|
conn = get(conn, ~p"/api/v1/beacons/not-a-uuid")
|
|
assert json_response(conn, 404)
|
|
end
|
|
end
|
|
|
|
describe "POST /api/v1/beacons" do
|
|
test "requires auth", %{conn: conn} do
|
|
conn = post(conn, ~p"/api/v1/beacons", @valid)
|
|
assert json_response(conn, 401)
|
|
end
|
|
|
|
test "creates a beacon under the authenticated user", %{conn: conn, plaintext: pt} do
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer " <> pt)
|
|
|> post(~p"/api/v1/beacons", @valid)
|
|
|
|
body = json_response(conn, 201)
|
|
assert body["data"]["callsign"] == "W5HN"
|
|
assert body["data"]["approved"] == false
|
|
end
|
|
|
|
test "422 on invalid payload", %{conn: conn, plaintext: pt} do
|
|
conn =
|
|
conn
|
|
|> put_req_header("authorization", "Bearer " <> pt)
|
|
|> post(~p"/api/v1/beacons", %{"callsign" => "x"})
|
|
|
|
assert json_response(conn, 422)
|
|
end
|
|
end
|
|
end
|