Merge pull request #321 from christianlouis/copilot/increase-test-coverage-process-document

test: Increase coverage for process_document.py from 84% to 99%
This commit is contained in:
Christian Krakau-Louis
2026-02-14 01:16:26 +01:00
committed by GitHub
10 changed files with 507 additions and 148 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+19 -12
View File
@@ -842,8 +842,9 @@ class TestDeleteFileExceptions:
db_session.commit()
file_id = file.id
with patch("app.config.settings") as mock_settings, patch.object(
db_session, "delete", side_effect=Exception("Database error")
with (
patch("app.config.settings") as mock_settings,
patch.object(db_session, "delete", side_effect=Exception("Database error")),
):
mock_settings.allow_file_delete = True
response = client.delete(f"/api/files/{file_id}")
@@ -869,8 +870,9 @@ class TestDeleteFileExceptions:
def failing_commit():
raise Exception("Database commit error")
with patch("app.config.settings") as mock_settings, patch.object(
db_session, "commit", side_effect=failing_commit
with (
patch("app.config.settings") as mock_settings,
patch.object(db_session, "commit", side_effect=failing_commit),
):
mock_settings.allow_file_delete = True
response = client.post("/api/files/bulk-delete", json=[file_id])
@@ -948,7 +950,9 @@ class TestRetryPipelineSteps:
db_session.add(file)
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")
result = _retry_pipeline_step(file, "process_with_azure_document_intelligence", db_session)
assert result["task_id"] == "task-azure"
@@ -991,8 +995,9 @@ class TestRetryPipelineSteps:
db_session.add(file)
db_session.commit()
with 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"
with (
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")
result = _retry_pipeline_step(file, "extract_metadata_with_gpt", db_session)
@@ -1035,8 +1040,9 @@ class TestRetryPipelineSteps:
db_session.add(file)
db_session.commit()
with 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"
with (
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")
result = _retry_pipeline_step(file, "embed_metadata_into_pdf", db_session)
@@ -1065,9 +1071,10 @@ class TestRetryUploadTasks:
db_session.add(file)
db_session.commit()
with patch("app.api.files.settings") as mock_settings, patch(
"app.tasks.upload_to_dropbox.upload_to_dropbox"
) as mock_task:
with (
patch("app.api.files.settings") as mock_settings,
patch("app.tasks.upload_to_dropbox.upload_to_dropbox") as mock_task,
):
mock_settings.workdir = str(tmp_path)
mock_task.delay.return_value = Mock(id="task-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."""
import os
from pathlib import Path
from unittest.mock import Mock, patch
import pytest
@@ -169,9 +167,7 @@ class TestProcessEndpoints:
test_file = tmp_path / "test1.pdf"
test_file.write_text("test content")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-1")
@@ -192,9 +188,7 @@ class TestProcessEndpoints:
for i in range(3):
(tmp_path / f"test{i}.pdf").write_text(f"test content {i}")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-id")
@@ -214,9 +208,7 @@ class TestProcessEndpoints:
for i in range(12):
(tmp_path / f"test{i}.pdf").write_text(f"test content {i}")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10
mock_settings.processall_throttle_delay = 5
@@ -246,9 +238,7 @@ class TestProcessEndpoints:
(tmp_path / "test.docx").write_text("word content")
(tmp_path / "test.jpg").write_text("image content")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = 10
mock_task.delay.return_value = Mock(id="task-id")
@@ -270,9 +260,7 @@ class TestProcessEndpoints:
for i in range(threshold):
(tmp_path / f"at_threshold_{i}.pdf").write_text(f"content {i}")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = threshold
mock_settings.processall_throttle_delay = 2
@@ -291,9 +279,7 @@ class TestProcessEndpoints:
for i in range(threshold + 1):
(tmp_path / f"above_threshold_{i}.pdf").write_text(f"content {i}")
with patch("app.api.process.settings") as mock_settings, patch(
"app.api.process.process_document"
) as mock_task:
with patch("app.api.process.settings") as mock_settings, patch("app.api.process.process_document") as mock_task:
mock_settings.workdir = str(tmp_path)
mock_settings.processall_throttle_threshold = threshold
mock_settings.processall_throttle_delay = 2
+2 -2
View File
@@ -211,11 +211,11 @@ class TestTaskFailureHandler:
mock_settings.notify_on_task_failure = True
# Import to ensure signal is connected
from app.celery_app import task_failure_handler
# Import the signal
from celery.signals import task_failure
from app.celery_app import task_failure_handler
# The handler should be connected to the signal
# We can test this by verifying the signal has receivers
receivers = task_failure.receivers
+3 -2
View File
@@ -200,9 +200,10 @@ class TestGetCipherSuite:
def test_get_cipher_suite_import_error(self):
"""Test _get_cipher_suite when cryptography is not installed"""
import app.utils.encryption
import sys
import app.utils.encryption
# Reset the cached cipher suite
original_cipher = app.utils.encryption._cipher_suite
app.utils.encryption._cipher_suite = None
@@ -218,6 +219,7 @@ class TestGetCipherSuite:
# Mock the import to raise ImportError
import builtins
real_import = builtins.__import__
def mock_import(name, *args, **kwargs):
@@ -245,7 +247,6 @@ class TestGetCipherSuite:
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()
+54 -44
View File
@@ -5,11 +5,11 @@ Tests FastAPI application initialization, middleware, error handlers,
and lifecycle management.
"""
from unittest.mock import Mock, patch, MagicMock
import os
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException, status
from fastapi.testclient import TestClient
from fastapi import HTTPException
@pytest.mark.unit
@@ -39,20 +39,21 @@ class TestLifespanEvents:
@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"):
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
from app.main import app, lifespan
# Execute the startup and shutdown
async with lifespan(app):
@@ -64,25 +65,23 @@ class TestLifespanEvents:
@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:
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"]}
}
mock_check.return_value = {"email": ["Invalid email config"], "storage": {"dropbox": ["Missing token"]}}
from app.main import lifespan, app
from app.main import app, lifespan
async with lifespan(app):
pass
@@ -93,20 +92,21 @@ class TestLifespanEvents:
@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:
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
from app.main import app, lifespan
# Should not raise exception, just log error
async with lifespan(app):
@@ -121,9 +121,10 @@ class TestExceptionHandlers:
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
from app.main import http_exception_handler
# Create a mock request for a frontend route
mock_request = MagicMock(spec=Request)
mock_request.url.path = "/nonexistent"
@@ -132,15 +133,17 @@ class TestExceptionHandlers:
# 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
from app.main import http_exception_handler
# Create a mock request for a frontend route
mock_request = MagicMock(spec=Request)
mock_request.url.path = "/some-page"
@@ -149,15 +152,17 @@ class TestExceptionHandlers:
# 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
from app.main import custom_500_handler
# Create a mock request for an API route
mock_request = MagicMock(spec=Request)
mock_request.url.path = "/api/something"
@@ -166,19 +171,22 @@ class TestExceptionHandlers:
# 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
from app.main import custom_500_handler
# Create a mock request for a frontend route
mock_request = MagicMock(spec=Request)
mock_request.url.path = "/dashboard"
@@ -187,6 +195,7 @@ class TestExceptionHandlers:
# Call the handler directly
import asyncio
response = asyncio.run(custom_500_handler(mock_request, exc))
assert response.status_code == 500
@@ -211,9 +220,10 @@ class TestStaticFileMount:
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
from app.main import app
# Check if static directory exists
static_dir = pathlib.Path(__file__).parents[1] / "frontend" / "static"
+360
View File
@@ -383,3 +383,363 @@ def test_process_document_reprocess_nonexistent_file_id(db_session, tmp_path):
# Verify error is returned
assert "error" in result
assert result["file_id"] == 99999
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_file_not_found(db_session, tmp_path):
"""
Test that process_document returns an error when the file doesn't exist.
"""
# Use a non-existent file path
nonexistent_file = tmp_path / "nonexistent.pdf"
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.log_task_progress"),
):
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
# Call with a file that doesn't exist
result = process_document.run(str(nonexistent_file))
# Verify error is returned
assert "error" in result
assert result["error"] == "File not found"
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_deduplication_disabled(db_session, tmp_path):
"""
Test that process_document works correctly when deduplication is disabled.
"""
# Create a test PDF file with embedded text
test_pdf = tmp_path / "test.pdf"
pdf_content = b"""%PDF-1.4
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
/Contents 4 0 R
>>
endobj
4 0 obj
<<
/Length 44
>>
stream
BT
/F1 12 Tf
100 700 Td
(Test content) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000306 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
399
%%EOF
"""
test_pdf.write_bytes(pdf_content)
# Mock environment and dependencies
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.settings") as mock_settings,
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.extract_metadata_with_gpt") as mock_extract,
):
# Setup mocks - disable deduplication
mock_settings.workdir = str(tmp_path)
mock_settings.enable_deduplication = False
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
mock_extract.delay = MagicMock()
# Call the task's run method directly
result = process_document.run(str(test_pdf))
# Verify that the task completed successfully
assert "file_id" in result
assert result["status"] == "Text extracted locally"
# Verify that a FileRecord was created
file_record = db_session.query(FileRecord).first()
assert file_record is not None
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_unknown_mime_type(db_session, tmp_path):
"""
Test that process_document handles files with unknown MIME types correctly,
falling back to 'application/octet-stream'.
"""
# Create a test file with an unusual extension that will be treated as non-PDF
test_file = tmp_path / "test.unknownext"
test_file.write_bytes(b"some binary content")
# Mock environment and dependencies
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.settings") as mock_settings,
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.celery") as mock_celery,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
mock_celery.send_task = MagicMock()
# Call the task's run method directly
result = process_document.run(str(test_file))
# Verify that the task completed successfully
assert "file_id" in result
# Verify that the mime_type was set to octet-stream fallback
file_record = db_session.query(FileRecord).first()
assert file_record is not None
assert file_record.mime_type == "application/octet-stream"
# File should be queued for PDF conversion since it's not a PDF
assert result["status"] == "Queued for PDF conversion"
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_force_cloud_ocr(db_session, tmp_path):
"""
Test that process_document correctly handles force_cloud_ocr flag,
skipping embedded text extraction and forcing cloud OCR.
"""
# Create a test PDF file with embedded text
test_pdf = tmp_path / "test.pdf"
pdf_content = b"""%PDF-1.4
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
/Resources <<
/Font <<
/F1 <<
/Type /Font
/Subtype /Type1
/BaseFont /Helvetica
>>
>>
>>
/Contents 4 0 R
>>
endobj
4 0 obj
<<
/Length 44
>>
stream
BT
/F1 12 Tf
100 700 Td
(Test content) Tj
ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000306 00000 n
trailer
<<
/Size 5
/Root 1 0 R
>>
startxref
399
%%EOF
"""
test_pdf.write_bytes(pdf_content)
# Mock environment and dependencies
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.settings") as mock_settings,
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.process_with_azure_document_intelligence") as mock_azure,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
mock_azure.delay = MagicMock()
# Call the task with force_cloud_ocr=True
result = process_document.run(str(test_pdf), force_cloud_ocr=True)
# Verify that cloud OCR was queued
assert result["status"] == "Queued for forced OCR"
assert "file_id" in result
# Verify that process_with_azure_document_intelligence was called
mock_azure.delay.assert_called_once()
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_non_pdf_file(db_session, tmp_path):
"""
Test that non-PDF files are queued for PDF conversion.
"""
# Create a test image file
test_image = tmp_path / "test.jpg"
test_image.write_bytes(b"fake image content")
# Mock environment and dependencies
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.settings") as mock_settings,
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.celery") as mock_celery,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
mock_celery.send_task = MagicMock()
# Call the task's run method directly
result = process_document.run(str(test_image))
# Verify that PDF conversion was queued
assert result["status"] == "Queued for PDF conversion"
assert "file_id" in result
# Verify that convert_to_pdf task was queued
mock_celery.send_task.assert_called_once()
call_args = mock_celery.send_task.call_args
assert call_args[0][0] == "app.tasks.convert_to_pdf.convert_to_pdf"
@pytest.mark.unit
@pytest.mark.requires_db
def test_process_document_pdf_read_error_retry(db_session, tmp_path):
"""
Test that PdfReadError during embedded text check triggers a retry.
"""
# Create a test PDF file
test_pdf = tmp_path / "test.pdf"
pdf_content = b"""%PDF-1.4
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/MediaBox [0 0 612 792]
>>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<<
/Size 4
/Root 1 0 R
>>
startxref
197
%%EOF
"""
test_pdf.write_bytes(pdf_content)
# Mock environment and dependencies
with (
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.settings") as mock_settings,
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.pypdf.PdfReader") as mock_pdf_reader,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
# Make PdfReader raise PdfReadError
from pypdf.errors import PdfReadError
mock_pdf_reader.side_effect = PdfReadError("Test error")
# Call the task's run method and expect it to raise retry exception
with pytest.raises(Exception) as exc_info:
process_document.run(str(test_pdf))
# Verify that retry was triggered
# The retry method raises a special exception
assert exc_info.value is not None
+1
View File
@@ -163,6 +163,7 @@ class TestAttachLogo:
@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):
"""Test attaches SVG logo with correct MIME type (image/svg+xml)."""
# Create a custom side effect that returns True only for SVG path
def custom_exists(path):
return "logo.svg" in path
+6 -12
View File
@@ -1,8 +1,9 @@
"""Tests for app/views/google_drive.py module."""
import pytest
from unittest.mock import patch
import urllib.parse
from unittest.mock import patch
import pytest
@pytest.mark.integration
@@ -39,8 +40,7 @@ class TestGoogleDriveViews:
client_id = "test_client_id_123"
redirect_uri = "https://example.com/callback"
response = client.get(
f"/google-drive-auth-start?client_id={client_id}&redirect_uri={redirect_uri}",
follow_redirects=False
f"/google-drive-auth-start?client_id={client_id}&redirect_uri={redirect_uri}", follow_redirects=False
)
assert response.status_code in [302, 307] # Redirect status codes
@@ -60,10 +60,7 @@ class TestGoogleDriveViews:
def test_google_drive_auth_start_without_redirect_uri(self, client):
"""Test starting Google Drive OAuth flow without explicit redirect_uri."""
client_id = "test_client_id_456"
response = client.get(
f"/google-drive-auth-start?client_id={client_id}",
follow_redirects=False
)
response = client.get(f"/google-drive-auth-start?client_id={client_id}", follow_redirects=False)
assert response.status_code in [302, 307] # Redirect status codes
@@ -78,10 +75,7 @@ class TestGoogleDriveViews:
def test_google_drive_auth_start_scope_configuration(self, client):
"""Test that Google Drive auth start uses correct OAuth scope."""
client_id = "test_client_id_789"
response = client.get(
f"/google-drive-auth-start?client_id={client_id}",
follow_redirects=False
)
response = client.get(f"/google-drive-auth-start?client_id={client_id}", follow_redirects=False)
location = response.headers.get("location")
assert location is not None