""" Integration tests for authentication API endpoints. Tests POST /auth/login, GET /auth/verify, POST /auth/logout endpoints with valid/invalid credentials and tokens. """ import pytest import sys import os from fastapi.testclient import TestClient from unittest.mock import patch, Mock # Add source directory to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) # Import after path setup from src.server.fastapi_app import app # noqa: E402 @pytest.fixture def client(): """Test client for FastAPI app.""" return TestClient(app) @pytest.fixture def mock_auth_settings(): """Mock settings for authentication tests.""" settings = Mock() settings.jwt_secret_key = "test-secret-key" settings.password_salt = "test-salt" settings.master_password = "test_password" settings.master_password_hash = None settings.token_expiry_hours = 1 return settings @pytest.mark.integration class TestAuthLogin: """Test authentication login endpoint.""" def test_login_valid_credentials(self, client, mock_auth_settings): """Test login with valid credentials.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/login", json={"password": "test_password"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "token" in data assert "expires_at" in data assert data["message"] == "Login successful" def test_login_invalid_credentials(self, client, mock_auth_settings): """Test login with invalid credentials.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/login", json={"password": "wrong_password"} ) assert response.status_code == 401 data = response.json() assert data["success"] is False assert "token" not in data assert "Invalid password" in data["message"] def test_login_missing_password(self, client): """Test login with missing password field.""" response = client.post( "/auth/login", json={} ) assert response.status_code == 422 # Validation error def test_login_empty_password(self, client, mock_auth_settings): """Test login with empty password.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/login", json={"password": ""} ) assert response.status_code == 422 # Validation error (min_length=1) def test_login_invalid_json(self, client): """Test login with invalid JSON payload.""" response = client.post( "/auth/login", data="invalid json", headers={"Content-Type": "application/json"} ) assert response.status_code == 422 def test_login_wrong_content_type(self, client): """Test login with wrong content type.""" response = client.post( "/auth/login", data="password=test_password" ) assert response.status_code == 422 @pytest.mark.integration class TestAuthVerify: """Test authentication token verification endpoint.""" def test_verify_valid_token(self, client, mock_auth_settings, valid_jwt_token): """Test token verification with valid token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.get( "/auth/verify", headers={"Authorization": f"Bearer {valid_jwt_token}"} ) assert response.status_code == 200 data = response.json() assert data["valid"] is True assert data["user"] == "test_user" assert "expires_at" in data def test_verify_expired_token(self, client, mock_auth_settings, expired_jwt_token): """Test token verification with expired token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.get( "/auth/verify", headers={"Authorization": f"Bearer {expired_jwt_token}"} ) assert response.status_code == 401 data = response.json() assert data["valid"] is False assert "expired" in data["message"].lower() def test_verify_invalid_token(self, client, mock_auth_settings): """Test token verification with invalid token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.get( "/auth/verify", headers={"Authorization": "Bearer invalid.token.here"} ) assert response.status_code == 401 data = response.json() assert data["valid"] is False def test_verify_missing_token(self, client): """Test token verification without token.""" response = client.get("/auth/verify") assert response.status_code == 403 # Forbidden - no credentials def test_verify_malformed_header(self, client): """Test token verification with malformed authorization header.""" response = client.get( "/auth/verify", headers={"Authorization": "InvalidFormat token"} ) assert response.status_code == 403 def test_verify_empty_token(self, client): """Test token verification with empty token.""" response = client.get( "/auth/verify", headers={"Authorization": "Bearer "} ) assert response.status_code == 401 @pytest.mark.integration class TestAuthLogout: """Test authentication logout endpoint.""" def test_logout_valid_token(self, client, mock_auth_settings, valid_jwt_token): """Test logout with valid token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/logout", headers={"Authorization": f"Bearer {valid_jwt_token}"} ) assert response.status_code == 200 data = response.json() assert data["success"] is True assert "logged out" in data["message"].lower() def test_logout_invalid_token(self, client, mock_auth_settings): """Test logout with invalid token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/logout", headers={"Authorization": "Bearer invalid.token"} ) assert response.status_code == 401 def test_logout_missing_token(self, client): """Test logout without token.""" response = client.post("/auth/logout") assert response.status_code == 403 def test_logout_expired_token(self, client, mock_auth_settings, expired_jwt_token): """Test logout with expired token.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): response = client.post( "/auth/logout", headers={"Authorization": f"Bearer {expired_jwt_token}"} ) assert response.status_code == 401 @pytest.mark.integration class TestAuthFlow: """Test complete authentication flow.""" def test_complete_login_verify_logout_flow(self, client, mock_auth_settings): """Test complete authentication flow: login -> verify -> logout.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): # Step 1: Login login_response = client.post( "/auth/login", json={"password": "test_password"} ) assert login_response.status_code == 200 login_data = login_response.json() token = login_data["token"] # Step 2: Verify token verify_response = client.get( "/auth/verify", headers={"Authorization": f"Bearer {token}"} ) assert verify_response.status_code == 200 verify_data = verify_response.json() assert verify_data["valid"] is True # Step 3: Logout logout_response = client.post( "/auth/logout", headers={"Authorization": f"Bearer {token}"} ) assert logout_response.status_code == 200 logout_data = logout_response.json() assert logout_data["success"] is True def test_multiple_login_attempts(self, client, mock_auth_settings): """Test multiple login attempts with rate limiting consideration.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): # Multiple successful logins should work for _ in range(3): response = client.post( "/auth/login", json={"password": "test_password"} ) assert response.status_code == 200 # Failed login attempts for _ in range(3): response = client.post( "/auth/login", json={"password": "wrong_password"} ) assert response.status_code == 401 def test_concurrent_sessions(self, client, mock_auth_settings): """Test that multiple valid tokens can exist simultaneously.""" with patch('src.server.fastapi_app.settings', mock_auth_settings): # Get first token response1 = client.post( "/auth/login", json={"password": "test_password"} ) token1 = response1.json()["token"] # Get second token response2 = client.post( "/auth/login", json={"password": "test_password"} ) token2 = response2.json()["token"] # Both tokens should be valid verify1 = client.get( "/auth/verify", headers={"Authorization": f"Bearer {token1}"} ) verify2 = client.get( "/auth/verify", headers={"Authorization": f"Bearer {token2}"} ) assert verify1.status_code == 200 assert verify2.status_code == 200