test: expand auth and encryption test coverage with 299 new test lines

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-09 21:16:55 +00:00
parent 91755ee5e6
commit acb8951dd9
2 changed files with 299 additions and 1 deletions
+189 -1
View File
@@ -1,9 +1,11 @@
"""Tests for app/auth.py module."""
import hashlib
import os
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from starlette.testclient import TestClient
from fastapi import Request
from fastapi import Request, status
from starlette.responses import RedirectResponse
from app.auth import get_current_user, get_gravatar_url, require_login
@@ -26,6 +28,13 @@ class TestGetCurrentUser:
result = get_current_user(mock_request)
assert result is None
def test_returns_none_when_user_is_none(self):
"""Test that get_current_user returns None when user is None."""
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": None}
result = get_current_user(mock_request)
assert result is None
@pytest.mark.unit
class TestGetGravatarUrl:
@@ -65,6 +74,103 @@ class TestRequireLogin:
# When AUTH_ENABLED is False, the decorator returns the function unchanged
assert decorated is my_func
@pytest.mark.asyncio
async def test_redirects_to_login_when_no_user(self):
"""Test that require_login redirects to /login when no user in session and auth is enabled."""
with patch("app.auth.AUTH_ENABLED", True):
# Import fresh to get patched AUTH_ENABLED
from app.auth import require_login
@require_login
async def protected_endpoint(request: Request):
return {"message": "success"}
mock_request = MagicMock(spec=Request)
mock_request.session = {}
mock_request.url = MagicMock()
mock_request.url.__str__ = MagicMock(return_value="http://test.com/protected")
result = await protected_endpoint(mock_request)
assert isinstance(result, RedirectResponse)
assert result.status_code == status.HTTP_302_FOUND
assert "/login" in str(result.headers.get("location"))
@pytest.mark.asyncio
async def test_saves_redirect_url_when_not_authenticated(self):
"""Test that require_login saves the original URL in session."""
with patch("app.auth.AUTH_ENABLED", True):
from app.auth import require_login
@require_login
async def protected_endpoint(request: Request):
return {"message": "success"}
mock_request = MagicMock(spec=Request)
mock_request.session = {}
mock_request.url = MagicMock()
original_url = "http://test.com/protected/page?param=value"
mock_request.url.__str__ = MagicMock(return_value=original_url)
await protected_endpoint(mock_request)
assert mock_request.session.get("redirect_after_login") == original_url
@pytest.mark.asyncio
async def test_allows_access_when_user_in_session(self):
"""Test that require_login allows access when user is in session."""
with patch("app.auth.AUTH_ENABLED", True):
from app.auth import require_login
@require_login
async def protected_endpoint(request: Request):
return {"message": "success", "user": request.session.get("user")}
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"id": "test_user", "name": "Test"}}
result = await protected_endpoint(mock_request)
assert result["message"] == "success"
assert result["user"]["id"] == "test_user"
@pytest.mark.asyncio
async def test_handles_async_functions(self):
"""Test that require_login correctly wraps async functions."""
with patch("app.auth.AUTH_ENABLED", True):
from app.auth import require_login
@require_login
async def async_endpoint(request: Request, param: str):
return {"message": "async", "param": param}
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"id": "test"}}
result = await async_endpoint(mock_request, param="test_value")
assert result["message"] == "async"
assert result["param"] == "test_value"
def test_handles_sync_functions(self):
"""Test that require_login correctly wraps sync functions."""
with patch("app.auth.AUTH_ENABLED", True):
from app.auth import require_login
@require_login
def sync_endpoint(request: Request, param: str):
return {"message": "sync", "param": param}
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"id": "test"}}
# Call the decorated sync function
import asyncio
result = asyncio.run(sync_endpoint(request=mock_request, param="test_value"))
assert result["message"] == "sync"
assert result["param"] == "test_value"
@pytest.mark.integration
class TestWhoamiEndpoint:
@@ -84,3 +190,85 @@ class TestWhoamiEndpoint:
assert response.status_code == 200
data = response.json()
assert "message" in data
@pytest.mark.integration
class TestAuthEndpoints:
"""Integration tests for authentication endpoints."""
def test_login_page_not_available_when_auth_disabled(self, client):
"""Test that login page returns 404 when auth is disabled (default in tests)."""
# In the test environment, AUTH_ENABLED is False by default
response = client.get("/login")
# When auth is disabled, the auth routes are not registered
assert response.status_code == 404
def test_oauth_login_not_available_when_auth_disabled(self, client):
"""Test that /oauth-login returns 404 when auth is disabled."""
response = client.get("/oauth-login")
assert response.status_code == 404
def test_logout_not_available_when_auth_disabled(self, client):
"""Test that /logout returns 404 when auth is disabled."""
response = client.get("/logout")
assert response.status_code == 404
def test_auth_post_not_available_when_auth_disabled(self, client):
"""Test that POST /auth returns 404 when auth is disabled."""
response = client.post("/auth", data={"username": "admin", "password": "test"})
assert response.status_code == 404
@pytest.mark.unit
class TestSessionValidation:
"""Tests for session validation edge cases."""
def test_empty_user_object(self):
"""Test get_current_user with empty user object."""
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {}}
result = get_current_user(mock_request)
assert result == {}
def test_user_object_missing_id(self):
"""Test session with user missing id field."""
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"name": "Test", "email": "test@example.com"}}
result = get_current_user(mock_request)
# Should still return the user object even if id is missing
assert result["name"] == "Test"
assert "id" not in result
def test_user_object_with_extra_fields(self):
"""Test session with user having extra fields."""
mock_request = MagicMock(spec=Request)
user = {
"id": "123",
"name": "Test",
"email": "test@example.com",
"is_admin": True,
"groups": ["admin"],
"picture": "https://example.com/pic.jpg",
}
mock_request.session = {"user": user}
result = get_current_user(mock_request)
assert result == user
@pytest.mark.asyncio
async def test_require_login_with_user_missing_required_fields(self):
"""Test require_login with user object missing typical fields."""
with patch("app.auth.AUTH_ENABLED", True):
from app.auth import require_login
@require_login
async def protected_endpoint(request: Request):
return {"message": "success"}
# User object exists but is minimal
mock_request = MagicMock(spec=Request)
mock_request.session = {"user": {"id": "123"}} # Missing name, email, etc.
result = await protected_endpoint(mock_request)
# Should still allow access as long as user key exists
assert result["message"] == "success"
+110
View File
@@ -229,3 +229,113 @@ class TestEncryptionIntegration:
assert encrypted != original
assert encrypted.startswith("enc:")
assert decrypted == original
def test_real_encryption_round_trip(self):
"""Test actual encryption/decryption with real cryptography library."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
# Skip if encryption is not available
if not is_encryption_available():
pytest.skip("Encryption not available (cryptography library not installed)")
original_value = "my_super_secret_password_123"
# Encrypt the value
encrypted = encrypt_value(original_value)
# Should be encrypted (has enc: prefix)
assert encrypted.startswith("enc:")
assert encrypted != original_value
# Decrypt should return original value
decrypted = decrypt_value(encrypted)
assert decrypted == original_value
def test_encryption_with_special_characters(self):
"""Test encryption with special characters and symbols."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
original = "P@ssw0rd!#$%^&*()_+-=[]{}|;:',.<>?/~`"
encrypted = encrypt_value(original)
decrypted = decrypt_value(encrypted)
assert decrypted == original
def test_encryption_with_unicode(self):
"""Test encryption with unicode characters."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
original = "Hello 世界 🌍 Привет мир"
encrypted = encrypt_value(original)
decrypted = decrypt_value(encrypted)
assert decrypted == original
def test_encryption_with_long_string(self):
"""Test encryption with very long strings."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
# Create a long string (1000 characters)
original = "A" * 1000
encrypted = encrypt_value(original)
decrypted = decrypt_value(encrypted)
assert decrypted == original
assert len(decrypted) == 1000
def test_encryption_with_newlines_and_whitespace(self):
"""Test encryption preserves newlines and whitespace."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
original = "line1\n line2\t\ttabbed\r\nline3 "
encrypted = encrypt_value(original)
decrypted = decrypt_value(encrypted)
assert decrypted == original
def test_encryption_with_json_string(self):
"""Test encryption with JSON string."""
from app.utils.encryption import encrypt_value, decrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
original = '{"key": "value", "nested": {"array": [1, 2, 3]}}'
encrypted = encrypt_value(original)
decrypted = decrypt_value(encrypted)
assert decrypted == original
def test_multiple_encrypt_same_value_produces_different_ciphertext(self):
"""Test that encrypting the same value twice produces different ciphertext (if using random IV)."""
from app.utils.encryption import encrypt_value, is_encryption_available
if not is_encryption_available():
pytest.skip("Encryption not available")
original = "same_value"
encrypted1 = encrypt_value(original)
encrypted2 = encrypt_value(original)
# Both should be encrypted
assert encrypted1.startswith("enc:")
assert encrypted2.startswith("enc:")
# Fernet uses timestamp-based encryption, so they might be different
# (depending on timing). This test documents the behavior.
# We'll just verify both decrypt correctly
from app.utils.encryption import decrypt_value
assert decrypt_value(encrypted1) == original
assert decrypt_value(encrypted2) == original