"""WebAuthn passkey integration tests. Covers WebAuthn registration, login, and credential management. These tests mock the cryptographic operations since real WebAuthn requires a browser environment. """ import pytest from unittest.mock import patch, MagicMock from tests.integration.client.base import ApiError def assert_success(response: dict, message_contains: str = "") -> dict: data = response.get("data", {}) assert response.get("success") is not False, ( f"Expected success but got error: {response.get('message')}" ) if message_contains: assert message_contains.lower() in response.get("message", "").lower() return data class TestWebAuthnRegistration: """Test WebAuthn passkey registration.""" def test_begin_registration_positive(self, integration_client, create_test_user): """TEST: WEBAUTHN-01 — Begin passkey registration. WHAT: POST /auth/webauthn/register/begin. WHY: First step of passkey enrollment. EXPECTED: 200 OK with challenge options. """ user = create_test_user(password="MyPassword123!") integration_client.auth.login(email=user["email"], password="MyPassword123!") result = integration_client.post("/auth/webauthn/register/begin") # Endpoint returns jsonify directly, not api_response wrapper assert "rp" in result or result.get("success") is not False def test_complete_registration_mocked_positive(self, integration_app, integration_client, create_test_user): """TEST: WEBAUTHN-02 — Complete passkey registration (mocked). WHAT: POST /auth/webauthn/register/complete with mocked verification. WHY: Full registration flow requires mocking crypto. EXPECTED: 201 Created when verification succeeds. """ from gatehouse_app.models.auth.authentication_method import AuthenticationMethod user = create_test_user(password="MyPassword123!") integration_client.auth.login(email=user["email"], password="MyPassword123!") with patch("gatehouse_app.api.v1.auth.webauthn.WebAuthnService.verify_registration_response") as mock_verify: mock_auth_method = MagicMock() mock_auth_method.to_webauthn_dict.return_value = {"id": "cred-123", "type": "public-key"} mock_verify.return_value = mock_auth_method import base64 client_data = base64.urlsafe_b64encode(b'{"challenge":"test-challenge"}').rstrip(b"=").decode() result = integration_client.post( "/auth/webauthn/register/complete", data={ "id": "cred-123", "rawId": "raw-123", "response": { "clientDataJSON": client_data, "attestationObject": "o2Nmb", }, "type": "public-key", }, ) # Mock path may return 201 or wrapped response depending on flow assert result.get("success") is not False or result.get("code") == 201 def test_list_credentials_positive(self, integration_client, create_test_user): """TEST: WEBAUTHN-03 — List WebAuthn credentials. WHAT: GET /auth/webauthn/credentials. WHY: Security page displays registered passkeys. EXPECTED: 200 OK with credentials array. """ user = create_test_user(password="MyPassword123!") integration_client.auth.login(email=user["email"], password="MyPassword123!") result = integration_client.get("/auth/webauthn/credentials") assert_success(result) class TestWebAuthnLogin: """Test WebAuthn login flow.""" def test_begin_login_positive(self, integration_client, create_test_user): """TEST: WEBAUTHN-04 — Begin WebAuthn login. WHAT: POST /auth/webauthn/login/begin with email. WHY: First step of passkey authentication. EXPECTED: 200 OK with challenge options (or 404 if no passkeys). """ user = create_test_user(password="MyPassword123!") try: result = integration_client.post("/auth/webauthn/login/begin", data={"email": user["email"]}) assert "challenge" in result except ApiError as exc: # Accept 404 when user has no passkeys registered assert exc.status_code == 404, f"Expected 200 or 404, got {exc.status_code}" def test_get_webauthn_status_positive(self, integration_client, create_test_user): """TEST: WEBAUTHN-05 — Get WebAuthn status. WHAT: GET /auth/webauthn/status. WHY: Security page shows whether passkeys are enabled. EXPECTED: 200 OK. """ user = create_test_user(password="MyPassword123!") integration_client.auth.login(email=user["email"], password="MyPassword123!") result = integration_client.get("/auth/webauthn/status") assert_success(result)