- 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
313 lines
11 KiB
Python
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 |