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