prop/test/microwaveprop_web/controllers/api/v1/profile_controller_test.exs
Graham McIntire c6d2c48264
feat: secure /api/v1 REST API for regular-user actions
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).
2026-05-09 08:59:54 -05:00

65 lines
1.9 KiB
Elixir

defmodule MicrowavepropWeb.Api.V1.ProfileControllerTest do
use MicrowavepropWeb.ConnCase, async: true
alias Microwaveprop.AccountsFixtures
alias Microwaveprop.Beacons
alias Microwaveprop.Radio
alias MicrowavepropWeb.Api.RateLimiter
setup do
RateLimiter.reset()
:ok
end
describe "GET /api/v1/profiles/:callsign" do
test "returns the profile + contacts + approved beacons", %{conn: conn} do
user = AccountsFixtures.user_fixture(%{callsign: "W5TEST"})
{:ok, _contact} =
Radio.create_contact(
%{
"station1" => "W5TEST",
"station2" => "K5OK",
"qso_timestamp" => "2026-01-02T00:00:00Z",
"band" => "10000",
"grid1" => "EM12",
"grid2" => "EM13",
"mode" => "CW",
"submitter_email" => "fixture@example.com"
},
user.id
)
{:ok, beacon} =
Beacons.create_beacon(user, %{
frequency_mhz: 10_368.1,
callsign: "W5HN",
lat: 32.897,
lon: -97.038,
power_mw: 1000.0,
height_ft: 100,
keying: "on_off"
})
{:ok, _approved} = Beacons.approve_beacon(beacon)
body = conn |> get(~p"/api/v1/profiles/W5TEST") |> json_response(200)
assert body["user"]["callsign"] == "W5TEST"
refute Map.has_key?(body["user"], "email")
assert length(body["contacts"]) >= 1
assert length(body["beacons"]) >= 1
end
test "case-insensitive lookup", %{conn: conn} do
_user = AccountsFixtures.user_fixture(%{callsign: "W5LOWER"})
body = conn |> get(~p"/api/v1/profiles/w5lower") |> json_response(200)
assert body["user"]["callsign"] == "W5LOWER"
end
test "404 when callsign is not registered", %{conn: conn} do
conn = get(conn, ~p"/api/v1/profiles/Z9NOPE")
assert json_response(conn, 404)
end
end
end