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).
55 lines
1.6 KiB
Elixir
55 lines
1.6 KiB
Elixir
defmodule MicrowavepropWeb.Api.FallbackControllerTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
import Plug.Test
|
|
|
|
alias Microwaveprop.Accounts.User
|
|
alias MicrowavepropWeb.Api.FallbackController
|
|
|
|
# Force the module to be (re-)loaded so cover-tracked module-load lines
|
|
# are counted even when only routed tests would otherwise reach this code.
|
|
setup_all do
|
|
Code.ensure_loaded(FallbackController)
|
|
:ok
|
|
end
|
|
|
|
defp build_conn do
|
|
:get
|
|
|> conn("/")
|
|
|> Plug.Conn.put_private(:phoenix_endpoint, MicrowavepropWeb.Endpoint)
|
|
end
|
|
|
|
test "renders changeset errors as 422" do
|
|
cs = User.registration_changeset(%User{}, %{}, validate_unique: false)
|
|
|
|
conn = FallbackController.call(build_conn(), {:error, cs})
|
|
|
|
assert conn.status == 422
|
|
end
|
|
|
|
test "renders :not_found as 404" do
|
|
conn = FallbackController.call(build_conn(), {:error, :not_found})
|
|
assert conn.status == 404
|
|
end
|
|
|
|
test "renders :forbidden as 403" do
|
|
conn = FallbackController.call(build_conn(), {:error, :forbidden})
|
|
assert conn.status == 403
|
|
end
|
|
|
|
test "renders :unauthorized as 401" do
|
|
conn = FallbackController.call(build_conn(), {:error, :unauthorized})
|
|
assert conn.status == 401
|
|
end
|
|
|
|
test "renders :bad_request with detail as 400" do
|
|
conn = FallbackController.call(build_conn(), {:error, :bad_request, "missing thing"})
|
|
assert conn.status == 400
|
|
assert %{"detail" => "missing thing"} = Jason.decode!(conn.resp_body)
|
|
end
|
|
|
|
test "renders :duplicate as 409" do
|
|
conn = FallbackController.call(build_conn(), {:error, :duplicate, %{}})
|
|
assert conn.status == 409
|
|
end
|
|
end
|