Aniworld/src/tests/integration/test_auth_endpoints.py
Lukas Pupka-Lipinski 7f27ff823a Add comprehensive authentication and security tests
- Unit tests for password hashing, JWT generation/validation, session timeout
- Integration tests for auth endpoints (login, verify, logout)
- E2E tests for complete authentication flows
- Tests cover valid/invalid credentials, token expiry, error handling
- Added security tests to prevent information leakage
2025-10-06 10:50:19 +02:00

313 lines
11 KiB
Python

"""
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