- 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.
112 lines
3.5 KiB
Elixir
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
|