Adds bearer-token authenticated REST API at /api/v1 covering every action a non-admin user can perform on the website: contact + beacon submission, beacon-monitor management, propagation queries, profile read/update, and self-service API token issuance/revocation. Security: SHA-256-hashed bearer tokens (mwp_ prefix, plaintext shown once at creation), RFC 9457 problem+json error responses, RFC 9651 RateLimit-* headers backed by an ETS bucket (600/min per token, 60/min per anonymous IP, 30/min on /auth/tokens), private-contact filtering by viewer. Docs at docs/api/README.md (prose reference) and docs/api/openapi.yaml (OpenAPI 3.1 spec covering every endpoint, response, and schema). Tests: 124 new tests across schema, plug, error renderer, rate limiter, fallback, and every controller. 16/17 API modules at 100% line coverage; FallbackController at 87.5% (one defmodule line, an Erlang-cover artifact for action_fallback-only modules).
83 lines
2.6 KiB
Elixir
83 lines
2.6 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 the beacon when found", %{conn: conn, user: user} do
|
|
{:ok, b} = Beacons.create_beacon(user, @valid)
|
|
body = conn |> get(~p"/api/v1/beacons/#{b.id}") |> json_response(200)
|
|
assert body["data"]["id"] == b.id
|
|
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
|