defmodule ToweropsWeb.UserCredentialControllerTest do use ToweropsWeb.ConnCase, async: true import Towerops.AccountsFixtures alias Towerops.Accounts describe "POST /api/webauthn/registration/challenge" do setup :register_and_log_in_user test "returns challenge for authenticated user with confirmed email", %{conn: conn, user: user} do conn = post(conn, ~p"/api/webauthn/registration/challenge") assert %{"challenge" => challenge, "user" => user_data} = json_response(conn, 200) assert is_binary(challenge) assert user_data["name"] == user.email end test "returns error for user without confirmed email" do # Create unconfirmed user user = unconfirmed_user_fixture() conn = log_in_user(build_conn(), user) conn = post(conn, ~p"/api/webauthn/registration/challenge") assert %{"error" => error} = json_response(conn, 403) assert error =~ "Email confirmation required" end test "requires authentication" do conn = build_conn() conn = post(conn, ~p"/api/webauthn/registration/challenge") assert redirected_to(conn) == ~p"/users/log-in" end end describe "POST /api/webauthn/register" do setup :register_and_log_in_user test "registers credential successfully", %{conn: conn} do # Set challenge in session conn = put_session(conn, :webauthn_challenge, "test_challenge_123") # Mock successful registration params params = %{ "name" => "My Passkey", "rawId" => Base.url_encode64("credential_id", padding: false), "response" => %{ "clientDataJSON" => Base.url_encode64(~s({"type":"webauthn.create","challenge":"test_challenge_123"}), padding: false), "attestationObject" => Base.url_encode64("mock_attestation", padding: false) }, "type" => "public-key" } conn = post(conn, ~p"/api/webauthn/register", params) # Should return error since WebAuthn verification will fail in test # But this tests the controller flow assert conn.status in [201, 422] end test "returns error when email not confirmed", %{conn: _conn} do # Create unconfirmed user unconfirmed_user = unconfirmed_user_fixture() conn = log_in_user(build_conn(), unconfirmed_user) conn = put_session(conn, :webauthn_challenge, "test_challenge") params = %{ "name" => "My Passkey", "rawId" => Base.url_encode64("credential_id", padding: false) } conn = post(conn, ~p"/api/webauthn/register", params) assert %{"error" => error} = json_response(conn, 403) assert error =~ "Email confirmation required" end test "returns error on registration failure", %{conn: conn} do conn = put_session(conn, :webauthn_challenge, "test_challenge") # Invalid params params = %{"invalid" => "data"} conn = post(conn, ~p"/api/webauthn/register", params) assert %{"error" => _} = json_response(conn, 422) end test "requires authentication" do conn = build_conn() conn = post(conn, ~p"/api/webauthn/register", %{}) assert redirected_to(conn) == ~p"/users/log-in" end end describe "POST /api/webauthn/authentication/challenge" do test "returns challenge for user with passkeys", %{conn: conn} do user = user_fixture() _credential = credential_fixture(user, %{name: "Test Key"}) conn = post(conn, ~p"/api/webauthn/authentication/challenge", %{"email" => user.email}) assert %{"challenge" => challenge} = json_response(conn, 200) assert is_binary(challenge) end test "returns challenge for discoverable authentication", %{conn: conn} do # No email provided - discoverable credential flow conn = post(conn, ~p"/api/webauthn/authentication/challenge", %{}) assert %{"challenge" => challenge} = json_response(conn, 200) assert is_binary(challenge) end test "returns error for user without passkeys", %{conn: conn} do user = user_fixture() conn = post(conn, ~p"/api/webauthn/authentication/challenge", %{"email" => user.email}) assert %{"error" => _} = json_response(conn, 404) end test "returns error for non-existent user", %{conn: conn} do conn = post(conn, ~p"/api/webauthn/authentication/challenge", %{ "email" => "nonexistent@example.com" }) assert %{"error" => _} = json_response(conn, 404) end test "does not require authentication", %{conn: conn} do user = user_fixture() conn = post(conn, ~p"/api/webauthn/authentication/challenge", %{"email" => user.email}) # Should not redirect to login assert conn.status in [200, 404] end end describe "POST /api/webauthn/authenticate" do test "returns error when no challenge in session" do conn = build_conn() conn = post(conn, ~p"/api/webauthn/authenticate", %{}) assert %{"error" => _} = json_response(conn, 401) end end describe "DELETE /users/credentials/:id" do setup :register_and_log_in_user test "deletes credential successfully", %{conn: conn, user: user} do # Create a test credential credential = credential_fixture(user, %{name: "Test Key"}) conn = delete(conn, ~p"/users/credentials/#{credential.id}") assert redirected_to(conn) == ~p"/users/settings" assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "deleted successfully" # Verify credential is deleted assert Accounts.list_user_credentials(user.id) == [] end test "cannot delete another user's credential", %{conn: conn} do other_user = user_fixture() credential = credential_fixture(other_user, %{name: "Other Key"}) conn = delete(conn, ~p"/users/credentials/#{credential.id}") assert redirected_to(conn) == ~p"/users/settings" assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "not found" # Verify credential still exists assert length(Accounts.list_user_credentials(other_user.id)) == 1 end test "requires authentication" do conn = build_conn() conn = delete(conn, ~p"/users/credentials/#{Ecto.UUID.generate()}") assert redirected_to(conn) == ~p"/users/log-in" end end end