towerops/test/towerops_web/controllers/api/mobile_auth_controller_test.exs
Graham McIntire abfd44fd75 fix: 3 more high-severity bugs (H19, H20, H25)
- H19: /api/v1/mobile/auth/qr/verify no longer returns user_email. Knowing
  a QR token now only tells the caller the token is valid; the email is
  only revealed by /complete which consumes the token.
- H20: CoverageWorker max_attempts dropped from 3 to 1. The fail/2 path
  already returns :ok and writes the failure onto the coverage record,
  so the 3-attempt retry policy was never reachable and was misleading.
- H25: ChecksController.create rejects device_ids that don't belong to
  the caller's organization (via ScopedResource.fetch). Without this an
  API token could attach service checks to devices in another tenant.

Also strip bug ID references from in-code comments (per feedback that
H/M/L numbers don't survive past their bugs.md removal); commit history
keeps the audit trail.
2026-05-12 11:32:20 -05:00

112 lines
3.5 KiB
Elixir

defmodule ToweropsWeb.Api.MobileAuthControllerTest do
use ToweropsWeb.ConnCase, async: true
import Towerops.AccountsFixtures
alias Towerops.MobileSessions
describe "verify_qr_token" do
test "returns error for missing token", %{conn: conn} do
conn = post(conn, ~p"/api/v1/mobile/auth/qr/verify", %{})
assert json_response(conn, 400)["error"] == "Missing token parameter"
end
test "returns error for invalid token", %{conn: conn} do
conn = post(conn, ~p"/api/v1/mobile/auth/qr/verify", %{"token" => "invalid"})
assert json_response(conn, 401)["valid"] == false
end
test "returns valid for an active QR token without leaking the user email", %{conn: conn} do
user = user_fixture()
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
conn = post(conn, ~p"/api/v1/mobile/auth/qr/verify", %{"token" => qr_token.token})
response = json_response(conn, 200)
assert response["valid"] == true
# Email must not be returned by /verify (only by /complete, once the
# token is consumed). Otherwise anyone with a QR token can read the
# linked user's email.
refute Map.has_key?(response, "user_email")
end
end
describe "get_session and revoke_session (authenticated)" do
setup do
user = user_fixture()
{:ok, session} =
MobileSessions.create_mobile_session(%{
user_id: user.id,
device_name: "iTest"
})
%{session: session, raw_token: session.raw_token, user: user}
end
test "returns session metadata for an authenticated request",
%{conn: conn, raw_token: raw_token, session: session} do
conn =
conn
|> put_req_header("authorization", "Bearer #{raw_token}")
|> get(~p"/api/v1/mobile/auth/session")
response = json_response(conn, 200)
assert response["id"] == session.id
assert response["device_name"] == "iTest"
end
test "revokes the current session", %{conn: conn, raw_token: raw_token} do
conn =
conn
|> put_req_header("authorization", "Bearer #{raw_token}")
|> delete(~p"/api/v1/mobile/auth/session")
assert json_response(conn, 200)["success"] == true
end
end
describe "complete_qr_login" do
test "returns error for missing token", %{conn: conn} do
conn = post(conn, ~p"/api/v1/mobile/auth/qr/complete", %{})
assert json_response(conn, 400)["error"] == "Missing token parameter"
end
test "returns error for invalid token", %{conn: conn} do
conn =
post(conn, ~p"/api/v1/mobile/auth/qr/complete", %{
"token" => "invalid",
"device_name" => "Test Phone"
})
assert json_response(conn, 401)["error"] == "Invalid or expired token"
end
test "returns a usable session token on success", %{conn: conn} do
user = user_fixture()
{:ok, qr_token} = MobileSessions.create_qr_login_token(user.id)
conn =
post(conn, ~p"/api/v1/mobile/auth/qr/complete", %{
"token" => qr_token.token,
"device_name" => "Test iPhone",
"device_os" => "iOS 17.2",
"app_version" => "1.0.0"
})
assert %{"session_token" => session_token} = json_response(conn, 200)
assert is_binary(session_token)
# The returned token must be usable for subsequent authenticated requests
authed_conn =
build_conn()
|> put_req_header("authorization", "Bearer #{session_token}")
|> get(~p"/api/v1/mobile/organizations")
assert json_response(authed_conn, 200)
end
end
end