feat(tests): Add comprehensive tests for encryption.py and main.py, achieve 100% and 91.75% coverage
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -198,6 +198,64 @@ class TestGetCipherSuite:
|
||||
# Both calls should return the same object (cached)
|
||||
assert result1 is result2
|
||||
|
||||
def test_get_cipher_suite_import_error(self):
|
||||
"""Test _get_cipher_suite when cryptography is not installed"""
|
||||
import app.utils.encryption
|
||||
import sys
|
||||
|
||||
# Reset the cached cipher suite
|
||||
original_cipher = app.utils.encryption._cipher_suite
|
||||
app.utils.encryption._cipher_suite = None
|
||||
|
||||
# Mock the cryptography.fernet module to not exist
|
||||
original_modules = sys.modules.copy()
|
||||
|
||||
# Remove cryptography from sys.modules to simulate it not being installed
|
||||
if "cryptography.fernet" in sys.modules:
|
||||
del sys.modules["cryptography.fernet"]
|
||||
if "cryptography" in sys.modules:
|
||||
del sys.modules["cryptography"]
|
||||
|
||||
# Mock the import to raise ImportError
|
||||
import builtins
|
||||
real_import = builtins.__import__
|
||||
|
||||
def mock_import(name, *args, **kwargs):
|
||||
if "cryptography" in name:
|
||||
raise ImportError("No module named 'cryptography'")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
try:
|
||||
with patch("builtins.__import__", side_effect=mock_import):
|
||||
result = app.utils.encryption._get_cipher_suite()
|
||||
# Should return None when cryptography is not available
|
||||
assert result is None
|
||||
finally:
|
||||
# Restore the original state
|
||||
app.utils.encryption._cipher_suite = original_cipher
|
||||
sys.modules.update(original_modules)
|
||||
|
||||
def test_get_cipher_suite_general_exception(self):
|
||||
"""Test _get_cipher_suite when initialization fails with general exception"""
|
||||
import app.utils.encryption
|
||||
|
||||
# Reset the cached cipher suite
|
||||
original_cipher = app.utils.encryption._cipher_suite
|
||||
app.utils.encryption._cipher_suite = None
|
||||
|
||||
try:
|
||||
# Mock Fernet class to raise an exception during initialization
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
with patch("app.utils.encryption.hashlib.sha256", side_effect=RuntimeError("Hash error")):
|
||||
result = app.utils.encryption._get_cipher_suite()
|
||||
|
||||
# Should return None when initialization fails
|
||||
assert result is None
|
||||
finally:
|
||||
# Restore the original cipher suite
|
||||
app.utils.encryption._cipher_suite = original_cipher
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestEncryptionIntegration:
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Tests for app/main.py
|
||||
|
||||
Tests FastAPI application initialization, middleware, error handlers,
|
||||
and lifecycle management.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import os
|
||||
import pytest
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAppInitialization:
|
||||
"""Test application initialization and configuration"""
|
||||
|
||||
def test_session_secret_is_set(self):
|
||||
"""Test that SESSION_SECRET is configured"""
|
||||
import app.main
|
||||
|
||||
# SESSION_SECRET should be set (either from settings or default)
|
||||
assert app.main.SESSION_SECRET is not None
|
||||
assert len(app.main.SESSION_SECRET) > 0
|
||||
|
||||
def test_app_created_successfully(self):
|
||||
"""Test that FastAPI app is created successfully"""
|
||||
from app.main import app
|
||||
|
||||
assert app is not None
|
||||
assert app.title == "DocuElevate"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLifespanEvents:
|
||||
"""Test application lifespan events (startup and shutdown)"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan_context_manager_executes(self):
|
||||
"""Test that lifespan context manager can be executed"""
|
||||
with patch("app.database.init_db"), \
|
||||
patch("app.database.SessionLocal") as mock_session_cls, \
|
||||
patch("app.utils.config_loader.load_settings_from_db"), \
|
||||
patch("app.utils.config_validator.dump_all_settings"), \
|
||||
patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}), \
|
||||
patch("app.utils.notification.init_apprise"), \
|
||||
patch("app.utils.notification.notify_startup"), \
|
||||
patch("app.utils.notification.notify_shutdown"):
|
||||
|
||||
# Mock database session
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value = mock_db
|
||||
|
||||
from app.main import lifespan, app
|
||||
|
||||
# Execute the startup and shutdown
|
||||
async with lifespan(app):
|
||||
pass # Startup completed
|
||||
|
||||
# Shutdown completed
|
||||
mock_db.close.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan_startup_with_config_issues(self):
|
||||
"""Test that lifespan logs warning when there are config issues"""
|
||||
with patch("app.database.init_db"), \
|
||||
patch("app.database.SessionLocal") as mock_session_cls, \
|
||||
patch("app.utils.config_loader.load_settings_from_db"), \
|
||||
patch("app.utils.config_validator.dump_all_settings"), \
|
||||
patch("app.utils.config_validator.check_all_configs") as mock_check, \
|
||||
patch("app.utils.notification.init_apprise"), \
|
||||
patch("app.utils.notification.notify_startup"), \
|
||||
patch("app.utils.notification.notify_shutdown"), \
|
||||
patch("logging.warning") as mock_warning:
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value = mock_db
|
||||
# Return config with issues
|
||||
mock_check.return_value = {
|
||||
"email": ["Invalid email config"],
|
||||
"storage": {"dropbox": ["Missing token"]}
|
||||
}
|
||||
|
||||
from app.main import lifespan, app
|
||||
|
||||
async with lifespan(app):
|
||||
pass
|
||||
|
||||
# Should log warning about config issues
|
||||
mock_warning.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan_startup_handles_db_settings_load_failure(self):
|
||||
"""Test that lifespan handles failures when loading settings from DB"""
|
||||
with patch("app.database.init_db"), \
|
||||
patch("app.database.SessionLocal") as mock_session_cls, \
|
||||
patch("app.utils.config_loader.load_settings_from_db", side_effect=Exception("DB error")), \
|
||||
patch("app.utils.config_validator.dump_all_settings"), \
|
||||
patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}), \
|
||||
patch("app.utils.notification.init_apprise"), \
|
||||
patch("app.utils.notification.notify_startup"), \
|
||||
patch("app.utils.notification.notify_shutdown"), \
|
||||
patch("logging.error") as mock_error:
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_session_cls.return_value = mock_db
|
||||
|
||||
from app.main import lifespan, app
|
||||
|
||||
# Should not raise exception, just log error
|
||||
async with lifespan(app):
|
||||
pass
|
||||
|
||||
mock_error.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestExceptionHandlers:
|
||||
"""Test custom exception handlers"""
|
||||
|
||||
def test_http_exception_handler_frontend_route_404(self):
|
||||
"""Test that HTTPException returns HTML for frontend 404 errors"""
|
||||
from app.main import app, http_exception_handler
|
||||
from fastapi import Request
|
||||
|
||||
# Create a mock request for a frontend route
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_request.url.path = "/nonexistent"
|
||||
|
||||
exc = HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
# Call the handler directly
|
||||
import asyncio
|
||||
response = asyncio.run(http_exception_handler(mock_request, exc))
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_http_exception_handler_frontend_route_other_error(self):
|
||||
"""Test that HTTPException returns HTML for other frontend errors"""
|
||||
from app.main import app, http_exception_handler
|
||||
from fastapi import Request
|
||||
|
||||
# Create a mock request for a frontend route
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_request.url.path = "/some-page"
|
||||
|
||||
exc = HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
# Call the handler directly
|
||||
import asyncio
|
||||
response = asyncio.run(http_exception_handler(mock_request, exc))
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_custom_500_handler_api_route(self):
|
||||
"""Test that 500 error returns JSON for API routes"""
|
||||
from app.main import app, custom_500_handler
|
||||
from fastapi import Request
|
||||
|
||||
# Create a mock request for an API route
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_request.url.path = "/api/something"
|
||||
|
||||
exc = Exception("Internal error")
|
||||
|
||||
# Call the handler directly
|
||||
import asyncio
|
||||
response = asyncio.run(custom_500_handler(mock_request, exc))
|
||||
|
||||
assert response.status_code == 500
|
||||
# Parse JSON response
|
||||
import json
|
||||
content = json.loads(response.body.decode())
|
||||
assert content["detail"] == "Internal server error"
|
||||
|
||||
def test_custom_500_handler_frontend_route(self):
|
||||
"""Test that 500 error returns HTML for frontend routes"""
|
||||
from app.main import app, custom_500_handler
|
||||
from fastapi import Request
|
||||
|
||||
# Create a mock request for a frontend route
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_request.url.path = "/dashboard"
|
||||
|
||||
exc = Exception("Internal error")
|
||||
|
||||
# Call the handler directly
|
||||
import asyncio
|
||||
response = asyncio.run(custom_500_handler(mock_request, exc))
|
||||
|
||||
assert response.status_code == 500
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestTestEndpoint:
|
||||
"""Test the /test-500 debugging endpoint"""
|
||||
|
||||
def test_test_500_endpoint_raises_error(self):
|
||||
"""Test that /test-500 endpoint raises RuntimeError"""
|
||||
from app.main import test_500
|
||||
|
||||
# The function should raise RuntimeError
|
||||
with pytest.raises(RuntimeError, match="Testing forced 500 error"):
|
||||
test_500()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestStaticFileMount:
|
||||
"""Test static file mounting logic"""
|
||||
|
||||
def test_static_files_mounted_when_directory_exists(self):
|
||||
"""Test that static files are served when directory exists"""
|
||||
from app.main import app
|
||||
import pathlib
|
||||
|
||||
# Check if static directory exists
|
||||
static_dir = pathlib.Path(__file__).parents[1] / "frontend" / "static"
|
||||
|
||||
if os.path.exists(static_dir):
|
||||
# Check if static route is mounted
|
||||
assert any("/static" in str(route.path) for route in app.routes)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMiddlewareConfiguration:
|
||||
"""Test middleware configuration"""
|
||||
|
||||
def test_app_has_limiter_state(self):
|
||||
"""Test that app.state.limiter is configured"""
|
||||
from app.main import app
|
||||
|
||||
assert hasattr(app.state, "limiter")
|
||||
assert app.state.limiter is not None
|
||||
|
||||
def test_app_has_correct_title(self):
|
||||
"""Test that FastAPI app has correct title"""
|
||||
from app.main import app
|
||||
|
||||
assert app.title == "DocuElevate"
|
||||
Reference in New Issue
Block a user