| """ |
| Auth 2FA Routes API Tests |
| |
| Tests for two-factor authentication endpoints including: |
| - Getting 2FA status |
| - Setting up 2FA |
| - Enabling 2FA |
| - Disabling 2FA |
| """ |
|
|
| import pytest |
| from fastapi.testclient import TestClient |
| from unittest.mock import Mock, patch, AsyncMock |
| from sqlalchemy.orm import Session |
|
|
| from main_api_app import app |
| from core.auth import get_current_user |
| from core.models import User |
|
|
|
|
| class TestAuth2FARoutes: |
| """Test 2FA authentication API endpoints.""" |
|
|
| @pytest.fixture |
| def mock_user(self): |
| """Create mock user.""" |
| user = Mock(spec=User) |
| user.id = "user-123" |
| user.email = "user@example.com" |
| user.two_factor_enabled = False |
| user.two_factor_secret = None |
| user.two_factor_backup_codes = None |
| return user |
|
|
| @pytest.fixture |
| def client(self, mock_user): |
| """Create test client with auth override.""" |
| app.dependency_overrides[get_current_user] = lambda: mock_user |
| try: |
| yield TestClient(app) |
| finally: |
| app.dependency_overrides.clear() |
|
|
| def test_get_2fa_status_disabled(self, client, mock_user): |
| """Test getting 2FA status when disabled.""" |
| mock_user.two_factor_enabled = False |
|
|
| response = client.get("/api/auth/2fa/status") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "enabled" in data |
| assert data["enabled"] is False |
|
|
| def test_get_2fa_status_enabled(self, client, mock_user): |
| """Test getting 2FA status when enabled.""" |
| mock_user.two_factor_enabled = True |
|
|
| response = client.get("/api/auth/2fa/status") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "enabled" in data |
| assert data["enabled"] is True |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_setup_2fa_success(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test successful 2FA setup.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_pyotp.random_base32.return_value = "test-secret-123" |
| mock_totp = Mock() |
| mock_totp.provisioning_uri.return_value = "otpauth://totp/test" |
| mock_pyotp.totp.TOTP.return_value = mock_totp |
|
|
| response = client.post("/api/auth/2fa/setup") |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "secret" in data |
| assert "otpauth_url" in data |
| assert data["secret"] == "test-secret-123" |
|
|
| def test_setup_2fa_already_enabled(self, client, mock_user): |
| """Test setup when 2FA is already enabled.""" |
| mock_user.two_factor_enabled = True |
|
|
| response = client.post("/api/auth/2fa/setup") |
|
|
| assert response.status_code == 409 |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_enable_2fa_success(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test successfully enabling 2FA.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = "existing-secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = True |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 200 |
| data = response.json() |
| assert "backup_codes" in data or "success" in data |
| assert mock_user.two_factor_enabled is True |
|
|
| def test_enable_2fa_already_enabled(self, client, mock_user): |
| """Test enable when 2FA is already enabled.""" |
| mock_user.two_factor_enabled = True |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 409 |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_enable_2fa_no_setup(self, mock_get_db, mock_pyotp, client, mock_user): |
| """Test enable without prior setup.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = None |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code in [400, 422] |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_enable_2fa_invalid_code(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test enable with invalid verification code.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = "test-secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = False |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "000000"} |
| ) |
|
|
| assert response.status_code in [400, 422] |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_disable_2fa_success(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test successfully disabling 2FA.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = True |
| mock_user.two_factor_secret = "test-secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = True |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| response = client.post( |
| "/api/auth/2fa/disable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 200 |
| assert mock_user.two_factor_enabled is False |
| assert mock_user.two_factor_secret is None |
|
|
| def test_disable_2fa_not_enabled(self, client, mock_user): |
| """Test disable when 2FA is not enabled.""" |
| mock_user.two_factor_enabled = False |
|
|
| response = client.post( |
| "/api/auth/2fa/disable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code in [400, 422] |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_disable_2fa_invalid_code(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test disable with invalid verification code.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = True |
| mock_user.two_factor_secret = "test-secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = False |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| response = client.post( |
| "/api/auth/2fa/disable", |
| json={"code": "000000"} |
| ) |
|
|
| assert response.status_code in [400, 422] |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_2fa_endpoints_require_auth(self, mock_get_db, mock_audit, mock_pyotp): |
| """Test that 2FA endpoints require authentication.""" |
| |
| client = TestClient(app) |
|
|
| |
| response = client.get("/api/auth/2fa/status") |
| assert response.status_code == 401 |
|
|
| |
| response = client.post("/api/auth/2fa/setup") |
| assert response.status_code == 401 |
|
|
| |
| response = client.post("/api/auth/2fa/enable", json={"code": "123456"}) |
| assert response.status_code == 401 |
|
|
| |
| response = client.post("/api/auth/2fa/disable", json={"code": "123456"}) |
| assert response.status_code == 401 |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_2fa_workflow_full_cycle(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test full 2FA enable/disable cycle.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_pyotp.random_base32.return_value = "secret-123" |
| mock_totp = Mock() |
| mock_totp.provisioning_uri.return_value = "otpauth://test" |
| mock_totp.verify.return_value = True |
| mock_pyotp.totp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| |
| response = client.post("/api/auth/2fa/setup") |
| assert response.status_code == 200 |
| assert mock_user.two_factor_secret == "secret-123" |
|
|
| |
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
| assert response.status_code == 200 |
| assert mock_user.two_factor_enabled is True |
|
|
| |
| response = client.post( |
| "/api/auth/2fa/disable", |
| json={"code": "123456"} |
| ) |
| assert response.status_code == 200 |
| assert mock_user.two_factor_enabled is False |
| assert mock_user.two_factor_secret is None |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_enable_creates_backup_codes(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test that enabling 2FA creates backup codes.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = "secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = True |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 200 |
| assert mock_user.two_factor_backup_codes is not None |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_enable_logs_audit_event(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test that enabling 2FA logs audit event.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = "secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = True |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| response = client.post( |
| "/api/auth/2fa/enable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 200 |
| mock_audit.log_event.assert_called_once() |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_disable_logs_audit_event(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test that disabling 2FA logs audit event.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| mock_user.two_factor_enabled = True |
| mock_user.two_factor_secret = "secret" |
|
|
| mock_totp = Mock() |
| mock_totp.verify.return_value = True |
| mock_pyotp.TOTP.return_value = mock_totp |
|
|
| mock_audit.log_event = Mock() |
|
|
| response = client.post( |
| "/api/auth/2fa/disable", |
| json={"code": "123456"} |
| ) |
|
|
| assert response.status_code == 200 |
| mock_audit.log_event.assert_called_once() |
|
|
| @patch('api.auth_2fa_routes.pyotp') |
| @patch('api.auth_2fa_routes.audit_service') |
| @patch('api.auth_2fa_routes.get_db') |
| def test_2fa_endpoints_return_json(self, mock_get_db, mock_audit, mock_pyotp, client, mock_user): |
| """Test that 2FA endpoints return JSON.""" |
| mock_db = Mock(spec=Session) |
| mock_get_db.return_value = mock_db |
|
|
| |
| response = client.get("/api/auth/2fa/status") |
| assert response.headers["content-type"].startswith("application/json") |
|
|
| |
| mock_pyotp.random_base32.return_value = "secret" |
| mock_totp = Mock() |
| mock_totp.provisioning_uri.return_value = "otpauth://test" |
| mock_totp.verify.return_value = True |
| mock_pyotp.totp.TOTP.return_value = mock_totp |
|
|
| |
| response = client.post("/api/auth/2fa/setup") |
| assert response.headers["content-type"].startswith("application/json") |
|
|
| |
| mock_user.two_factor_enabled = False |
| mock_user.two_factor_secret = "secret" |
| response = client.post("/api/auth/2fa/enable", json={"code": "123456"}) |
| assert response.headers["content-type"].startswith("application/json") |
|
|
| |
| mock_user.two_factor_enabled = True |
| response = client.post("/api/auth/2fa/disable", json={"code": "123456"}) |
| assert response.headers["content-type"].startswith("application/json") |
|
|