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:
+1
-1
File diff suppressed because one or more lines are too long
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user