prop/test/microwaveprop_web/controllers/api/v1/beacon_controller_test.exs
Graham McIntire 14b90ee9f3
fix(security,perf): address 9 audit findings (access control, DoS, crashes)
- 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.
2026-05-11 18:53:21 -05:00

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