| """ |
| Auth Routes Integration Tests |
| |
| Tests for authentication endpoints from api/auth_routes.py. |
| |
| Coverage: |
| - POST /api/auth/mobile/login - Mobile user authentication |
| - POST /api/auth/mobile/biometric/register - Biometric registration |
| - POST /api/auth/mobile/biometric/authenticate - Biometric authentication |
| - POST /api/auth/mobile/refresh - Token refresh |
| - GET /api/auth/mobile/device - Device info |
| - DELETE /api/auth/mobile/device - Device deletion |
| """ |
|
|
| import os |
| import pytest |
| from datetime import datetime |
| from fastapi.testclient import TestClient |
| from sqlalchemy.orm import Session |
| from unittest.mock import patch, MagicMock |
|
|
| from api.auth_routes import router |
| from core.models import User, MobileDevice, UserRole |
| from core.database import get_db |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def db_session(): |
| """Create test database session""" |
| from core.database import SessionLocal |
| session = SessionLocal() |
| try: |
| yield session |
| finally: |
| session.rollback() |
| session.close() |
|
|
|
|
| @pytest.fixture |
| def test_user(db_session: Session): |
| """Create test user""" |
| import uuid |
| user_id = str(uuid.uuid4()) |
| user = User( |
| id=user_id, |
| email=f"test-{user_id}@example.com", |
| password_hash="hashed_password", |
| first_name="Test", |
| last_name="User", |
| role=UserRole.MEMBER, |
| status="active" |
| ) |
| db_session.add(user) |
| db_session.commit() |
| db_session.refresh(user) |
| return user |
|
|
|
|
| @pytest.fixture |
| def test_device(db_session: Session, test_user: User): |
| """Create test mobile device""" |
| import uuid |
| device = MobileDevice( |
| id=str(uuid.uuid4()), |
| user_id=str(test_user.id), |
| device_token="test_device_token_123", |
| platform="ios", |
| status="active", |
| notification_enabled=True, |
| last_active=datetime.utcnow(), |
| created_at=datetime.utcnow(), |
| device_info={"model": "iPhone 14", "os_version": "16.0"} |
| ) |
| db_session.add(device) |
| db_session.commit() |
| db_session.refresh(device) |
| return device |
|
|
|
|
| @pytest.fixture |
| def client(): |
| """Create TestClient for auth routes""" |
| from main import app |
| app.include_router(router) |
| with TestClient(app) as test_client: |
| yield test_client |
|
|
|
|
| |
| |
| |
|
|
| class TestMobileLogin: |
| """Test mobile login endpoint""" |
|
|
| def test_mobile_login_success(self, client: TestClient, test_user: User, db_session: Session): |
| """Test successful mobile login""" |
| |
| with patch('api.auth_routes.authenticate_mobile_user') as mock_auth: |
| mock_auth.return_value = { |
| "access_token": "test_access_token", |
| "refresh_token": "test_refresh_token", |
| "expires_at": "2026-02-14T00:00:00Z", |
| "token_type": "bearer", |
| "user": { |
| "id": test_user.id, |
| "email": test_user.email |
| } |
| } |
|
|
| response = client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": test_user.email, |
| "password": "test_password", |
| "device_token": "new_device_token", |
| "platform": "ios", |
| "device_info": {"model": "iPhone 14"} |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 401] |
| if response.status_code == 200: |
| data = response.json() |
| assert "access_token" in data |
| assert "refresh_token" in data |
|
|
| def test_mobile_login_invalid_credentials(self, client: TestClient, test_user: User): |
| """Test mobile login with invalid credentials""" |
| with patch('api.auth_routes.authenticate_mobile_user') as mock_auth: |
| mock_auth.return_value = None |
|
|
| response = client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": test_user.email, |
| "password": "wrong_password", |
| "device_token": "device_token", |
| "platform": "android" |
| } |
| ) |
|
|
| |
| assert response.status_code in [400, 401] |
|
|
| def test_mobile_login_missing_fields(self, client: TestClient): |
| """Test mobile login with missing required fields""" |
| response = client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": "test@example.com" |
| |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
|
|
| |
| |
| |
|
|
| class TestBiometricRegistration: |
| """Test biometric registration endpoint""" |
|
|
| def test_register_biometric_success(self, client: TestClient, test_user: User, test_device: MobileDevice): |
| """Test successful biometric registration""" |
| |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/register", |
| json={ |
| "public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA", |
| "device_token": test_device.device_token, |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 404, 500] |
| if response.status_code == 200: |
| data = response.json() |
| assert data["success"] is True |
| assert "challenge" in data |
|
|
| def test_register_biometric_device_not_found(self, client: TestClient, test_user: User): |
| """Test biometric registration with non-existent device""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/register", |
| json={ |
| "public_key": "test_key", |
| "device_token": "nonexistent_device", |
| "platform": "android" |
| } |
| ) |
|
|
| |
| assert response.status_code == 404 |
|
|
| def test_register_biometric_missing_fields(self, client: TestClient, test_user: User): |
| """Test biometric registration with missing fields""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/register", |
| json={ |
| "public_key": "test_key" |
| |
| } |
| ) |
|
|
| assert response.status_code == 422 |
|
|
|
|
| |
| |
| |
|
|
| class TestBiometricAuthentication: |
| """Test biometric authentication endpoint""" |
|
|
| def test_authenticate_biometric_success(self, client: TestClient, test_device: MobileDevice): |
| """Test successful biometric authentication""" |
| |
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = test_device |
|
|
| |
| with patch('api.auth_routes.verify_biometric_signature') as mock_verify: |
| mock_verify.return_value = True |
|
|
| |
| with patch('api.auth_routes.create_mobile_token') as mock_token: |
| mock_token.return_value = { |
| "access_token": "biometric_access_token", |
| "refresh_token": "biometric_refresh_token" |
| } |
|
|
| |
| test_device.device_info = { |
| "biometric_public_key": "test_key", |
| "biometric_challenge": "test_challenge" |
| } |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/authenticate", |
| json={ |
| "device_id": test_device.id, |
| "signature": "valid_signature", |
| "challenge": "test_challenge" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 404] |
| if response.status_code == 200: |
| data = response.json() |
| assert data["success"] is True |
| assert "access_token" in data |
|
|
| def test_authenticate_biometric_invalid_signature(self, client: TestClient, test_device: MobileDevice): |
| """Test biometric authentication with invalid signature""" |
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = test_device |
|
|
| with patch('api.auth_routes.verify_biometric_signature') as mock_verify: |
| mock_verify.return_value = False |
|
|
| test_device.device_info = { |
| "biometric_public_key": "test_key", |
| "biometric_challenge": "test_challenge" |
| } |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/authenticate", |
| json={ |
| "device_id": test_device.id, |
| "signature": "invalid_signature", |
| "challenge": "test_challenge" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 404] |
|
|
| def test_authenticate_biometric_device_not_found(self, client: TestClient): |
| """Test biometric authentication with non-existent device""" |
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = None |
|
|
| response = client.post( |
| "/api/auth/mobile/biometric/authenticate", |
| json={ |
| "device_id": "nonexistent_device", |
| "signature": "signature", |
| "challenge": "challenge" |
| } |
| ) |
|
|
| |
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestMobileTokenRefresh: |
| """Test mobile token refresh endpoint""" |
|
|
| def test_refresh_mobile_token_success(self, client: TestClient, test_device: MobileDevice): |
| """Test successful token refresh""" |
| |
| with patch('jose.jwt.decode') as mock_decode: |
| mock_decode.return_value = { |
| "sub": test_device.user_id, |
| "type": "refresh", |
| "device_id": test_device.id, |
| "exp": int(datetime.utcnow().timestamp()) + 3600 |
| } |
|
|
| |
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = test_device |
|
|
| |
| with patch('api.auth_routes.create_mobile_token') as mock_token: |
| mock_token.return_value = { |
| "access_token": "new_access_token", |
| "refresh_token": "new_refresh_token" |
| } |
|
|
| response = client.post( |
| "/api/auth/mobile/refresh", |
| json={ |
| "refresh_token": "valid_refresh_token" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 401] |
| if response.status_code == 200: |
| data = response.json() |
| assert "access_token" in data or "refresh_token" in data |
|
|
| def test_refresh_mobile_token_invalid(self, client: TestClient): |
| """Test refresh with invalid token""" |
| from jose import JWTError |
|
|
| with patch('jose.jwt.decode') as mock_decode: |
| mock_decode.side_effect = JWTError("Invalid token") |
|
|
| response = client.post( |
| "/api/auth/mobile/refresh", |
| json={ |
| "refresh_token": "invalid_token" |
| } |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 401] |
|
|
| def test_refresh_mobile_token_wrong_type(self, client: TestClient): |
| """Test refresh with wrong token type""" |
| with patch('jose.jwt.decode') as mock_decode: |
| mock_decode.return_value = { |
| "sub": "user_id", |
| "type": "access", |
| "device_id": "device_id" |
| } |
|
|
| response = client.post( |
| "/api/auth/mobile/refresh", |
| json={ |
| "refresh_token": "access_token_instead" |
| } |
| ) |
|
|
| |
| assert response.status_code in [400, 422] |
|
|
|
|
| |
| |
| |
|
|
| class TestGetMobileDevice: |
| """Test get mobile device info endpoint""" |
|
|
| def test_get_device_info_success(self, client: TestClient, test_user: User, test_device: MobileDevice): |
| """Test successful device info retrieval""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = test_device |
|
|
| response = client.get( |
| f"/api/auth/mobile/device?device_id={test_device.id}" |
| ) |
|
|
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert "device_id" in data |
| assert "platform" in data |
|
|
| def test_get_device_info_not_found(self, client: TestClient, test_user: User): |
| """Test getting info for non-existent device""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = None |
|
|
| response = client.get( |
| "/api/auth/mobile/device?device_id=nonexistent_device" |
| ) |
|
|
| |
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestDeleteMobileDevice: |
| """Test delete mobile device endpoint""" |
|
|
| def test_delete_device_success(self, client: TestClient, test_user: User, test_device: MobileDevice): |
| """Test successful device deletion""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = test_device |
|
|
| response = client.delete( |
| f"/api/auth/mobile/device?device_id={test_device.id}" |
| ) |
|
|
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert "success" in data or "message" in data |
|
|
| def test_delete_device_not_found(self, client: TestClient, test_user: User): |
| """Test deleting non-existent device""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| with patch('api.auth_routes.get_mobile_device') as mock_get_device: |
| mock_get_device.return_value = None |
|
|
| response = client.delete( |
| "/api/auth/mobile/device?device_id=nonexistent_device" |
| ) |
|
|
| |
| assert response.status_code == 404 |
|
|
|
|
| |
| |
| |
|
|
| class TestErrorHandling: |
| """Test error handling in auth routes""" |
|
|
| def test_mobile_login_exception_handled(self, client: TestClient, test_user: User): |
| """Test exception handling during mobile login""" |
| with patch('api.auth_routes.authenticate_mobile_user') as mock_auth: |
| mock_auth.side_effect = Exception("Database error") |
|
|
| response = client.post( |
| "/api/auth/mobile/login", |
| json={ |
| "email": test_user.email, |
| "password": "password", |
| "device_token": "token", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code == 500 |
|
|
| def test_biometric_registration_exception_handled(self, client: TestClient, test_user: User): |
| """Test exception handling during biometric registration""" |
| with patch('api.auth_routes.get_current_user') as mock_auth: |
| mock_auth.return_value = test_user |
|
|
| |
| response = client.post( |
| "/api/auth/mobile/biometric/register", |
| json={ |
| "public_key": "key", |
| "device_token": "nonexistent", |
| "platform": "ios" |
| } |
| ) |
|
|
| |
| assert response.status_code in [404, 500] |
|
|
| def test_token_refresh_exception_handled(self, client: TestClient): |
| """Test exception handling during token refresh""" |
| with patch('jose.jwt.decode') as mock_decode: |
| mock_decode.side_effect = Exception("JWT error") |
|
|
| response = client.post( |
| "/api/auth/mobile/refresh", |
| json={ |
| "refresh_token": "malformed_token" |
| } |
| ) |
|
|
| |
| assert response.status_code == 500 |
|
|