style: apply ruff auto-fix

- Auto-formatted code with ruff format
- Applied ruff linting fixes with --fix

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-02-14 00:16:21 +00:00
parent 992e420556
commit e84c031538
8 changed files with 146 additions and 147 deletions
+19 -12
View File
@@ -842,8 +842,9 @@ class TestDeleteFileExceptions:
db_session.commit() db_session.commit()
file_id = file.id file_id = file.id
with patch("app.config.settings") as mock_settings, patch.object( with (
db_session, "delete", side_effect=Exception("Database error") patch("app.config.settings") as mock_settings,
patch.object(db_session, "delete", side_effect=Exception("Database error")),
): ):
mock_settings.allow_file_delete = True mock_settings.allow_file_delete = True
response = client.delete(f"/api/files/{file_id}") response = client.delete(f"/api/files/{file_id}")
@@ -869,8 +870,9 @@ class TestDeleteFileExceptions:
def failing_commit(): def failing_commit():
raise Exception("Database commit error") raise Exception("Database commit error")
with patch("app.config.settings") as mock_settings, patch.object( with (
db_session, "commit", side_effect=failing_commit patch("app.config.settings") as mock_settings,
patch.object(db_session, "commit", side_effect=failing_commit),
): ):
mock_settings.allow_file_delete = True mock_settings.allow_file_delete = True
response = client.post("/api/files/bulk-delete", json=[file_id]) response = client.post("/api/files/bulk-delete", json=[file_id])
@@ -948,7 +950,9 @@ class TestRetryPipelineSteps:
db_session.add(file) db_session.add(file)
db_session.commit() db_session.commit()
with patch("app.tasks.process_with_azure_document_intelligence.process_with_azure_document_intelligence") as mock_task: with patch(
"app.tasks.process_with_azure_document_intelligence.process_with_azure_document_intelligence"
) as mock_task:
mock_task.delay.return_value = Mock(id="task-azure") mock_task.delay.return_value = Mock(id="task-azure")
result = _retry_pipeline_step(file, "process_with_azure_document_intelligence", db_session) result = _retry_pipeline_step(file, "process_with_azure_document_intelligence", db_session)
assert result["task_id"] == "task-azure" assert result["task_id"] == "task-azure"
@@ -991,8 +995,9 @@ class TestRetryPipelineSteps:
db_session.add(file) db_session.add(file)
db_session.commit() db_session.commit()
with patch("app.tasks.extract_metadata_with_gpt.extract_metadata_with_gpt") as mock_task, patch( with (
"app.api.files._extract_text_from_pdf", return_value="Sample text" patch("app.tasks.extract_metadata_with_gpt.extract_metadata_with_gpt") as mock_task,
patch("app.api.files._extract_text_from_pdf", return_value="Sample text"),
): ):
mock_task.delay.return_value = Mock(id="task-gpt") mock_task.delay.return_value = Mock(id="task-gpt")
result = _retry_pipeline_step(file, "extract_metadata_with_gpt", db_session) result = _retry_pipeline_step(file, "extract_metadata_with_gpt", db_session)
@@ -1035,8 +1040,9 @@ class TestRetryPipelineSteps:
db_session.add(file) db_session.add(file)
db_session.commit() db_session.commit()
with patch("app.tasks.extract_metadata_with_gpt.extract_metadata_with_gpt") as mock_task, patch( with (
"app.api.files._extract_text_from_pdf", return_value="Sample text" patch("app.tasks.extract_metadata_with_gpt.extract_metadata_with_gpt") as mock_task,
patch("app.api.files._extract_text_from_pdf", return_value="Sample text"),
): ):
mock_task.delay.return_value = Mock(id="task-embed") mock_task.delay.return_value = Mock(id="task-embed")
result = _retry_pipeline_step(file, "embed_metadata_into_pdf", db_session) result = _retry_pipeline_step(file, "embed_metadata_into_pdf", db_session)
@@ -1065,9 +1071,10 @@ class TestRetryUploadTasks:
db_session.add(file) db_session.add(file)
db_session.commit() db_session.commit()
with patch("app.api.files.settings") as mock_settings, patch( with (
"app.tasks.upload_to_dropbox.upload_to_dropbox" patch("app.api.files.settings") as mock_settings,
) as mock_task: patch("app.tasks.upload_to_dropbox.upload_to_dropbox") as mock_task,
):
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_task.delay.return_value = Mock(id="task-dropbox") mock_task.delay.return_value = Mock(id="task-dropbox")
response = client.post(f"/api/files/{file.id}/retry-subtask?subtask_name=upload_to_dropbox") response = client.post(f"/api/files/{file.id}/retry-subtask?subtask_name=upload_to_dropbox")
+6 -20
View File
@@ -1,7 +1,5 @@
"""Tests for app/api/process.py module.""" """Tests for app/api/process.py module."""
import os
from pathlib import Path
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest import pytest
@@ -169,9 +167,7 @@ class TestProcessEndpoints:
test_file = tmp_path / "test1.pdf" test_file = tmp_path / "test1.pdf"
test_file.write_text("test content") test_file.write_text("test content")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10 mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-1") mock_task.delay.return_value = Mock(id="task-1")
@@ -192,9 +188,7 @@ class TestProcessEndpoints:
for i in range(3): for i in range(3):
(tmp_path / f"test{i}.pdf").write_text(f"test content {i}") (tmp_path / f"test{i}.pdf").write_text(f"test content {i}")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10 mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-id") mock_task.delay.return_value = Mock(id="task-id")
@@ -214,9 +208,7 @@ class TestProcessEndpoints:
for i in range(12): for i in range(12):
(tmp_path / f"test{i}.pdf").write_text(f"test content {i}") (tmp_path / f"test{i}.pdf").write_text(f"test content {i}")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10 mock_settings.processall_throttle_threshold = 10
mock_settings.processall_throttle_delay = 5 mock_settings.processall_throttle_delay = 5
@@ -246,9 +238,7 @@ class TestProcessEndpoints:
(tmp_path / "test.docx").write_text("word content") (tmp_path / "test.docx").write_text("word content")
(tmp_path / "test.jpg").write_text("image content") (tmp_path / "test.jpg").write_text("image content")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10 mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-id") mock_task.delay.return_value = Mock(id="task-id")
@@ -270,9 +260,7 @@ class TestProcessEndpoints:
for i in range(threshold): for i in range(threshold):
(tmp_path / f"at_threshold_{i}.pdf").write_text(f"content {i}") (tmp_path / f"at_threshold_{i}.pdf").write_text(f"content {i}")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = threshold mock_settings.processall_throttle_threshold = threshold
mock_settings.processall_throttle_delay = 2 mock_settings.processall_throttle_delay = 2
@@ -291,9 +279,7 @@ class TestProcessEndpoints:
for i in range(threshold + 1): for i in range(threshold + 1):
(tmp_path / f"above_threshold_{i}.pdf").write_text(f"content {i}") (tmp_path / f"above_threshold_{i}.pdf").write_text(f"content {i}")
with patch("app.api.process.settings") as mock_settings, patch( with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
"app.api.process.process_document"
) as mock_task:
mock_settings.workdir = str(tmp_path) mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = threshold mock_settings.processall_throttle_threshold = threshold
mock_settings.processall_throttle_delay = 2 mock_settings.processall_throttle_delay = 2
+3 -3
View File
@@ -211,16 +211,16 @@ class TestTaskFailureHandler:
mock_settings.notify_on_task_failure = True mock_settings.notify_on_task_failure = True
# Import to ensure signal is connected # Import to ensure signal is connected
from app.celery_app import task_failure_handler
# Import the signal # Import the signal
from celery.signals import task_failure from celery.signals import task_failure
from app.celery_app import task_failure_handler
# The handler should be connected to the signal # The handler should be connected to the signal
# We can test this by verifying the signal has receivers # We can test this by verifying the signal has receivers
receivers = task_failure.receivers receivers = task_failure.receivers
assert len(receivers) > 0 assert len(receivers) > 0
# Simply verify that importing the handler doesn't cause errors # Simply verify that importing the handler doesn't cause errors
# The actual signal connection is tested implicitly by the other tests # The actual signal connection is tested implicitly by the other tests
assert callable(task_failure_handler) assert callable(task_failure_handler)
+6 -6
View File
@@ -173,16 +173,16 @@ class TestSchemaMigrations:
inspector = inspect(engine) inspector = inspect(engine)
columns = {col["name"]: col for col in inspector.get_columns("files")} columns = {col["name"]: col for col in inspector.get_columns("files")}
assert "original_file_path" in columns assert "original_file_path" in columns
assert columns["original_file_path"]["type"].__class__.__name__ in ("VARCHAR", "String", "TEXT") assert columns["original_file_path"]["type"].__class__.__name__ in ("VARCHAR", "String", "TEXT")
assert "processed_file_path" in columns assert "processed_file_path" in columns
assert columns["processed_file_path"]["type"].__class__.__name__ in ("VARCHAR", "String", "TEXT") assert columns["processed_file_path"]["type"].__class__.__name__ in ("VARCHAR", "String", "TEXT")
assert "is_duplicate" in columns assert "is_duplicate" in columns
assert columns["is_duplicate"]["type"].__class__.__name__ in ("BOOLEAN", "Integer") assert columns["is_duplicate"]["type"].__class__.__name__ in ("BOOLEAN", "Integer")
assert "duplicate_of_id" in columns assert "duplicate_of_id" in columns
assert columns["duplicate_of_id"]["type"].__class__.__name__ in ("INTEGER", "Integer") assert columns["duplicate_of_id"]["type"].__class__.__name__ in ("INTEGER", "Integer")
@@ -290,7 +290,7 @@ class TestSchemaMigrations:
from sqlalchemy import inspect from sqlalchemy import inspect
inspector = inspect(engine) inspector = inspect(engine)
processing_log_columns = [col["name"] for col in inspector.get_columns("processing_logs")] processing_log_columns = [col["name"] for col in inspector.get_columns("processing_logs")]
assert "detail" in processing_log_columns assert "detail" in processing_log_columns
@@ -318,7 +318,7 @@ class TestInitDbErrors:
mock_url.get_backend_name.return_value = "sqlite" mock_url.get_backend_name.return_value = "sqlite"
mock_url.database = ":memory:" mock_url.database = ":memory:"
mock_make_url.return_value = mock_url mock_make_url.return_value = mock_url
mock_base.metadata.create_all.side_effect = exc.SQLAlchemyError("Database error") mock_base.metadata.create_all.side_effect = exc.SQLAlchemyError("Database error")
with pytest.raises(exc.SQLAlchemyError): with pytest.raises(exc.SQLAlchemyError):
+6 -5
View File
@@ -200,24 +200,26 @@ class TestGetCipherSuite:
def test_get_cipher_suite_import_error(self): def test_get_cipher_suite_import_error(self):
"""Test _get_cipher_suite when cryptography is not installed""" """Test _get_cipher_suite when cryptography is not installed"""
import app.utils.encryption
import sys import sys
import app.utils.encryption
# Reset the cached cipher suite # Reset the cached cipher suite
original_cipher = app.utils.encryption._cipher_suite original_cipher = app.utils.encryption._cipher_suite
app.utils.encryption._cipher_suite = None app.utils.encryption._cipher_suite = None
# Mock the cryptography.fernet module to not exist # Mock the cryptography.fernet module to not exist
original_modules = sys.modules.copy() original_modules = sys.modules.copy()
# Remove cryptography from sys.modules to simulate it not being installed # Remove cryptography from sys.modules to simulate it not being installed
if "cryptography.fernet" in sys.modules: if "cryptography.fernet" in sys.modules:
del sys.modules["cryptography.fernet"] del sys.modules["cryptography.fernet"]
if "cryptography" in sys.modules: if "cryptography" in sys.modules:
del sys.modules["cryptography"] del sys.modules["cryptography"]
# Mock the import to raise ImportError # Mock the import to raise ImportError
import builtins import builtins
real_import = builtins.__import__ real_import = builtins.__import__
def mock_import(name, *args, **kwargs): def mock_import(name, *args, **kwargs):
@@ -245,8 +247,7 @@ class TestGetCipherSuite:
try: try:
# Mock Fernet class to raise an exception during initialization # 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")): with patch("app.utils.encryption.hashlib.sha256", side_effect=RuntimeError("Hash error")):
result = app.utils.encryption._get_cipher_suite() result = app.utils.encryption._get_cipher_suite()
+86 -76
View File
@@ -5,11 +5,11 @@ Tests FastAPI application initialization, middleware, error handlers,
and lifecycle management. and lifecycle management.
""" """
from unittest.mock import Mock, patch, MagicMock
import os import os
from unittest.mock import MagicMock, patch
import pytest import pytest
from fastapi import HTTPException, status from fastapi import HTTPException
from fastapi.testclient import TestClient
@pytest.mark.unit @pytest.mark.unit
@@ -19,7 +19,7 @@ class TestAppInitialization:
def test_session_secret_is_set(self): def test_session_secret_is_set(self):
"""Test that SESSION_SECRET is configured""" """Test that SESSION_SECRET is configured"""
import app.main import app.main
# SESSION_SECRET should be set (either from settings or default) # SESSION_SECRET should be set (either from settings or default)
assert app.main.SESSION_SECRET is not None assert app.main.SESSION_SECRET is not None
assert len(app.main.SESSION_SECRET) > 0 assert len(app.main.SESSION_SECRET) > 0
@@ -27,7 +27,7 @@ class TestAppInitialization:
def test_app_created_successfully(self): def test_app_created_successfully(self):
"""Test that FastAPI app is created successfully""" """Test that FastAPI app is created successfully"""
from app.main import app from app.main import app
assert app is not None assert app is not None
assert app.title == "DocuElevate" assert app.title == "DocuElevate"
@@ -39,79 +39,79 @@ class TestLifespanEvents:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lifespan_context_manager_executes(self): async def test_lifespan_context_manager_executes(self):
"""Test that lifespan context manager can be executed""" """Test that lifespan context manager can be executed"""
with patch("app.database.init_db"), \ with (
patch("app.database.SessionLocal") as mock_session_cls, \ patch("app.database.init_db"),
patch("app.utils.config_loader.load_settings_from_db"), \ patch("app.database.SessionLocal") as mock_session_cls,
patch("app.utils.config_validator.dump_all_settings"), \ patch("app.utils.config_loader.load_settings_from_db"),
patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}), \ patch("app.utils.config_validator.dump_all_settings"),
patch("app.utils.notification.init_apprise"), \ patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}),
patch("app.utils.notification.notify_startup"), \ patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_shutdown"): patch("app.utils.notification.notify_startup"),
patch("app.utils.notification.notify_shutdown"),
):
# Mock database session # Mock database session
mock_db = MagicMock() mock_db = MagicMock()
mock_session_cls.return_value = mock_db mock_session_cls.return_value = mock_db
from app.main import lifespan, app from app.main import app, lifespan
# Execute the startup and shutdown # Execute the startup and shutdown
async with lifespan(app): async with lifespan(app):
pass # Startup completed pass # Startup completed
# Shutdown completed # Shutdown completed
mock_db.close.assert_called() mock_db.close.assert_called()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lifespan_startup_with_config_issues(self): async def test_lifespan_startup_with_config_issues(self):
"""Test that lifespan logs warning when there are config issues""" """Test that lifespan logs warning when there are config issues"""
with patch("app.database.init_db"), \ with (
patch("app.database.SessionLocal") as mock_session_cls, \ patch("app.database.init_db"),
patch("app.utils.config_loader.load_settings_from_db"), \ patch("app.database.SessionLocal") as mock_session_cls,
patch("app.utils.config_validator.dump_all_settings"), \ patch("app.utils.config_loader.load_settings_from_db"),
patch("app.utils.config_validator.check_all_configs") as mock_check, \ patch("app.utils.config_validator.dump_all_settings"),
patch("app.utils.notification.init_apprise"), \ patch("app.utils.config_validator.check_all_configs") as mock_check,
patch("app.utils.notification.notify_startup"), \ patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_shutdown"), \ patch("app.utils.notification.notify_startup"),
patch("logging.warning") as mock_warning: patch("app.utils.notification.notify_shutdown"),
patch("logging.warning") as mock_warning,
):
mock_db = MagicMock() mock_db = MagicMock()
mock_session_cls.return_value = mock_db mock_session_cls.return_value = mock_db
# Return config with issues # Return config with issues
mock_check.return_value = { mock_check.return_value = {"email": ["Invalid email config"], "storage": {"dropbox": ["Missing token"]}}
"email": ["Invalid email config"],
"storage": {"dropbox": ["Missing token"]} from app.main import app, lifespan
}
from app.main import lifespan, app
async with lifespan(app): async with lifespan(app):
pass pass
# Should log warning about config issues # Should log warning about config issues
mock_warning.assert_called() mock_warning.assert_called()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lifespan_startup_handles_db_settings_load_failure(self): async def test_lifespan_startup_handles_db_settings_load_failure(self):
"""Test that lifespan handles failures when loading settings from DB""" """Test that lifespan handles failures when loading settings from DB"""
with patch("app.database.init_db"), \ with (
patch("app.database.SessionLocal") as mock_session_cls, \ patch("app.database.init_db"),
patch("app.utils.config_loader.load_settings_from_db", side_effect=Exception("DB error")), \ patch("app.database.SessionLocal") as mock_session_cls,
patch("app.utils.config_validator.dump_all_settings"), \ patch("app.utils.config_loader.load_settings_from_db", side_effect=Exception("DB error")),
patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}), \ patch("app.utils.config_validator.dump_all_settings"),
patch("app.utils.notification.init_apprise"), \ patch("app.utils.config_validator.check_all_configs", return_value={"email": [], "storage": {}}),
patch("app.utils.notification.notify_startup"), \ patch("app.utils.notification.init_apprise"),
patch("app.utils.notification.notify_shutdown"), \ patch("app.utils.notification.notify_startup"),
patch("logging.error") as mock_error: patch("app.utils.notification.notify_shutdown"),
patch("logging.error") as mock_error,
):
mock_db = MagicMock() mock_db = MagicMock()
mock_session_cls.return_value = mock_db mock_session_cls.return_value = mock_db
from app.main import lifespan, app from app.main import app, lifespan
# Should not raise exception, just log error # Should not raise exception, just log error
async with lifespan(app): async with lifespan(app):
pass pass
mock_error.assert_called() mock_error.assert_called()
@@ -121,74 +121,83 @@ class TestExceptionHandlers:
def test_http_exception_handler_frontend_route_404(self): def test_http_exception_handler_frontend_route_404(self):
"""Test that HTTPException returns HTML for frontend 404 errors""" """Test that HTTPException returns HTML for frontend 404 errors"""
from app.main import app, http_exception_handler
from fastapi import Request from fastapi import Request
from app.main import http_exception_handler
# Create a mock request for a frontend route # Create a mock request for a frontend route
mock_request = MagicMock(spec=Request) mock_request = MagicMock(spec=Request)
mock_request.url.path = "/nonexistent" mock_request.url.path = "/nonexistent"
exc = HTTPException(status_code=404, detail="Not found") exc = HTTPException(status_code=404, detail="Not found")
# Call the handler directly # Call the handler directly
import asyncio import asyncio
response = asyncio.run(http_exception_handler(mock_request, exc)) response = asyncio.run(http_exception_handler(mock_request, exc))
assert response.status_code == 404 assert response.status_code == 404
def test_http_exception_handler_frontend_route_other_error(self): def test_http_exception_handler_frontend_route_other_error(self):
"""Test that HTTPException returns HTML for other frontend errors""" """Test that HTTPException returns HTML for other frontend errors"""
from app.main import app, http_exception_handler
from fastapi import Request from fastapi import Request
from app.main import http_exception_handler
# Create a mock request for a frontend route # Create a mock request for a frontend route
mock_request = MagicMock(spec=Request) mock_request = MagicMock(spec=Request)
mock_request.url.path = "/some-page" mock_request.url.path = "/some-page"
exc = HTTPException(status_code=403, detail="Forbidden") exc = HTTPException(status_code=403, detail="Forbidden")
# Call the handler directly # Call the handler directly
import asyncio import asyncio
response = asyncio.run(http_exception_handler(mock_request, exc)) response = asyncio.run(http_exception_handler(mock_request, exc))
assert response.status_code == 403 assert response.status_code == 403
def test_custom_500_handler_api_route(self): def test_custom_500_handler_api_route(self):
"""Test that 500 error returns JSON for API routes""" """Test that 500 error returns JSON for API routes"""
from app.main import app, custom_500_handler
from fastapi import Request from fastapi import Request
from app.main import custom_500_handler
# Create a mock request for an API route # Create a mock request for an API route
mock_request = MagicMock(spec=Request) mock_request = MagicMock(spec=Request)
mock_request.url.path = "/api/something" mock_request.url.path = "/api/something"
exc = Exception("Internal error") exc = Exception("Internal error")
# Call the handler directly # Call the handler directly
import asyncio import asyncio
response = asyncio.run(custom_500_handler(mock_request, exc)) response = asyncio.run(custom_500_handler(mock_request, exc))
assert response.status_code == 500 assert response.status_code == 500
# Parse JSON response # Parse JSON response
import json import json
content = json.loads(response.body.decode()) content = json.loads(response.body.decode())
assert content["detail"] == "Internal server error" assert content["detail"] == "Internal server error"
def test_custom_500_handler_frontend_route(self): def test_custom_500_handler_frontend_route(self):
"""Test that 500 error returns HTML for frontend routes""" """Test that 500 error returns HTML for frontend routes"""
from app.main import app, custom_500_handler
from fastapi import Request from fastapi import Request
from app.main import custom_500_handler
# Create a mock request for a frontend route # Create a mock request for a frontend route
mock_request = MagicMock(spec=Request) mock_request = MagicMock(spec=Request)
mock_request.url.path = "/dashboard" mock_request.url.path = "/dashboard"
exc = Exception("Internal error") exc = Exception("Internal error")
# Call the handler directly # Call the handler directly
import asyncio import asyncio
response = asyncio.run(custom_500_handler(mock_request, exc)) response = asyncio.run(custom_500_handler(mock_request, exc))
assert response.status_code == 500 assert response.status_code == 500
@@ -199,7 +208,7 @@ class TestTestEndpoint:
def test_test_500_endpoint_raises_error(self): def test_test_500_endpoint_raises_error(self):
"""Test that /test-500 endpoint raises RuntimeError""" """Test that /test-500 endpoint raises RuntimeError"""
from app.main import test_500 from app.main import test_500
# The function should raise RuntimeError # The function should raise RuntimeError
with pytest.raises(RuntimeError, match="Testing forced 500 error"): with pytest.raises(RuntimeError, match="Testing forced 500 error"):
test_500() test_500()
@@ -211,12 +220,13 @@ class TestStaticFileMount:
def test_static_files_mounted_when_directory_exists(self): def test_static_files_mounted_when_directory_exists(self):
"""Test that static files are served when directory exists""" """Test that static files are served when directory exists"""
from app.main import app
import pathlib import pathlib
from app.main import app
# Check if static directory exists # Check if static directory exists
static_dir = pathlib.Path(__file__).parents[1] / "frontend" / "static" static_dir = pathlib.Path(__file__).parents[1] / "frontend" / "static"
if os.path.exists(static_dir): if os.path.exists(static_dir):
# Check if static route is mounted # Check if static route is mounted
assert any("/static" in str(route.path) for route in app.routes) assert any("/static" in str(route.path) for route in app.routes)
@@ -229,12 +239,12 @@ class TestMiddlewareConfiguration:
def test_app_has_limiter_state(self): def test_app_has_limiter_state(self):
"""Test that app.state.limiter is configured""" """Test that app.state.limiter is configured"""
from app.main import app from app.main import app
assert hasattr(app.state, "limiter") assert hasattr(app.state, "limiter")
assert app.state.limiter is not None assert app.state.limiter is not None
def test_app_has_correct_title(self): def test_app_has_correct_title(self):
"""Test that FastAPI app has correct title""" """Test that FastAPI app has correct title"""
from app.main import app from app.main import app
assert app.title == "DocuElevate" assert app.title == "DocuElevate"
+4 -3
View File
@@ -68,7 +68,7 @@ class TestGetEmailTemplate:
# Workdir exists, but template loading fails; falls back to built-in # Workdir exists, but template loading fails; falls back to built-in
mock_exists.return_value = True mock_exists.return_value = True
mock_template = Mock() mock_template = Mock()
# First environment (workdir) raises exception, second (app) returns template # First environment (workdir) raises exception, second (app) returns template
mock_env_workdir = Mock() mock_env_workdir = Mock()
mock_env_workdir.globals = {} mock_env_workdir.globals = {}
@@ -163,6 +163,7 @@ class TestAttachLogo:
@patch("builtins.open", new_callable=mock_open, read_data=b"fake_svg_data") @patch("builtins.open", new_callable=mock_open, read_data=b"fake_svg_data")
def test_attaches_svg_logo_with_correct_mime_type(self, mock_file, mock_exists): def test_attaches_svg_logo_with_correct_mime_type(self, mock_file, mock_exists):
"""Test attaches SVG logo with correct MIME type (image/svg+xml).""" """Test attaches SVG logo with correct MIME type (image/svg+xml)."""
# Create a custom side effect that returns True only for SVG path # Create a custom side effect that returns True only for SVG path
def custom_exists(path): def custom_exists(path):
return "logo.svg" in path return "logo.svg" in path
@@ -178,9 +179,9 @@ class TestAttachLogo:
assert result is True assert result is True
assert len(msg.get_payload()) > 0 assert len(msg.get_payload()) > 0
# Verify SVG MIME type is used (the function detects .svg extension) # Verify SVG MIME type is used (the function detects .svg extension)
# Note: MIMEImage may default to a different subtype, but the key is that # Note: MIMEImage may default to a different subtype, but the key is that
# the function passes 'image/svg+xml' as mimetype parameter # the function passes 'image/svg+xml' as mimetype parameter
# Since we're using mock_open, we can't verify the exact MIME in the attachment, # Since we're using mock_open, we can't verify the exact MIME in the attachment,
# but we verified the code path is exercised # but we verified the code path is exercised
+16 -22
View File
@@ -1,8 +1,9 @@
"""Tests for app/views/google_drive.py module.""" """Tests for app/views/google_drive.py module."""
import pytest
from unittest.mock import patch
import urllib.parse import urllib.parse
from unittest.mock import patch
import pytest
@pytest.mark.integration @pytest.mark.integration
@@ -39,12 +40,11 @@ class TestGoogleDriveViews:
client_id = "test_client_id_123" client_id = "test_client_id_123"
redirect_uri = "https://example.com/callback" redirect_uri = "https://example.com/callback"
response = client.get( response = client.get(
f"/google-drive-auth-start?client_id={client_id}&redirect_uri={redirect_uri}", f"/google-drive-auth-start?client_id={client_id}&redirect_uri={redirect_uri}", follow_redirects=False
follow_redirects=False
) )
assert response.status_code in [302, 307] # Redirect status codes assert response.status_code in [302, 307] # Redirect status codes
# Verify redirect location # Verify redirect location
location = response.headers.get("location") location = response.headers.get("location")
assert location is not None assert location is not None
@@ -60,13 +60,10 @@ class TestGoogleDriveViews:
def test_google_drive_auth_start_without_redirect_uri(self, client): def test_google_drive_auth_start_without_redirect_uri(self, client):
"""Test starting Google Drive OAuth flow without explicit redirect_uri.""" """Test starting Google Drive OAuth flow without explicit redirect_uri."""
client_id = "test_client_id_456" client_id = "test_client_id_456"
response = client.get( response = client.get(f"/google-drive-auth-start?client_id={client_id}", follow_redirects=False)
f"/google-drive-auth-start?client_id={client_id}",
follow_redirects=False
)
assert response.status_code in [302, 307] # Redirect status codes assert response.status_code in [302, 307] # Redirect status codes
# Verify redirect location # Verify redirect location
location = response.headers.get("location") location = response.headers.get("location")
assert location is not None assert location is not None
@@ -78,14 +75,11 @@ class TestGoogleDriveViews:
def test_google_drive_auth_start_scope_configuration(self, client): def test_google_drive_auth_start_scope_configuration(self, client):
"""Test that Google Drive auth start uses correct OAuth scope.""" """Test that Google Drive auth start uses correct OAuth scope."""
client_id = "test_client_id_789" client_id = "test_client_id_789"
response = client.get( response = client.get(f"/google-drive-auth-start?client_id={client_id}", follow_redirects=False)
f"/google-drive-auth-start?client_id={client_id}",
follow_redirects=False
)
location = response.headers.get("location") location = response.headers.get("location")
assert location is not None assert location is not None
# The scope should be URL encoded, so check for the encoded version # The scope should be URL encoded, so check for the encoded version
# drive.file scope: https://www.googleapis.com/auth/drive.file # drive.file scope: https://www.googleapis.com/auth/drive.file
expected_scope = urllib.parse.quote("https://www.googleapis.com/auth/drive.file") expected_scope = urllib.parse.quote("https://www.googleapis.com/auth/drive.file")
@@ -100,7 +94,7 @@ class TestGoogleDriveViews:
mock_settings.google_drive_refresh_token = "test_token" mock_settings.google_drive_refresh_token = "test_token"
mock_settings.google_drive_credentials_json = '{"test": "creds"}' mock_settings.google_drive_credentials_json = '{"test": "creds"}'
mock_settings.google_drive_folder_id = None # Empty folder ID mock_settings.google_drive_folder_id = None # Empty folder ID
response = client.get("/google-drive-setup") response = client.get("/google-drive-setup")
assert response.status_code == 200 assert response.status_code == 200
# Verify the response context indicates configuration is incomplete # Verify the response context indicates configuration is incomplete
@@ -116,7 +110,7 @@ class TestGoogleDriveViews:
mock_settings.google_drive_refresh_token = "test_token" mock_settings.google_drive_refresh_token = "test_token"
mock_settings.google_drive_credentials_json = '{"test": "creds"}' mock_settings.google_drive_credentials_json = '{"test": "creds"}'
mock_settings.google_drive_folder_id = "" # Empty string folder ID mock_settings.google_drive_folder_id = "" # Empty string folder ID
response = client.get("/google-drive-setup") response = client.get("/google-drive-setup")
assert response.status_code == 200 assert response.status_code == 200
# Should handle empty string folder_id similar to None # Should handle empty string folder_id similar to None
@@ -130,7 +124,7 @@ class TestGoogleDriveViews:
mock_settings.google_drive_refresh_token = "oauth_token" mock_settings.google_drive_refresh_token = "oauth_token"
mock_settings.google_drive_folder_id = "test_folder_id" mock_settings.google_drive_folder_id = "test_folder_id"
mock_settings.google_drive_credentials_json = None mock_settings.google_drive_credentials_json = None
response = client.get("/google-drive-setup") response = client.get("/google-drive-setup")
assert response.status_code == 200 assert response.status_code == 200
@@ -143,6 +137,6 @@ class TestGoogleDriveViews:
mock_settings.google_drive_client_id = None mock_settings.google_drive_client_id = None
mock_settings.google_drive_client_secret = None mock_settings.google_drive_client_secret = None
mock_settings.google_drive_refresh_token = None mock_settings.google_drive_refresh_token = None
response = client.get("/google-drive-setup") response = client.get("/google-drive-setup")
assert response.status_code == 200 assert response.status_code == 200