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).
65 lines
1.9 KiB
Elixir
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
|