refactor: consolidate linting tools into Ruff

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-13 09:10:52 +00:00
parent 071c62c03f
commit 43bc58770d
98 changed files with 739 additions and 1168 deletions
+9 -82
View File
@@ -73,9 +73,9 @@ jobs:
junit.xml
coverage.xml
# ── Flake8 ─────────────────────────────────────────────────────────────
flake8:
name: Flake8
# ── Lint (Ruff) ───────────────────────────────────────────────────────
lint:
name: Ruff Lint & Format
runs-on: ubuntu-latest
steps:
- name: Checkout Code
@@ -86,34 +86,14 @@ jobs:
with:
python-version: "3.11"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
- name: Install Ruff
run: pip install ruff
- name: Run Flake8
run: flake8 app/ --max-line-length=120 --extend-ignore=E203,W503
- name: Run Ruff Check
run: ruff check app/ tests/
# ── Black ──────────────────────────────────────────────────────────────
black:
name: Black
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install black
- name: Run Black
run: black --check app/ --line-length=120
- name: Run Ruff Format
run: ruff format --check app/ tests/
# ── Mypy ───────────────────────────────────────────────────────────────
mypy:
@@ -135,56 +115,3 @@ jobs:
- name: Run Mypy
run: mypy app/
# ── Pylint ─────────────────────────────────────────────────────────────
pylint:
name: Pylint
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Run Pylint
run: pylint app/
# ── Bandit ─────────────────────────────────────────────────────────────
bandit:
name: Bandit
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install bandit
- name: Run Bandit - Full Report
if: ${{ !cancelled() }}
run: bandit -r app/ -f json -o bandit-report.json || true
- name: Run Bandit - Fail on High/Medium
run: bandit -r app/ -ll
- name: Upload Bandit Report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: bandit-report
path: bandit-report.json
+6 -28
View File
@@ -19,35 +19,13 @@ repos:
- id: detect-aws-credentials
args: ['--allow-missing-credentials']
# Python code formatting
- repo: https://github.com/psf/black
rev: 24.1.1
# Ruff - Fast Python linter and formatter (replaces Black, Flake8, isort, Bandit)
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: black
args: ['--line-length=120']
language_version: python3.11
# Import sorting
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
args: ['--profile=black', '--line-length=120']
# Linting
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
args: ['--max-line-length=120', '--extend-ignore=E203,W503']
# Security linting
- repo: https://github.com/PyCQA/bandit
rev: 1.7.6
hooks:
- id: bandit
args: ['-ll', '-r', 'app/']
exclude: 'tests/'
- id: ruff
args: [ --fix ]
- id: ruff-format
# Type checking
- repo: https://github.com/pre-commit/mirrors-mypy
+2 -2
View File
@@ -374,7 +374,7 @@ def reprocess_single_file(request: Request, file_id: int, db: DbSession):
)
logger.info(
f"Reprocessing file: ID={file_record.id}, " f"Filename={file_record.original_filename}, TaskID={task.id}"
f"Reprocessing file: ID={file_record.id}, Filename={file_record.original_filename}, TaskID={task.id}"
)
return {
@@ -645,7 +645,7 @@ def retry_subtask(
upload_task = task_map[subtask_name]
task = upload_task.delay(file_path, file_id)
logger.info(f"Retrying upload subtask: FileID={file_record.id}, " f"Subtask={subtask_name}, TaskID={task.id}")
logger.info(f"Retrying upload subtask: FileID={file_record.id}, Subtask={subtask_name}, TaskID={task.id}")
return {
"status": "success",
+1 -1
View File
@@ -146,7 +146,7 @@ def validate_file_type(content_type: str, filename: str) -> bool:
# Check content type from header
if content_type:
# Handle content-type with charset (e.g., "application/pdf; charset=utf-8")
base_content_type = content_type.split(";")[0].strip().lower()
base_content_type = content_type.split(";", maxsplit=1)[0].strip().lower()
if base_content_type in ALLOWED_MIME_TYPES or base_content_type in IMAGE_MIME_TYPES:
return True
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
import os
from typing import Any, List, Optional, Union
from typing import List, Optional, Union
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
+1 -2
View File
@@ -5,8 +5,7 @@ import os
from sqlalchemy import create_engine, exc
from sqlalchemy.engine.url import make_url
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import declarative_base, sessionmaker
from app.config import settings
+1 -3
View File
@@ -340,9 +340,7 @@ def convert_to_pdf(self, file_path: str, original_filename: Optional[str] = None
else:
error_msg = f"Status code: {response.status_code}"
logger.error(
f"[{task_id}] Conversion failed for {file_path}. "
f"{error_msg}, "
f"Response: {response.text[:500]}..."
f"[{task_id}] Conversion failed for {file_path}. {error_msg}, Response: {response.text[:500]}..."
)
log_task_progress(task_id, "call_gotenberg", "failure", error_msg)
log_task_progress(task_id, "convert_to_pdf", "failure", f"Conversion failed: {error_msg}")
+1 -2
View File
@@ -256,8 +256,7 @@ def embed_metadata_into_pdf(self, local_file_path: str, extracted_text: str, met
f"Exception: {str(e)}",
file_id=file_id,
detail=(
f"Failed to embed metadata into {processed_file}.\n"
f"Original file: {original_file}\nException: {str(e)}"
f"Failed to embed metadata into {processed_file}.\nOriginal file: {original_file}\nException: {str(e)}"
),
)
# Clean up temporary file in case of error
+1 -1
View File
@@ -42,7 +42,7 @@ def monitor_stalled_steps():
f"Marked as failed due to timeout."
)
else:
logger.debug(f"[{datetime.utcnow().isoformat()}] " f"No stalled steps found.")
logger.debug(f"[{datetime.utcnow().isoformat()}] No stalled steps found.")
return {"recovered": stalled_count}
@@ -70,13 +70,13 @@ def check_page_rotation(result, filename, task_id=None):
if hasattr(page, "angle"):
rotation_angle = page.angle
if rotation_angle != 0:
logger.info(f"{prefix}Page {i+1} is rotated by {rotation_angle} degrees")
logger.info(f"{prefix}Page {i + 1} is rotated by {rotation_angle} degrees")
# Store page index as integer, not string
rotation_data[i] = rotation_angle
else:
logger.info(f"{prefix}Page {i+1} has no rotation (0 degrees)")
logger.info(f"{prefix}Page {i + 1} has no rotation (0 degrees)")
else:
logger.info(f"{prefix}Page {i+1} rotation information not available")
logger.info(f"{prefix}Page {i + 1} rotation information not available")
return rotation_data
+3 -3
View File
@@ -136,13 +136,13 @@ def rotate_pdf_pages(self, filename: str, extracted_text: str, rotation_data=Non
# pypdf uses clockwise rotation in 90-degree increments
page.rotate(rotation_angle)
logger.info(
f"[{task_id}] Page {page_idx+1} rotated by {rotation_angle}° "
f"[{task_id}] Page {page_idx + 1} rotated by {rotation_angle}° "
f"(from detected {detected_angle}°)"
)
applied_rotations[str(page_idx)] = rotation_angle
else:
logger.info(
f"[{task_id}] Page {page_idx+1} had detected angle {detected_angle}° "
f"[{task_id}] Page {page_idx + 1} had detected angle {detected_angle}° "
"but determined it doesn't need rotation"
)
@@ -154,7 +154,7 @@ def rotate_pdf_pages(self, filename: str, extracted_text: str, rotation_data=Non
if applied_rotations:
logger.info(
f"[{task_id}] Successfully rotated PDF: {filename} with rotations: " f"{json.dumps(applied_rotations)}"
f"[{task_id}] Successfully rotated PDF: {filename} with rotations: {json.dumps(applied_rotations)}"
)
else:
logger.info(
+2 -2
View File
@@ -188,11 +188,11 @@ def upload_large_file(file_path, upload_url):
# 201 = Created (final chunk), 202 = Accepted (more chunks coming)
break
else:
logger.warning(f"Chunk upload failed (attempt {attempt+1}): {response.status_code}")
logger.warning(f"Chunk upload failed (attempt {attempt + 1}): {response.status_code}")
if attempt < max_retries - 1:
time.sleep(retry_delay * (attempt + 1))
except Exception as e:
logger.warning(f"Chunk upload error (attempt {attempt+1}): {str(e)}")
logger.warning(f"Chunk upload error (attempt {attempt + 1}): {str(e)}")
if attempt < max_retries - 1:
time.sleep(retry_delay * (attempt + 1))
@@ -192,9 +192,7 @@ def get_settings_for_display(show_values=False):
[
key
for key in dir(settings)
if not key.startswith("_")
and key not in _PYDANTIC_INTERNALS
and not callable(getattr(settings, key))
if not key.startswith("_") and key not in _PYDANTIC_INTERNALS and not callable(getattr(settings, key))
]
)
-1
View File
@@ -97,7 +97,6 @@ def split_pdf_by_size(pdf_path: str, max_size_bytes: int, output_dir: Optional[s
# If adding this page exceeds the limit (and we have more than 1 page in current chunk)
# save the previous chunk and start a new one
if exceeds_limit and current_page_count > 1:
# Create a new writer without the last page
previous_writer = PdfWriter()
for prev_page_num in range(page_num - current_page_count + 1, page_num):
+1 -1
View File
@@ -310,7 +310,7 @@ def verify_migration(db: Session, file_id: int) -> Dict:
if expected["status"] != actual.status:
result["discrepancies"].append(
f"Step '{step_name}' status mismatch: " f"expected '{expected['status']}', got '{actual.status}'"
f"Step '{step_name}' status mismatch: expected '{expected['status']}', got '{actual.status}'"
)
result["is_valid"] = False
+1 -1
View File
@@ -73,7 +73,7 @@ def mark_stalled_steps_as_failed(
return 0
logger.warning(
f"Found {len(stalled_steps)} stalled step(s) that exceeded " f"{timeout_seconds}s timeout. Marking as failed."
f"Found {len(stalled_steps)} stalled step(s) that exceeded {timeout_seconds}s timeout. Marking as failed."
)
count = 0
+22 -85
View File
@@ -104,33 +104,30 @@ upload_to_vcs_release = true
upload_to_pypi = false
upload_to_repository = false
# Black configuration
[tool.black]
# Ruff configuration
[tool.ruff]
line-length = 120
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
| migrations
)/
'''
target-version = "py311"
# isort configuration
[tool.isort]
profile = "black"
line_length = 120
skip_gitignore = true
known_first_party = ["app"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
[tool.ruff.lint]
# Enable Pyflakes (`F`), pycodestyle (`E`, `W`), isort (`I`), bandit (`S`), flake8-bugbear (`B`), and pylint (`PL`)
select = ["E", "F", "W", "I", "S", "B", "PL"]
ignore = [
"E501", # Line too long (handled by formatter)
"S108", # Hardcoded temp file (common pattern)
"S105", # Hardcoded password string (false positives with 'password' variable names)
"S106", # Hardcoded password func arg (false positives)
"PLC0415", # Import outside top-level (common in FastAPI/Celery)
"PLR0913", # Too many arguments
"PLR0912", # Too many branches
"PLR0915", # Too many statements
"PLR0911", # Too many return statements
"PLR2004", # Magic value comparison
"PLW0603", # Global statement
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "S110", "B017"] # Allow assert, try-except-pass, assert-raises-exception in tests
# pytest configuration
[tool.pytest.ini_options]
@@ -195,66 +192,6 @@ disable_error_code = [
"call-arg", # Dynamic call signatures in framework code
]
# pylint configuration
[tool.pylint.format]
max-line-length = 120
[tool.pylint."messages control"]
disable = [
"C0111", # missing-docstring (already documented functions use docstrings selectively)
"C0103", # invalid-name (project uses domain-specific naming conventions)
"C0114", # missing-module-docstring
"C0115", # missing-class-docstring
"C0116", # missing-function-docstring
"C0415", # import-outside-toplevel (common pattern in FastAPI/Celery)
"C0200", # consider-using-enumerate
"C0201", # consider-iterating-dictionary
"C0206", # consider-using-dict-items
"C0207", # use-maxsplit-arg
"C0123", # unidiomatic-typecheck
"C0302", # too-many-lines
"R0801", # duplicate-code (intentional patterns across storage providers)
"R0401", # cyclic-import (FastAPI app structure with lazy imports)
"R0903", # too-few-public-methods
"R0911", # too-many-return-statements
"R0912", # too-many-branches
"R0913", # too-many-arguments
"R0914", # too-many-locals
"R0915", # too-many-statements
"R0916", # too-many-boolean-expressions
"R0917", # too-many-positional-arguments
"R1702", # too-many-nested-blocks
"R1705", # no-else-return
"R1710", # inconsistent-return-statements
"R1718", # consider-using-set-comprehension
"R1720", # no-else-raise
"R1723", # no-else-break
"R1732", # consider-using-with
"W0105", # pointless-string-statement
"W0212", # protected-access
"W0223", # abstract-method
"W0404", # reimported
"W0511", # fixme (TODO comments are acceptable)
"W0603", # global-statement
"W0611", # unused-import (managed by flake8/isort)
"W0613", # unused-argument (common with framework callbacks)
"W0621", # redefined-outer-name
"W0641", # possibly-unused-variable
"W0707", # raise-missing-from
"W0718", # broad-exception-caught (intentional in error handlers)
"W0719", # broad-exception-raised
"W1203", # logging-fstring-interpolation (project uses f-strings consistently)
"W1510", # subprocess-run-check
"W1514", # unspecified-encoding
"W0612", # unused-variable
"E0213", # no-self-argument (Pydantic validators use cls)
"E0611", # no-name-in-module (false positives with package imports)
"E1101", # no-member (false positives with dynamic API clients)
"E1102", # not-callable (false positives with SQLAlchemy func.now())
"E1133", # not-an-iterable (false positives with Pydantic fields)
"E1135", # unsupported-membership-test (false positives with Pydantic fields)
]
# Coverage configuration
[tool.coverage.run]
source = ["app"]
+2 -7
View File
@@ -14,19 +14,14 @@ redis>=4.5.0 # For Redis integration tests
boto3>=1.26.0 # For S3 integration tests
# Code quality
flake8>=7.0.0
black>=24.0.0
ruff>=0.3.0
mypy>=1.8.0
pylint>=3.0.0
isort>=5.13.0
# Type stubs for mypy
types-requests>=2.31.0
types-paramiko>=3.0.0
# Security scanning
bandit>=1.7.6
safety>=3.0.0
# Security scanning (Ruff includes most security checks from bandit)
# Pre-commit hooks
pre-commit>=3.6.0
+1
View File
@@ -280,6 +280,7 @@ def pytest_configure(config):
config.addinivalue_line("markers", "requires_docker: Tests requiring Docker")
config.addinivalue_line("markers", "e2e: End-to-end tests with full infrastructure")
# Import OAuth fixtures (must be at end to avoid circular imports)
try:
from tests.conftest_oauth import (
+9 -8
View File
@@ -15,11 +15,13 @@ import pytest
from tests.mock_oauth_server import MockOAuth2ServerContainer, create_test_userinfo
# Check if we should use real OAuth credentials from environment
_REAL_OAUTH_AVAILABLE = all([
os.environ.get("AUTHENTIK_CLIENT_ID") not in {"", "NOT_SET", "test-key", None},
os.environ.get("AUTHENTIK_CLIENT_SECRET") not in {"", "NOT_SET", "test-key", None},
os.environ.get("AUTHENTIK_CONFIG_URL") not in {"", "NOT_SET", "test-key", None},
])
_REAL_OAUTH_AVAILABLE = all(
[
os.environ.get("AUTHENTIK_CLIENT_ID") not in {"", "NOT_SET", "test-key", None},
os.environ.get("AUTHENTIK_CLIENT_SECRET") not in {"", "NOT_SET", "test-key", None},
os.environ.get("AUTHENTIK_CONFIG_URL") not in {"", "NOT_SET", "test-key", None},
]
)
@pytest.fixture(scope="session")
@@ -181,11 +183,10 @@ def oauth_enabled_app(oauth_config: Dict[str, str]):
Yields:
Configured test client
"""
import os
from app.main import app
import app.auth as auth_module
from app.auth import login, oauth_login, oauth_callback, auth, logout
from app.auth import auth, login, logout, oauth_callback, oauth_login
from app.main import app
# Save original state
original_auth_enabled = auth_module.AUTH_ENABLED
-1
View File
@@ -14,7 +14,6 @@ These tests exercise the full application stack end-to-end.
import os
import time
from pathlib import Path
from typing import Generator
import pytest
-2
View File
@@ -8,11 +8,9 @@ and userinfo endpoints.
This allows for realistic OAuth testing without requiring a real IdP.
"""
import json
import logging
import time
from typing import Dict, Optional
from urllib.parse import urljoin
import requests
from testcontainers.core.container import DockerContainer
+6 -4
View File
@@ -1,8 +1,8 @@
"""Comprehensive unit tests for app/api/azure.py module."""
import pytest
from unittest.mock import MagicMock, patch
from fastapi.testclient import TestClient
import pytest
@pytest.mark.unit
@@ -65,9 +65,10 @@ class TestAzureTestConnection:
@patch("app.api.azure.azure.core.exceptions.ClientAuthenticationError")
def test_azure_connection_authentication_error(self, mock_auth_error, mock_admin_client_class):
"""Test connection with authentication error."""
from app.config import settings
import azure.core.exceptions
from app.config import settings
mock_admin_client_class.side_effect = azure.core.exceptions.ClientAuthenticationError("Invalid key")
with patch.object(settings, "azure_endpoint", "https://test.cognitiveservices.azure.com/"):
@@ -79,9 +80,10 @@ class TestAzureTestConnection:
@patch("app.api.azure.DocumentIntelligenceAdministrationClient")
def test_azure_connection_service_request_error(self, mock_admin_client_class):
"""Test connection with service request error."""
from app.config import settings
import azure.core.exceptions
from app.config import settings
mock_admin_client_class.side_effect = azure.core.exceptions.ServiceRequestError("Cannot reach endpoint")
with patch.object(settings, "azure_endpoint", "https://test.cognitiveservices.azure.com/"):
+3 -4
View File
@@ -1,8 +1,9 @@
"""Comprehensive unit tests for app/api/diagnostic.py module."""
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from unittest.mock import MagicMock, patch
@pytest.mark.unit
@@ -231,9 +232,7 @@ class TestTestNotification:
mock_send.return_value = True
with patch.object(
settings, "notification_urls", ["https://ntfy.sh/test1", "https://ntfy.sh/test2"]
):
with patch.object(settings, "notification_urls", ["https://ntfy.sh/test1", "https://ntfy.sh/test2"]):
# Response should indicate 2 services
pass
+1 -2
View File
@@ -4,8 +4,7 @@ Tests for app/api/dropbox.py module.
Covers Dropbox OAuth endpoints, settings management, and token testing.
"""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
import requests
+2 -6
View File
@@ -1,8 +1,8 @@
"""Comprehensive unit tests for app/api/dropbox.py module."""
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import MagicMock, patch, Mock
from fastapi import HTTPException
@pytest.mark.unit
@@ -53,21 +53,18 @@ class TestUpdateDropboxSettings:
def test_update_settings_refresh_token(self):
"""Test updating only refresh token."""
from app.config import settings
# Should update settings.dropbox_refresh_token
pass
def test_update_settings_all_fields(self):
"""Test updating all Dropbox settings."""
from app.config import settings
# Should update all fields: refresh_token, app_key, app_secret, folder_path
pass
def test_update_settings_partial_fields(self):
"""Test updating some fields (not all)."""
from app.config import settings
# Should only update provided fields
pass
@@ -275,7 +272,6 @@ class TestSaveDropboxSettings:
@patch("os.path.exists")
def test_save_settings_updates_memory(self, mock_exists, mock_open):
"""Test that in-memory settings are updated."""
from app.config import settings
mock_exists.return_value = True
mock_file = MagicMock()
+74 -44
View File
@@ -5,9 +5,8 @@ Tests all API endpoints with success and error cases, proper mocking, and edge c
Target: Bring coverage from 11.75% to 70%+
"""
import os
from io import BytesIO
from unittest.mock import Mock, MagicMock, patch
from unittest.mock import Mock, patch
import pytest
from fastapi import HTTPException
@@ -38,14 +37,14 @@ class TestListFilesAPI:
original_filename="test1.pdf",
local_filename="/tmp/test1.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="test2.pdf",
local_filename="/tmp/test2.pdf",
file_size=2048,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
@@ -66,7 +65,7 @@ class TestListFilesAPI:
original_filename=f"test{i}.pdf",
local_filename=f"/tmp/test{i}.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -93,14 +92,14 @@ class TestListFilesAPI:
original_filename="invoice.pdf",
local_filename="/tmp/invoice.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="receipt.pdf",
local_filename="/tmp/receipt.pdf",
file_size=2048,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
@@ -119,14 +118,14 @@ class TestListFilesAPI:
original_filename="doc.pdf",
local_filename="/tmp/doc.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="image.jpg",
local_filename="/tmp/image.jpg",
file_size=2048,
mime_type="image/jpeg"
mime_type="image/jpeg",
)
db_session.add(file1)
db_session.add(file2)
@@ -140,8 +139,20 @@ class TestListFilesAPI:
def test_list_files_sorting_asc(self, client: TestClient, db_session):
"""Test ascending sort order."""
file1 = FileRecord(filehash="hash1", original_filename="aaa.pdf", local_filename="/tmp/aaa.pdf", file_size=1024, mime_type="application/pdf")
file2 = FileRecord(filehash="hash2", original_filename="zzz.pdf", local_filename="/tmp/zzz.pdf", file_size=2048, mime_type="application/pdf")
file1 = FileRecord(
filehash="hash1",
original_filename="aaa.pdf",
local_filename="/tmp/aaa.pdf",
file_size=1024,
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="zzz.pdf",
local_filename="/tmp/zzz.pdf",
file_size=2048,
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
db_session.commit()
@@ -154,8 +165,20 @@ class TestListFilesAPI:
def test_list_files_sorting_desc(self, client: TestClient, db_session):
"""Test descending sort order."""
file1 = FileRecord(filehash="hash1", original_filename="aaa.pdf", local_filename="/tmp/aaa.pdf", file_size=1024, mime_type="application/pdf")
file2 = FileRecord(filehash="hash2", original_filename="zzz.pdf", local_filename="/tmp/zzz.pdf", file_size=2048, mime_type="application/pdf")
file1 = FileRecord(
filehash="hash1",
original_filename="aaa.pdf",
local_filename="/tmp/aaa.pdf",
file_size=1024,
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="zzz.pdf",
local_filename="/tmp/zzz.pdf",
file_size=2048,
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
db_session.commit()
@@ -178,7 +201,7 @@ class TestGetFileDetails:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -205,7 +228,7 @@ class TestGetFileDetails:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -216,7 +239,7 @@ class TestGetFileDetails:
task_id="task123",
step_name="process_document",
status="success",
message="Processing completed"
message="Processing completed",
)
db_session.add(log)
db_session.commit()
@@ -241,7 +264,7 @@ class TestDeleteFileRecord:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -263,7 +286,7 @@ class TestDeleteFileRecord:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -286,8 +309,20 @@ class TestBulkDeleteFiles:
@patch("app.config.settings.allow_file_delete", True)
def test_bulk_delete_success(self, client: TestClient, db_session):
"""Test bulk deletion of multiple files."""
file1 = FileRecord(filehash="hash1", original_filename="test1.pdf", local_filename="/tmp/test1.pdf", file_size=1024, mime_type="application/pdf")
file2 = FileRecord(filehash="hash2", original_filename="test2.pdf", local_filename="/tmp/test2.pdf", file_size=2048, mime_type="application/pdf")
file1 = FileRecord(
filehash="hash1",
original_filename="test1.pdf",
local_filename="/tmp/test1.pdf",
file_size=1024,
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="test2.pdf",
local_filename="/tmp/test2.pdf",
file_size=2048,
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
db_session.commit()
@@ -332,7 +367,7 @@ class TestBulkReprocessFiles:
original_filename="test1.pdf",
local_filename=str(file1_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file1)
db_session.commit()
@@ -357,7 +392,7 @@ class TestBulkReprocessFiles:
original_filename="test.pdf",
local_filename="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -391,7 +426,7 @@ class TestReprocessSingleFile:
original_filename="test.pdf",
local_filename=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -420,7 +455,7 @@ class TestReprocessSingleFile:
original_filename="test.pdf",
local_filename="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -446,7 +481,7 @@ class TestReprocessWithCloudOCR:
local_filename=str(file_path),
original_file_path=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -474,7 +509,7 @@ class TestReprocessWithCloudOCR:
original_filename="test.pdf",
local_filename="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -502,7 +537,7 @@ class TestRetrySubtask:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -525,7 +560,7 @@ class TestRetrySubtask:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -554,7 +589,7 @@ class TestFilePreview:
original_filename="test.pdf",
local_filename=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -575,7 +610,7 @@ class TestFilePreview:
original_filename="test.pdf",
local_filename="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -591,7 +626,7 @@ class TestFilePreview:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -614,7 +649,7 @@ class TestFileDownload:
original_filename="test.pdf",
local_filename=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -643,12 +678,11 @@ class TestUIUpload:
mock_delay.return_value = mock_task
# Create PDF content
pdf_content = b"%PDF-1.4\n%\xE2\xE3\xCF\xD3\n"
pdf_content = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"
with patch("app.config.settings.workdir", str(tmp_path)):
response = client.post(
"/api/ui-upload",
files={"file": ("test.pdf", BytesIO(pdf_content), "application/pdf")}
"/api/ui-upload", files={"file": ("test.pdf", BytesIO(pdf_content), "application/pdf")}
)
assert response.status_code == 200
@@ -671,10 +705,7 @@ class TestUIUpload:
image_content = b"\x89PNG\r\n\x1a\n"
with patch("app.config.settings.workdir", str(tmp_path)):
response = client.post(
"/api/ui-upload",
files={"file": ("image.png", BytesIO(image_content), "image/png")}
)
response = client.post("/api/ui-upload", files={"file": ("image.png", BytesIO(image_content), "image/png")})
assert response.status_code == 200
data = response.json()
@@ -690,8 +721,7 @@ class TestUIUpload:
with patch("app.config.settings.workdir", str(tmp_path)):
response = client.post(
"/api/ui-upload",
files={"file": ("large.pdf", BytesIO(large_content), "application/pdf")}
"/api/ui-upload", files={"file": ("large.pdf", BytesIO(large_content), "application/pdf")}
)
assert response.status_code == 413
@@ -712,7 +742,7 @@ class TestUIUpload:
# Upload with unsafe filename
response = client.post(
"/api/ui-upload",
files={"file": ("../../../etc/passwd.pdf", BytesIO(pdf_content), "application/pdf")}
files={"file": ("../../../etc/passwd.pdf", BytesIO(pdf_content), "application/pdf")},
)
assert response.status_code == 200
@@ -761,7 +791,7 @@ class TestRetryPipelineStep:
original_filename="test.pdf",
local_filename=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -784,7 +814,7 @@ class TestRetryPipelineStep:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
+49 -89
View File
@@ -5,9 +5,8 @@ Tests all API endpoints with success and error cases, proper mocking, and edge c
Target: Bring coverage from 9.45% to 70%+
"""
import os
from datetime import datetime, timedelta
from unittest.mock import Mock, MagicMock, patch, mock_open
from unittest.mock import MagicMock, Mock, mock_open, patch
import pytest
from fastapi import HTTPException
@@ -24,7 +23,7 @@ class TestExchangeGoogleDriveToken:
mock_exchange.return_value = {
"refresh_token": "test_refresh_token",
"access_token": "test_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -34,8 +33,8 @@ class TestExchangeGoogleDriveToken:
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "test_auth_code",
"folder_id": "test_folder"
}
"folder_id": "test_folder",
},
)
assert response.status_code == 200
@@ -52,7 +51,7 @@ class TestExchangeGoogleDriveToken:
mock_exchange.return_value = {
"refresh_token": "test_refresh_token",
"access_token": "test_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -61,8 +60,8 @@ class TestExchangeGoogleDriveToken:
"client_id": "test_client_id",
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "test_auth_code"
}
"code": "test_auth_code",
},
)
assert response.status_code == 200
@@ -78,8 +77,8 @@ class TestExchangeGoogleDriveToken:
"client_id": "test_client_id",
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "invalid_code"
}
"code": "invalid_code",
},
)
assert response.status_code == 400
@@ -99,8 +98,8 @@ class TestUpdateGoogleDriveSettings:
"client_id": "new_client_id",
"client_secret": "new_client_secret",
"folder_id": "new_folder_id",
"use_oauth": "true"
}
"use_oauth": "true",
},
)
assert response.status_code == 200
@@ -112,11 +111,7 @@ class TestUpdateGoogleDriveSettings:
def test_update_settings_with_use_oauth_false(self, mock_settings, client: TestClient):
"""Test updating with OAuth disabled."""
response = client.post(
"/api/google-drive/update-settings",
data={
"refresh_token": "new_refresh_token",
"use_oauth": "false"
}
"/api/google-drive/update-settings", data={"refresh_token": "new_refresh_token", "use_oauth": "false"}
)
assert response.status_code == 200
@@ -124,21 +119,13 @@ class TestUpdateGoogleDriveSettings:
@patch("app.config.settings")
def test_update_settings_minimal(self, mock_settings, client: TestClient):
"""Test update with only required fields."""
response = client.post(
"/api/google-drive/update-settings",
data={
"refresh_token": "new_refresh_token"
}
)
response = client.post("/api/google-drive/update-settings", data={"refresh_token": "new_refresh_token"})
assert response.status_code == 200
def test_update_settings_missing_required_field(self, client: TestClient):
"""Test update without required refresh_token."""
response = client.post(
"/api/google-drive/update-settings",
data={}
)
response = client.post("/api/google-drive/update-settings", data={})
assert response.status_code == 422 # Validation error
@@ -160,9 +147,7 @@ class TestTestGoogleDriveToken:
# Mock the Google Drive service
mock_service = MagicMock()
mock_about = MagicMock()
mock_about.get.return_value.execute.return_value = {
"user": {"emailAddress": "test@example.com"}
}
mock_about.get.return_value.execute.return_value = {"user": {"emailAddress": "test@example.com"}}
mock_service.about.return_value = mock_about
mock_get_service.return_value = mock_service
@@ -255,6 +240,7 @@ class TestGetGoogleDriveTokenInfo:
# Mock refresh
def mock_refresh(request):
mock_creds.valid = True
mock_creds.refresh = mock_refresh
mock_creds_class.return_value = mock_creds
@@ -315,18 +301,20 @@ class TestFormatTimeRemaining:
def test_format_expired_time(self):
"""Test formatting of expired time."""
from app.api.google_drive import format_time_remaining
from datetime import timedelta
from app.api.google_drive import format_time_remaining
expired = timedelta(seconds=-100)
result = format_time_remaining(expired)
assert result == "Expired"
def test_format_days_and_hours(self):
"""Test formatting with days and hours."""
from app.api.google_drive import format_time_remaining
from datetime import timedelta
from app.api.google_drive import format_time_remaining
time_left = timedelta(days=2, hours=5, minutes=30)
result = format_time_remaining(time_left)
assert "2 days" in result
@@ -335,9 +323,10 @@ class TestFormatTimeRemaining:
def test_format_hours_and_minutes(self):
"""Test formatting with hours and minutes."""
from app.api.google_drive import format_time_remaining
from datetime import timedelta
from app.api.google_drive import format_time_remaining
time_left = timedelta(hours=3, minutes=45)
result = format_time_remaining(time_left)
assert "3 hours" in result
@@ -345,18 +334,20 @@ class TestFormatTimeRemaining:
def test_format_minutes_only(self):
"""Test formatting with only minutes."""
from app.api.google_drive import format_time_remaining
from datetime import timedelta
from app.api.google_drive import format_time_remaining
time_left = timedelta(minutes=30)
result = format_time_remaining(time_left)
assert "30 minutes" in result
def test_format_single_unit(self):
"""Test singular form (1 day, not 1 days)."""
from app.api.google_drive import format_time_remaining
from datetime import timedelta
from app.api.google_drive import format_time_remaining
time_left = timedelta(days=1, hours=0)
result = format_time_remaining(time_left)
# Should use singular "day" not plural "days"
@@ -385,8 +376,8 @@ class TestSaveGoogleDriveSettings:
"client_id": "new_client_id",
"client_secret": "new_client_secret",
"folder_id": "new_folder_id",
"use_oauth": "true"
}
"use_oauth": "true",
},
)
assert response.status_code == 200
@@ -402,11 +393,7 @@ class TestSaveGoogleDriveSettings:
mock_dirname.return_value = "/app"
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "new_refresh_token",
"use_oauth": "true"
}
"/api/google-drive/save-settings", data={"refresh_token": "new_refresh_token", "use_oauth": "true"}
)
assert response.status_code == 200
@@ -418,17 +405,15 @@ class TestSaveGoogleDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_updates_existing_lines(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_updates_existing_lines(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that existing settings are updated, not duplicated."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "updated_token",
"use_oauth": "true"
}
"/api/google-drive/save-settings", data={"refresh_token": "updated_token", "use_oauth": "true"}
)
assert response.status_code == 200
@@ -437,18 +422,16 @@ class TestSaveGoogleDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_uncomments_lines(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_uncomments_lines(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that commented settings are uncommented when updated."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "new_token",
"client_id": "new_client_id",
"use_oauth": "true"
}
data={"refresh_token": "new_token", "client_id": "new_client_id", "use_oauth": "true"},
)
assert response.status_code == 200
@@ -458,11 +441,7 @@ class TestSaveGoogleDriveSettings:
"""Test saving with OAuth disabled."""
with patch("os.path.exists", return_value=False):
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "token",
"use_oauth": "false"
}
"/api/google-drive/save-settings", data={"refresh_token": "token", "use_oauth": "false"}
)
assert response.status_code == 200
@@ -471,17 +450,15 @@ class TestSaveGoogleDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_file_write_error_continues(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_file_write_error_continues(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that file write errors don't prevent in-memory update."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "new_token",
"use_oauth": "true"
}
"/api/google-drive/save-settings", data={"refresh_token": "new_token", "use_oauth": "true"}
)
# Should still succeed with in-memory update
@@ -489,12 +466,7 @@ class TestSaveGoogleDriveSettings:
def test_save_settings_missing_required_field(self, client: TestClient):
"""Test save without required refresh_token."""
response = client.post(
"/api/google-drive/save-settings",
data={
"use_oauth": "true"
}
)
response = client.post("/api/google-drive/save-settings", data={"use_oauth": "true"})
assert response.status_code == 422 # Validation error
@@ -504,10 +476,7 @@ class TestSaveGoogleDriveSettings:
with patch("os.path.exists", return_value=False):
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "existing_token",
"folder_id": "new_folder_id"
}
data={"refresh_token": "existing_token", "folder_id": "new_folder_id"},
)
assert response.status_code == 200
@@ -519,13 +488,7 @@ class TestSaveGoogleDriveSettings:
"""Test exception handling in save settings."""
mock_exists.side_effect = Exception("Unexpected error")
response = client.post(
"/api/google-drive/save-settings",
data={
"refresh_token": "token",
"use_oauth": "true"
}
)
response = client.post("/api/google-drive/save-settings", data={"refresh_token": "token", "use_oauth": "true"})
assert response.status_code == 500
data = response.json()
@@ -549,7 +512,7 @@ class TestGoogleDriveIntegration:
mock_exchange.return_value = {
"refresh_token": "new_refresh_token",
"access_token": "new_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -558,8 +521,8 @@ class TestGoogleDriveIntegration:
"client_id": "test_client_id",
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "auth_code"
}
"code": "auth_code",
},
)
assert response.status_code == 200
@@ -570,10 +533,7 @@ class TestGoogleDriveIntegration:
with patch("os.path.exists", return_value=False):
response = client.post(
"/api/google-drive/update-settings",
data={
"refresh_token": token_data["refresh_token"],
"use_oauth": "true"
}
data={"refresh_token": token_data["refresh_token"], "use_oauth": "true"},
)
assert response.status_code == 200
+3 -4
View File
@@ -1,8 +1,9 @@
"""Comprehensive unit tests for app/api/google_drive.py module."""
import pytest
from unittest.mock import MagicMock, patch
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
import pytest
@pytest.mark.unit
@@ -39,14 +40,12 @@ class TestUpdateGoogleDriveSettings:
def test_update_settings_oauth_enabled(self):
"""Test updating settings with OAuth enabled."""
from app.config import settings
# Should update OAuth credentials
pass
def test_update_settings_oauth_disabled(self):
"""Test updating settings with OAuth disabled."""
from app.config import settings
# Should set use_oauth to False
pass
-2
View File
@@ -1,7 +1,5 @@
"""Tests for app/api/logs.py module."""
from datetime import datetime
import pytest
from app.models import FileRecord, ProcessingLog
+50 -114
View File
@@ -5,9 +5,8 @@ Tests all API endpoints with success and error cases, proper mocking, and edge c
Target: Bring coverage from 10.51% to 70%+
"""
import os
from datetime import datetime, timedelta
from unittest.mock import Mock, MagicMock, patch, mock_open
from datetime import timedelta
from unittest.mock import Mock, mock_open, patch
import pytest
from fastapi import HTTPException
@@ -24,7 +23,7 @@ class TestExchangeOneDriveToken:
mock_exchange.return_value = {
"refresh_token": "test_refresh_token",
"access_token": "test_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -34,8 +33,8 @@ class TestExchangeOneDriveToken:
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "test_auth_code",
"tenant_id": "common"
}
"tenant_id": "common",
},
)
assert response.status_code == 200
@@ -51,7 +50,7 @@ class TestExchangeOneDriveToken:
mock_exchange.return_value = {
"refresh_token": "test_refresh_token",
"access_token": "test_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -61,8 +60,8 @@ class TestExchangeOneDriveToken:
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "test_auth_code",
"tenant_id": "specific-tenant-id"
}
"tenant_id": "specific-tenant-id",
},
)
assert response.status_code == 200
@@ -82,8 +81,8 @@ class TestExchangeOneDriveToken:
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "invalid_code",
"tenant_id": "common"
}
"tenant_id": "common",
},
)
assert response.status_code == 400
@@ -95,7 +94,7 @@ class TestExchangeOneDriveToken:
data={
"client_id": "test_client_id"
# Missing other required fields
}
},
)
assert response.status_code == 422 # Validation error
@@ -120,19 +119,13 @@ class TestTestOneDriveToken:
# Mock token refresh response
mock_post_response = Mock()
mock_post_response.status_code = 200
mock_post_response.json.return_value = {
"access_token": "test_access_token",
"expires_in": 3600
}
mock_post_response.json.return_value = {"access_token": "test_access_token", "expires_in": 3600}
mock_post.return_value = mock_post_response
# Mock user info response
mock_get_response = Mock()
mock_get_response.status_code = 200
mock_get_response.json.return_value = {
"displayName": "Test User",
"userPrincipalName": "test@example.com"
}
mock_get_response.json.return_value = {"displayName": "Test User", "userPrincipalName": "test@example.com"}
mock_get.return_value = mock_get_response
response = client.get("/api/onedrive/test-token")
@@ -198,17 +191,14 @@ class TestTestOneDriveToken:
mock_post_response.json.return_value = {
"access_token": "test_access_token",
"refresh_token": "new_refresh_token", # New token
"expires_in": 3600
"expires_in": 3600,
}
mock_post.return_value = mock_post_response
# Mock user info
mock_get_response = Mock()
mock_get_response.status_code = 200
mock_get_response.json.return_value = {
"displayName": "Test User",
"userPrincipalName": "test@example.com"
}
mock_get_response.json.return_value = {"displayName": "Test User", "userPrincipalName": "test@example.com"}
mock_get.return_value = mock_get_response
with patch("os.path.exists", return_value=False):
@@ -223,7 +213,9 @@ class TestTestOneDriveToken:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_test_token_updates_env_file(self, mock_settings, mock_dirname, mock_exists, mock_file, mock_get, mock_post, client: TestClient):
def test_test_token_updates_env_file(
self, mock_settings, mock_dirname, mock_exists, mock_file, mock_get, mock_post, client: TestClient
):
"""Test that new refresh token is saved to .env file."""
mock_settings.onedrive_refresh_token = "old_token"
mock_settings.onedrive_client_id = "test_client_id"
@@ -239,17 +231,14 @@ class TestTestOneDriveToken:
mock_post_response.json.return_value = {
"access_token": "test_access_token",
"refresh_token": "new_token",
"expires_in": 3600
"expires_in": 3600,
}
mock_post.return_value = mock_post_response
# Mock user info
mock_get_response = Mock()
mock_get_response.status_code = 200
mock_get_response.json.return_value = {
"displayName": "Test User",
"userPrincipalName": "test@example.com"
}
mock_get_response.json.return_value = {"displayName": "Test User", "userPrincipalName": "test@example.com"}
mock_get.return_value = mock_get_response
response = client.get("/api/onedrive/test-token")
@@ -269,10 +258,7 @@ class TestTestOneDriveToken:
# Mock successful refresh
mock_post_response = Mock()
mock_post_response.status_code = 200
mock_post_response.json.return_value = {
"access_token": "test_access_token",
"expires_in": 3600
}
mock_post_response.json.return_value = {"access_token": "test_access_token", "expires_in": 3600}
mock_post.return_value = mock_post_response
# Mock failed user info
@@ -295,7 +281,6 @@ class TestFormatTimeRemaining:
def test_format_expired_time(self):
"""Test formatting of expired time."""
from app.api.onedrive import format_time_remaining
from datetime import timedelta
expired = timedelta(seconds=-100)
result = format_time_remaining(expired)
@@ -304,7 +289,6 @@ class TestFormatTimeRemaining:
def test_format_days_and_hours(self):
"""Test formatting with days and hours."""
from app.api.onedrive import format_time_remaining
from datetime import timedelta
time_left = timedelta(days=2, hours=5, minutes=30)
result = format_time_remaining(time_left)
@@ -314,7 +298,6 @@ class TestFormatTimeRemaining:
def test_format_hours_only(self):
"""Test formatting with hours only."""
from app.api.onedrive import format_time_remaining
from datetime import timedelta
time_left = timedelta(hours=5)
result = format_time_remaining(time_left)
@@ -323,7 +306,6 @@ class TestFormatTimeRemaining:
def test_format_minutes_only(self):
"""Test formatting with minutes only."""
from app.api.onedrive import format_time_remaining
from datetime import timedelta
time_left = timedelta(minutes=45)
result = format_time_remaining(time_left)
@@ -350,8 +332,8 @@ class TestSaveOneDriveSettings:
"client_id": "new_client_id",
"client_secret": "new_client_secret",
"tenant_id": "common",
"folder_path": "/Documents"
}
"folder_path": "/Documents",
},
)
assert response.status_code == 200
@@ -365,13 +347,7 @@ class TestSaveOneDriveSettings:
mock_exists.return_value = False
mock_dirname.return_value = "/app"
response = client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "token",
"tenant_id": "common"
}
)
response = client.post("/api/onedrive/save-settings", data={"refresh_token": "token", "tenant_id": "common"})
assert response.status_code == 500
data = response.json()
@@ -381,17 +357,15 @@ class TestSaveOneDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_updates_existing_lines(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_updates_existing_lines(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that existing settings are updated."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "updated_token",
"tenant_id": "common"
}
"/api/onedrive/save-settings", data={"refresh_token": "updated_token", "tenant_id": "common"}
)
assert response.status_code == 200
@@ -400,18 +374,16 @@ class TestSaveOneDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_uncomments_lines(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_uncomments_lines(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that commented settings are uncommented."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "token",
"client_id": "new_client_id",
"tenant_id": "common"
}
data={"refresh_token": "token", "client_id": "new_client_id", "tenant_id": "common"},
)
assert response.status_code == 200
@@ -420,30 +392,23 @@ class TestSaveOneDriveSettings:
@patch("os.path.exists")
@patch("os.path.dirname")
@patch("app.config.settings")
def test_save_settings_adds_new_lines(self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient):
def test_save_settings_adds_new_lines(
self, mock_settings, mock_dirname, mock_exists, mock_file, client: TestClient
):
"""Test that new settings are added if not present."""
mock_exists.return_value = True
mock_dirname.return_value = "/app"
response = client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "new_token",
"folder_path": "/New/Path",
"tenant_id": "common"
}
data={"refresh_token": "new_token", "folder_path": "/New/Path", "tenant_id": "common"},
)
assert response.status_code == 200
def test_save_settings_missing_required_field(self, client: TestClient):
"""Test save without required refresh_token."""
response = client.post(
"/api/onedrive/save-settings",
data={
"tenant_id": "common"
}
)
response = client.post("/api/onedrive/save-settings", data={"tenant_id": "common"})
assert response.status_code == 422 # Validation error
@@ -453,13 +418,7 @@ class TestSaveOneDriveSettings:
"""Test exception handling in save settings."""
mock_exists.side_effect = Exception("Unexpected error")
response = client.post(
"/api/onedrive/save-settings",
data={
"refresh_token": "token",
"tenant_id": "common"
}
)
response = client.post("/api/onedrive/save-settings", data={"refresh_token": "token", "tenant_id": "common"})
assert response.status_code == 500
@@ -481,8 +440,8 @@ class TestUpdateOneDriveSettings:
"client_id": "new_client_id",
"client_secret": "new_client_secret",
"tenant_id": "common",
"folder_path": "/Documents"
}
"folder_path": "/Documents",
},
)
assert response.status_code == 200
@@ -496,11 +455,7 @@ class TestUpdateOneDriveSettings:
mock_get_token.return_value = "test_token"
response = client.post(
"/api/onedrive/update-settings",
data={
"refresh_token": "new_token",
"tenant_id": "common"
}
"/api/onedrive/update-settings", data={"refresh_token": "new_token", "tenant_id": "common"}
)
assert response.status_code == 200
@@ -512,11 +467,7 @@ class TestUpdateOneDriveSettings:
mock_get_token.side_effect = Exception("Token invalid")
response = client.post(
"/api/onedrive/update-settings",
data={
"refresh_token": "bad_token",
"tenant_id": "common"
}
"/api/onedrive/update-settings", data={"refresh_token": "bad_token", "tenant_id": "common"}
)
assert response.status_code == 200
@@ -526,12 +477,7 @@ class TestUpdateOneDriveSettings:
def test_update_settings_missing_required_field(self, client: TestClient):
"""Test update without required refresh_token."""
response = client.post(
"/api/onedrive/update-settings",
data={
"tenant_id": "common"
}
)
response = client.post("/api/onedrive/update-settings", data={"tenant_id": "common"})
assert response.status_code == 422
@@ -542,11 +488,7 @@ class TestUpdateOneDriveSettings:
with patch("app.tasks.upload_to_onedrive.get_onedrive_token", side_effect=Exception("Fatal error")):
response = client.post(
"/api/onedrive/update-settings",
data={
"refresh_token": "token",
"tenant_id": "common"
}
"/api/onedrive/update-settings", data={"refresh_token": "token", "tenant_id": "common"}
)
# Should still update settings even if test fails
@@ -615,7 +557,7 @@ class TestOneDriveIntegration:
mock_exchange.return_value = {
"refresh_token": "new_refresh_token",
"access_token": "new_access_token",
"expires_in": 3600
"expires_in": 3600,
}
response = client.post(
@@ -625,8 +567,8 @@ class TestOneDriveIntegration:
"client_secret": "test_client_secret",
"redirect_uri": "http://localhost/callback",
"code": "auth_code",
"tenant_id": "common"
}
"tenant_id": "common",
},
)
assert response.status_code == 200
@@ -636,10 +578,7 @@ class TestOneDriveIntegration:
with patch("app.tasks.upload_to_onedrive.get_onedrive_token"):
response = client.post(
"/api/onedrive/update-settings",
data={
"refresh_token": token_data["refresh_token"],
"tenant_id": "common"
}
data={"refresh_token": token_data["refresh_token"], "tenant_id": "common"},
)
assert response.status_code == 200
@@ -661,16 +600,13 @@ class TestOneDriveIntegration:
mock_post_response.json.return_value = {
"access_token": "access1",
"refresh_token": "new_token",
"expires_in": 3600
"expires_in": 3600,
}
mock_post.return_value = mock_post_response
mock_get_response = Mock()
mock_get_response.status_code = 200
mock_get_response.json.return_value = {
"displayName": "Test User",
"userPrincipalName": "test@example.com"
}
mock_get_response.json.return_value = {"displayName": "Test User", "userPrincipalName": "test@example.com"}
mock_get.return_value = mock_get_response
with patch("os.path.exists", return_value=False):
+3 -6
View File
@@ -1,8 +1,9 @@
"""Comprehensive unit tests for app/api/onedrive.py module."""
import pytest
from datetime import timedelta
from unittest.mock import MagicMock, patch
from datetime import datetime, timedelta
import pytest
@pytest.mark.unit
@@ -285,7 +286,6 @@ class TestSaveOneDriveSettings:
@patch("os.path.exists")
def test_save_settings_updates_memory(self, mock_exists, mock_open):
"""Test that in-memory settings are updated."""
from app.config import settings
mock_exists.return_value = True
mock_file = MagicMock()
@@ -303,7 +303,6 @@ class TestUpdateOneDriveSettings:
@patch("app.tasks.upload_to_onedrive.get_onedrive_token")
def test_update_settings_success(self, mock_get_token):
"""Test successful settings update."""
from app.config import settings
mock_get_token.return_value = "access_token"
@@ -313,7 +312,6 @@ class TestUpdateOneDriveSettings:
@patch("app.tasks.upload_to_onedrive.get_onedrive_token")
def test_update_settings_token_test_failed(self, mock_get_token):
"""Test when token test fails after update."""
from app.config import settings
mock_get_token.side_effect = Exception("Token test failed")
@@ -343,7 +341,6 @@ class TestGetOneDriveFullConfig:
def test_get_full_config_env_format(self):
"""Test that env_format is generated correctly."""
from app.config import settings
# env_format should contain all settings as KEY=value
pass
+2 -3
View File
@@ -1,8 +1,8 @@
"""Comprehensive unit tests for app/api/openai.py module."""
from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from unittest.mock import MagicMock, patch, Mock
@pytest.mark.unit
@@ -11,7 +11,6 @@ class TestOpenAITestConnection:
def test_openai_connection_success(self):
"""Test successful OpenAI API connection."""
import openai
from app.config import settings
with patch("openai.OpenAI") as mock_openai_class:
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests for app/api/settings.py module."""
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
+5 -13
View File
@@ -1,9 +1,10 @@
"""Comprehensive unit tests for app/api/settings.py module."""
from unittest.mock import MagicMock, patch
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from unittest.mock import MagicMock, patch
@pytest.mark.unit
@@ -51,9 +52,7 @@ class TestGetSettings:
@patch("app.api.settings.get_all_settings_from_db")
@patch("app.api.settings.get_settings_by_category")
@patch("app.api.settings.get_setting_metadata")
def test_get_settings_success(
self, mock_metadata, mock_category, mock_db_settings, client: TestClient, db_session
):
def test_get_settings_success(self, mock_metadata, mock_category, mock_db_settings, client: TestClient, db_session):
"""Test successful retrieval of settings."""
# Mock session to have admin user
mock_metadata.return_value = {"description": "Test setting", "type": "string"}
@@ -65,11 +64,7 @@ class TestGetSettings:
mock_settings.setting1 = "test_value"
# Create mock request with admin session
from starlette.testclient import TestClient as StarletteClient
response = client.get(
"/api/settings/",
cookies={"session": "admin_session"}
)
response = client.get("/api/settings/", cookies={"session": "admin_session"})
@patch("app.api.settings.get_all_settings_from_db")
def test_get_settings_database_error(self, mock_db_settings, client: TestClient, db_session):
@@ -87,7 +82,6 @@ class TestGetSetting:
@patch("app.api.settings.get_setting_metadata")
def test_get_setting_existing_key(self, mock_metadata):
"""Test retrieval of existing setting."""
from app.api.settings import get_setting
from app.config import settings
mock_metadata.return_value = {"description": "Test setting"}
@@ -263,9 +257,7 @@ class TestSettingModels:
"""Test SettingResponse model."""
from app.api.settings import SettingResponse
response = SettingResponse(
key="test_key", value="test_value", metadata={"description": "test"}
)
response = SettingResponse(key="test_key", value="test_value", metadata={"description": "test"})
assert response.key == "test_key"
assert response.value == "test_value"
assert response.metadata["description"] == "test"
+2 -3
View File
@@ -1,11 +1,10 @@
"""Integration tests for auth.py with AUTH_ENABLED=True scenarios."""
import hashlib
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import MagicMock, patch
import pytest
from app.auth import get_current_user, get_gravatar_url, require_login
from app.auth import get_gravatar_url
@pytest.mark.unit
+2 -2
View File
@@ -1,6 +1,6 @@
"""Comprehensive unit tests for app/auth.py module."""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import Request, status
@@ -344,7 +344,7 @@ class TestOAuthCallback:
"""Test OAuth callback returns error when OAuth not configured."""
with patch("app.auth.AUTH_ENABLED", True):
with patch("app.auth.OAUTH_CONFIGURED", False):
from app.auth import oauth_callback, oauth_login
from app.auth import oauth_login
mock_request = MagicMock()
+1 -1
View File
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from app.models import FileProcessingStep, FileRecord, ProcessingLog
from app.models import FileProcessingStep, FileRecord
@pytest.mark.integration
+17 -19
View File
@@ -5,8 +5,6 @@ This module tests the Celery worker configuration, task imports, and beat schedu
"""
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from celery.schedules import crontab
@pytest.mark.unit
@@ -24,7 +22,7 @@ class TestCeleryWorkerConfig:
"""Test that celery instance exists in module."""
from app import celery_worker
assert hasattr(celery_worker, 'celery')
assert hasattr(celery_worker, "celery")
assert celery_worker.celery is not None
def test_task_routes_exists(self):
@@ -32,7 +30,7 @@ class TestCeleryWorkerConfig:
from app import celery_worker
# Task routes should be configured
assert hasattr(celery_worker.celery.conf, 'task_routes')
assert hasattr(celery_worker.celery.conf, "task_routes")
def test_all_task_imports_successful(self):
"""Test that all task modules are imported successfully."""
@@ -55,39 +53,39 @@ class TestBeatScheduleConfiguration:
assert isinstance(celery.conf.beat_schedule, dict)
# Should include credential check tasks
assert 'check-credentials-regularly' in celery.conf.beat_schedule
assert 'check-credentials-daily' in celery.conf.beat_schedule
assert 'monitor-stalled-steps' in celery.conf.beat_schedule
assert "check-credentials-regularly" in celery.conf.beat_schedule
assert "check-credentials-daily" in celery.conf.beat_schedule
assert "monitor-stalled-steps" in celery.conf.beat_schedule
def test_credential_check_schedule(self):
"""Test credential check schedule configuration."""
from app.celery_worker import celery
schedule = celery.conf.beat_schedule.get('check-credentials-regularly')
schedule = celery.conf.beat_schedule.get("check-credentials-regularly")
assert schedule is not None
assert schedule['task'] == 'app.tasks.check_credentials.check_credentials'
assert 'schedule' in schedule
assert schedule['options']['expires'] == 240
assert schedule["task"] == "app.tasks.check_credentials.check_credentials"
assert "schedule" in schedule
assert schedule["options"]["expires"] == 240
def test_daily_credential_check_schedule(self):
"""Test daily credential check schedule."""
from app.celery_worker import celery
schedule = celery.conf.beat_schedule.get('check-credentials-daily')
schedule = celery.conf.beat_schedule.get("check-credentials-daily")
assert schedule is not None
assert schedule['task'] == 'app.tasks.check_credentials.check_credentials'
assert 'schedule' in schedule
assert schedule['options']['expires'] == 3600
assert schedule["task"] == "app.tasks.check_credentials.check_credentials"
assert "schedule" in schedule
assert schedule["options"]["expires"] == 3600
def test_monitor_stalled_steps_schedule(self):
"""Test monitor stalled steps schedule."""
from app.celery_worker import celery
schedule = celery.conf.beat_schedule.get('monitor-stalled-steps')
schedule = celery.conf.beat_schedule.get("monitor-stalled-steps")
assert schedule is not None
assert schedule['task'] == 'app.tasks.monitor_stalled_steps.monitor_stalled_steps'
assert 'schedule' in schedule
assert schedule['options']['expires'] == 55
assert schedule["task"] == "app.tasks.monitor_stalled_steps.monitor_stalled_steps"
assert "schedule" in schedule
assert schedule["options"]["expires"] == 55
def test_no_none_entries_in_beat_schedule(self):
"""Test that None entries are filtered from beat schedule."""
+1 -1
View File
@@ -2,7 +2,7 @@
import json
import os
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
-2
View File
@@ -2,8 +2,6 @@
Unit tests for configuration and security validation.
"""
import os
import pytest
from pydantic import ValidationError
+1 -1
View File
@@ -80,7 +80,7 @@ class TestConfigValidatorReexports:
"check_all_configs",
]
assert hasattr(config_validator, '__all__')
assert hasattr(config_validator, "__all__")
for export in expected_exports:
assert export in config_validator.__all__
-2
View File
@@ -1,7 +1,5 @@
"""Tests for app/utils/config_validator/validators.py module."""
from unittest.mock import patch
import pytest
from app.utils.config_validator.validators import (
+1 -1
View File
@@ -1,6 +1,6 @@
"""Comprehensive unit tests for app/tasks/convert_to_pdf.py module."""
from unittest.mock import MagicMock, Mock, mock_open, patch
from unittest.mock import MagicMock, mock_open, patch
import pytest
-1
View File
@@ -1,6 +1,5 @@
"""Tests for app/tasks/convert_to_pdf.py module."""
import os
from unittest.mock import MagicMock, patch
import pytest
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests to boost coverage for various small modules."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
+1 -2
View File
@@ -1,6 +1,6 @@
"""Final tests to push coverage over 60%."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
@@ -86,7 +86,6 @@ class TestCheckCredentialsFunctions:
def test_sync_test_s3_credentials(self):
"""Test save_failure_state accepts dict."""
import os
from unittest.mock import patch
from app.tasks.check_credentials import save_failure_state
+1 -3
View File
@@ -1,6 +1,5 @@
"""Tests for app/database.py module."""
import os
from unittest.mock import MagicMock, patch
import pytest
@@ -113,8 +112,7 @@ class TestSchemaMigrations:
def test_migration_adds_detail_column(self, tmp_path):
"""Test that _run_schema_migrations adds detail column to existing tables."""
from sqlalchemy import Column, Integer, String, create_engine, text
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine, text
from app.database import _run_schema_migrations
-17
View File
@@ -22,18 +22,6 @@ try:
except ModuleNotFoundError:
_has_psycopg2 = False
from tests.fixtures_integration import (
celery_app,
celery_worker,
db_session_real,
full_infrastructure,
gotenberg_container,
minio_container,
postgres_container,
redis_container,
sftp_container,
webdav_container,
)
_TEST_CREDENTIAL = "pass" # noqa: S105
@@ -67,7 +55,6 @@ class TestEndToEndWithRedis:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
# Configure to use real WebDAV server
mock_settings.webdav_url = webdav_container["url"] + "/"
mock_settings.webdav_username = webdav_container["username"]
@@ -169,7 +156,6 @@ class TestEndToEndWithRedis:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_container["url"] + "/"
mock_settings.webdav_username = webdav_container["username"]
mock_settings.webdav_password = webdav_container["password"]
@@ -233,7 +219,6 @@ class TestEndToEndWithRedis:
patch("app.tasks.upload_to_webdav.log_task_progress"),
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
):
mock_settings.webdav_url = "http://test.com/"
mock_settings.webdav_username = "user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -357,7 +342,6 @@ class TestFullInfrastructure:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
mock_settings.webdav_username = infra["webdav"]["username"]
mock_settings.webdav_password = infra["webdav"]["password"]
@@ -556,7 +540,6 @@ class TestProductionLikeScenarios:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = infra["webdav"]["url"] + "/"
mock_settings.webdav_username = infra["webdav"]["username"]
mock_settings.webdav_password = infra["webdav"]["password"]
-1
View File
@@ -1,7 +1,6 @@
"""Tests for app/tasks/embed_metadata_into_pdf.py module."""
import os
from unittest.mock import MagicMock, patch
import pytest
+4 -5
View File
@@ -1,9 +1,6 @@
"""Comprehensive unit tests for app/tasks/embed_metadata_into_pdf.py module."""
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock, mock_open, patch
from unittest.mock import MagicMock, mock_open, patch
import pytest
@@ -257,7 +254,9 @@ class TestEmbedMetadataIntoPdf:
assert "error" in result
# Verify failure was logged
failure_calls = [call for call in mock_log_progress.call_args_list if "failure" in str(call)]
failure_calls = [
call for call in mock_log_progress.call_args_list if "failure" in str(call)
]
assert len(failure_calls) > 0
@patch("app.tasks.embed_metadata_into_pdf.finalize_document_storage")
+5 -5
View File
@@ -41,9 +41,9 @@ class TestEndpointRegistration:
# We may get other errors (401, 400, 500, etc.) due to validation or missing mocks,
# but 404 specifically means the endpoint is not registered
assert response.status_code != 404, (
f"Endpoint /api/process-url returned 404 (not found). "
f"This indicates the router is not properly registered in the application. "
f"Verify that url_upload_router is included in app/api/__init__.py"
"Endpoint /api/process-url returned 404 (not found). "
"This indicates the router is not properly registered in the application. "
"Verify that url_upload_router is included in app/api/__init__.py"
)
@patch("app.api.url_upload.requests.get")
@@ -68,8 +68,8 @@ class TestEndpointRegistration:
# Should not return 405 (Method Not Allowed)
assert response.status_code != 405, (
f"Endpoint /api/process-url returned 405 (Method Not Allowed) for POST. "
f"Verify the endpoint is decorated with @router.post()"
"Endpoint /api/process-url returned 405 (Method Not Allowed) for POST. "
"Verify the endpoint is decorated with @router.post()"
)
@patch("app.api.url_upload.requests.get")
+6 -6
View File
@@ -244,9 +244,9 @@ class TestAzureDocumentIntelligenceIntegration:
assert len(result.content) > 10, f"OCR text too short: {result.content[:50]}"
# Verify the generated text is recognizable
assert (
"Acme" in result.content or "Invoice" in result.content
), f"OCR text does not contain expected keywords: {result.content[:200]}"
assert "Acme" in result.content or "Invoice" in result.content, (
f"OCR text does not contain expected keywords: {result.content[:200]}"
)
# Retrieve the searchable PDF output
operation_id = poller.details["operation_id"]
@@ -602,9 +602,9 @@ class TestFullOCRMetadataPipeline:
# The generated invoice should be classified reasonably
doc_type = metadata["document_type"].lower()
assert any(
kw in doc_type for kw in ("invoice", "rechnung", "bill")
), f"Unexpected document_type: {metadata['document_type']}"
assert any(kw in doc_type for kw in ("invoice", "rechnung", "bill")), (
f"Unexpected document_type: {metadata['document_type']}"
)
finally:
os.unlink(pdf_path)
+30 -29
View File
@@ -72,21 +72,23 @@ class TestExtractMetadataWithGpt:
"""Test successful metadata extraction with valid GPT response."""
# Mock the OpenAI client response
mock_completion = MagicMock()
mock_completion.choices[0].message.content = json.dumps({
"filename": "2024-01-15_Invoice_Amazon",
"empfaenger": "John Doe",
"absender": "Amazon",
"correspondent": "Amazon",
"kommunikationsart": "Rechnung",
"kommunikationskategorie": "Finanz_und_Vertragsdokumente",
"document_type": "Invoice",
"tags": ["invoice", "amazon", "online-shopping"],
"language": "de",
"title": "Amazon Purchase Invoice",
"confidence_score": 95,
"reference_number": "INV-2024-001",
"monetary_amounts": ["99.99 EUR"]
})
mock_completion.choices[0].message.content = json.dumps(
{
"filename": "2024-01-15_Invoice_Amazon",
"empfaenger": "John Doe",
"absender": "Amazon",
"correspondent": "Amazon",
"kommunikationsart": "Rechnung",
"kommunikationskategorie": "Finanz_und_Vertragsdokumente",
"document_type": "Invoice",
"tags": ["invoice", "amazon", "online-shopping"],
"language": "de",
"title": "Amazon Purchase Invoice",
"confidence_score": 95,
"reference_number": "INV-2024-001",
"monetary_amounts": ["99.99 EUR"],
}
)
mock_client.chat.completions.create.return_value = mock_completion
# Set task request context directly on the Celery task
@@ -119,7 +121,9 @@ class TestExtractMetadataWithGpt:
def test_handles_json_in_backticks(self, mock_client, mock_log_progress, mock_embed_task):
"""Test extraction handles JSON wrapped in markdown code blocks."""
mock_completion = MagicMock()
mock_completion.choices[0].message.content = '```json\n{"filename": "test.pdf", "document_type": "Unknown"}\n```'
mock_completion.choices[
0
].message.content = '```json\n{"filename": "test.pdf", "document_type": "Unknown"}\n```'
mock_client.chat.completions.create.return_value = mock_completion
extract_metadata_with_gpt.request.id = "test-task-id"
@@ -193,7 +197,7 @@ class TestExtractMetadataWithGpt:
result = extract_metadata_with_gpt.__wrapped__(
filename="test.pdf",
cleaned_text="Sample text",
file_id=None # Not provided
file_id=None, # Not provided
)
assert result["metadata"]["filename"] == "test.pdf"
@@ -207,10 +211,9 @@ class TestExtractMetadataWithGpt:
"""Test filename validation to prevent path traversal."""
mock_completion = MagicMock()
# Try to inject a malicious filename
mock_completion.choices[0].message.content = json.dumps({
"filename": "../../../etc/passwd",
"document_type": "Invoice"
})
mock_completion.choices[0].message.content = json.dumps(
{"filename": "../../../etc/passwd", "document_type": "Invoice"}
)
mock_client.chat.completions.create.return_value = mock_completion
extract_metadata_with_gpt.request.id = "test-task-id"
@@ -227,10 +230,9 @@ class TestExtractMetadataWithGpt:
def test_validates_filename_with_dots(self, mock_client, mock_log_progress, mock_embed_task):
"""Test filename validation rejects '..' in filenames."""
mock_completion = MagicMock()
mock_completion.choices[0].message.content = json.dumps({
"filename": "test..invoice.pdf",
"document_type": "Invoice"
})
mock_completion.choices[0].message.content = json.dumps(
{"filename": "test..invoice.pdf", "document_type": "Invoice"}
)
mock_client.chat.completions.create.return_value = mock_completion
extract_metadata_with_gpt.request.id = "test-task-id"
@@ -246,10 +248,9 @@ class TestExtractMetadataWithGpt:
def test_accepts_valid_filename(self, mock_client, mock_log_progress, mock_embed_task):
"""Test that valid filenames are accepted."""
mock_completion = MagicMock()
mock_completion.choices[0].message.content = json.dumps({
"filename": "2024-01-15_Invoice_Amazon.pdf",
"document_type": "Invoice"
})
mock_completion.choices[0].message.content = json.dumps(
{"filename": "2024-01-15_Invoice_Amazon.pdf", "document_type": "Invoice"}
)
mock_client.chat.completions.create.return_value = mock_completion
extract_metadata_with_gpt.request.id = "test-task-id"
-1
View File
@@ -2,7 +2,6 @@
Tests for file detail view improvements including reprocessing and preview endpoints.
"""
import os
from unittest.mock import MagicMock, patch
import pytest
-10
View File
@@ -9,14 +9,10 @@ Tests the new features:
"""
import json
import os
import tempfile
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.models import FileRecord
@@ -38,7 +34,6 @@ def sample_metadata():
@pytest.mark.integration
def test_file_detail_page_with_metadata(client: TestClient, db_session, sample_pdf_file):
"""Test file detail page displays GPT metadata correctly"""
from app.models import FileRecord
# Create a file record with paths
file_record = FileRecord(
@@ -68,7 +63,6 @@ def test_file_detail_page_with_metadata(client: TestClient, db_session, sample_p
@pytest.mark.integration
def test_file_detail_with_gpt_metadata(client: TestClient, db_session, sample_pdf_file, sample_metadata, tmp_path):
"""Test file detail page with GPT metadata JSON"""
from app.models import FileRecord
# Create processed file path and metadata JSON
processed_file = tmp_path / "2024-01-15_Company_Invoice.pdf"
@@ -106,7 +100,6 @@ def test_file_detail_with_gpt_metadata(client: TestClient, db_session, sample_pd
@pytest.mark.integration
def test_preview_original_file_endpoint(client: TestClient, db_session, sample_pdf_file):
"""Test original file preview endpoint"""
from app.models import FileRecord
file_record = FileRecord(
filehash="test789ghi",
@@ -130,7 +123,6 @@ def test_preview_original_file_endpoint(client: TestClient, db_session, sample_p
@pytest.mark.integration
def test_preview_processed_file_endpoint(client: TestClient, db_session, sample_pdf_file, tmp_path):
"""Test processed file preview endpoint"""
from app.models import FileRecord
# Create processed file
processed_file = tmp_path / "processed.pdf"
@@ -158,7 +150,6 @@ def test_preview_processed_file_endpoint(client: TestClient, db_session, sample_
@pytest.mark.integration
def test_preview_missing_file_returns_404(client: TestClient, db_session, sample_pdf_file):
"""Test preview endpoint returns 404 when file doesn't exist"""
from app.models import FileRecord
file_record = FileRecord(
filehash="test202mno",
@@ -185,7 +176,6 @@ def test_preview_missing_file_returns_404(client: TestClient, db_session, sample
@pytest.mark.integration
def test_file_detail_shows_file_status_indicators(client: TestClient, db_session, sample_pdf_file):
"""Test file detail page shows correct status indicators for original and processed files"""
from app.models import FileRecord
file_record = FileRecord(
filehash="test303pqr",
-2
View File
@@ -2,8 +2,6 @@
Tests for file listing, pagination, filtering, and detail endpoints.
"""
from datetime import datetime
import pytest
from fastapi.testclient import TestClient
+3 -3
View File
@@ -83,9 +83,9 @@ class TestSplitPdfBySize:
# that can cause files to exceed the target size by ~20-50%. We allow 1.5x (50%) margin.
PDF_OVERHEAD_MULTIPLIER = 1.5
for split_file in split_files:
assert (
os.path.getsize(split_file) <= max_size * PDF_OVERHEAD_MULTIPLIER
), f"Split file {split_file} should respect size limit (with PDF overhead allowance)"
assert os.path.getsize(split_file) <= max_size * PDF_OVERHEAD_MULTIPLIER, (
f"Split file {split_file} should respect size limit (with PDF overhead allowance)"
)
# Cleanup split files
for split_file in split_files:
+1 -3
View File
@@ -7,14 +7,12 @@ This test module verifies that:
3. Files with completed steps show "completed" not "processing"
"""
from datetime import datetime, timedelta
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import FileProcessingStep, FileRecord
from app.models import FileRecord
from app.utils.step_manager import get_file_overall_status, get_step_summary, initialize_file_steps, update_step_status
-1
View File
@@ -26,7 +26,6 @@ def mock_celery_tasks():
patch("app.api.files.process_document") as mock_process_task,
patch("app.api.files.convert_to_pdf") as mock_convert_task,
):
# Setup default return values for .delay()
mock_task = MagicMock()
mock_task.id = "test-task-id-123"
+1 -3
View File
@@ -5,9 +5,7 @@ Tests filename sanitization and manipulation functions.
"""
import os
from datetime import datetime
from pathlib import Path
from unittest.mock import Mock, patch
from unittest.mock import Mock
import pytest
+3 -1
View File
@@ -102,7 +102,9 @@ class TestFinalizeDocumentStorage:
with patch("app.tasks.finalize_document_storage.os.path.exists", return_value=True):
with patch("app.tasks.finalize_document_storage.os.path.getsize", return_value=50000):
with patch("app.tasks.finalize_document_storage.os.path.basename", return_value="doc.pdf"):
with patch("app.tasks.finalize_document_storage.os.path.join", return_value="/tmp/tmp/original.pdf"):
with patch(
"app.tasks.finalize_document_storage.os.path.join", return_value="/tmp/tmp/original.pdf"
):
with patch("app.tasks.finalize_document_storage.settings") as mock_settings:
mock_settings.workdir = "/tmp"
-2
View File
@@ -2,7 +2,6 @@
import json
import os
from datetime import datetime, timezone
from email.message import EmailMessage
from unittest.mock import MagicMock, patch
@@ -11,7 +10,6 @@ import pytest
from app.tasks.imap_tasks import (
fetch_attachments_and_enqueue,
find_all_mail_xlist,
load_processed_emails,
save_processed_emails,
)
-1
View File
@@ -1,6 +1,5 @@
"""Tests for app/tasks/imap_tasks.py module."""
import json
import os
from datetime import datetime, timedelta, timezone
from email.message import EmailMessage
+19 -19
View File
@@ -4,17 +4,17 @@ Tests for app/tasks/monitor_stalled_steps.py
This module tests the periodic task that monitors and recovers stalled processing steps.
"""
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import MagicMock, patch, call
from datetime import datetime
@pytest.mark.unit
class TestMonitorStalledSteps:
"""Test monitor_stalled_steps task."""
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
def test_monitor_stalled_steps_no_stalled(self, mock_session_local, mock_mark_stalled):
"""Test monitor_stalled_steps when no stalled steps found."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -33,8 +33,8 @@ class TestMonitorStalledSteps:
assert result == {"recovered": 0}
mock_mark_stalled.assert_called_once_with(mock_db)
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
def test_monitor_stalled_steps_with_stalled(self, mock_session_local, mock_mark_stalled):
"""Test monitor_stalled_steps when stalled steps are found."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -53,9 +53,9 @@ class TestMonitorStalledSteps:
assert result == {"recovered": 3}
mock_mark_stalled.assert_called_once_with(mock_db)
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch('app.tasks.monitor_stalled_steps.logger')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
@patch("app.tasks.monitor_stalled_steps.logger")
def test_monitor_stalled_steps_logs_recovery(self, mock_logger, mock_session_local, mock_mark_stalled):
"""Test that monitor_stalled_steps logs recovery actions."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -75,9 +75,9 @@ class TestMonitorStalledSteps:
log_message = mock_logger.warning.call_args[0][0]
assert "Recovered 2 stalled step(s)" in log_message
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch('app.tasks.monitor_stalled_steps.logger')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
@patch("app.tasks.monitor_stalled_steps.logger")
def test_monitor_stalled_steps_logs_debug_when_none(self, mock_logger, mock_session_local, mock_mark_stalled):
"""Test that monitor_stalled_steps logs debug message when no stalled steps."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -97,9 +97,9 @@ class TestMonitorStalledSteps:
log_message = mock_logger.debug.call_args[0][0]
assert "No stalled steps found" in log_message
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch('app.tasks.monitor_stalled_steps.logger')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
@patch("app.tasks.monitor_stalled_steps.logger")
def test_monitor_stalled_steps_handles_exceptions(self, mock_logger, mock_session_local, mock_mark_stalled):
"""Test that monitor_stalled_steps handles exceptions gracefully."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -118,8 +118,8 @@ class TestMonitorStalledSteps:
assert result == {"error": "Database error", "recovered": 0}
mock_logger.error.assert_called_once()
@patch('app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed')
@patch('app.tasks.monitor_stalled_steps.SessionLocal')
@patch("app.tasks.monitor_stalled_steps.mark_stalled_steps_as_failed")
@patch("app.tasks.monitor_stalled_steps.SessionLocal")
def test_monitor_stalled_steps_uses_context_manager(self, mock_session_local, mock_mark_stalled):
"""Test that monitor_stalled_steps uses context manager for database session."""
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
@@ -145,8 +145,8 @@ class TestMonitorStalledSteps:
from app.tasks.monitor_stalled_steps import monitor_stalled_steps
# Should have task attributes
assert hasattr(monitor_stalled_steps, 'apply_async')
assert hasattr(monitor_stalled_steps, 'delay')
assert hasattr(monitor_stalled_steps, "apply_async")
assert hasattr(monitor_stalled_steps, "delay")
assert callable(monitor_stalled_steps)
def test_monitor_stalled_steps_task_name(self):
+1 -1
View File
@@ -4,7 +4,7 @@ Tests for app/utils/notification.py
Tests notification utilities and URL masking.
"""
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, patch
import pytest
+1 -2
View File
@@ -1,12 +1,11 @@
"""Tests for app/utils/notification.py module."""
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
from app.utils.notification import (
_mask_sensitive_url,
init_apprise,
notify_celery_failure,
notify_credential_failure,
notify_file_processed,
+15 -25
View File
@@ -9,8 +9,9 @@ These tests use a real OIDC flow with a mock OAuth2 server to test:
- Session management
"""
from unittest.mock import patch
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
@@ -25,9 +26,7 @@ class TestOAuthLoginFlow:
# Check that OAuth option is shown
assert b"oauth" in response.content.lower() or b"sign" in response.content.lower()
def test_oauth_login_redirects_to_provider(
self, oauth_enabled_app: TestClient, oauth_config: dict
):
def test_oauth_login_redirects_to_provider(self, oauth_enabled_app: TestClient, oauth_config: dict):
"""Test that /oauth-login redirects to the OAuth provider."""
response = oauth_enabled_app.get("/oauth-login", follow_redirects=False)
@@ -44,12 +43,15 @@ class TestOAuthLoginFlow:
"""Test that OAuth login fails gracefully when not configured."""
# Test with OAuth disabled
import os
original = os.environ.get("AUTH_ENABLED")
os.environ["AUTH_ENABLED"] = "False"
try:
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app, base_url="http://localhost")
response = client.get("/oauth-login", follow_redirects=False)
@@ -112,9 +114,7 @@ class TestOAuthCallback:
@pytest.mark.asyncio
@patch("app.auth.oauth.authentik.authorize_access_token")
async def test_oauth_callback_with_admin_user(
self, mock_authorize, oauth_enabled_app: TestClient
):
async def test_oauth_callback_with_admin_user(self, mock_authorize, oauth_enabled_app: TestClient):
"""Test OAuth callback with admin user group."""
mock_authorize.return_value = {
"access_token": "mock-access-token",
@@ -136,9 +136,7 @@ class TestOAuthCallback:
@pytest.mark.asyncio
@patch("app.auth.oauth.authentik.authorize_access_token")
async def test_oauth_callback_rejects_non_admin(
self, mock_authorize, oauth_enabled_app: TestClient
):
async def test_oauth_callback_rejects_non_admin(self, mock_authorize, oauth_enabled_app: TestClient):
"""Test that OAuth callback authenticates non-admin users with is_admin=False."""
mock_authorize.return_value = {
"access_token": "mock-access-token",
@@ -185,9 +183,7 @@ class TestOAuthSessionManagement:
# Note: May redirect to login if session not properly set
assert response.status_code in [200, 302]
def test_unauthenticated_user_redirected_to_login(
self, oauth_enabled_app: TestClient
):
def test_unauthenticated_user_redirected_to_login(self, oauth_enabled_app: TestClient):
"""Test that unauthenticated users are redirected to login."""
# Try to access protected route without authentication
response = oauth_enabled_app.get("/files", follow_redirects=False)
@@ -198,9 +194,7 @@ class TestOAuthSessionManagement:
@pytest.mark.asyncio
@patch("app.auth.oauth.authentik.authorize_access_token")
async def test_logout_clears_session(
self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict
):
async def test_logout_clears_session(self, mock_authorize, oauth_enabled_app: TestClient, test_user_info: dict):
"""Test that logout clears user session."""
# Mock successful authentication
mock_authorize.return_value = {
@@ -222,9 +216,7 @@ class TestOAuthSessionManagement:
class TestOAuthErrorHandling:
"""Test error handling in OAuth flows."""
def test_oauth_callback_without_code_shows_error(
self, oauth_enabled_app: TestClient
):
def test_oauth_callback_without_code_shows_error(self, oauth_enabled_app: TestClient):
"""Test OAuth callback without authorization code."""
response = oauth_enabled_app.get("/oauth-callback", follow_redirects=False)
@@ -233,9 +225,7 @@ class TestOAuthErrorHandling:
@pytest.mark.asyncio
@patch("app.auth.oauth.authentik.authorize_access_token")
async def test_oauth_callback_with_invalid_token(
self, mock_authorize, oauth_enabled_app: TestClient
):
async def test_oauth_callback_with_invalid_token(self, mock_authorize, oauth_enabled_app: TestClient):
"""Test OAuth callback with invalid token."""
# Mock token exchange failure
mock_authorize.side_effect = Exception("Invalid authorization code")
@@ -252,9 +242,7 @@ class TestOAuthErrorHandling:
@pytest.mark.asyncio
@patch("app.auth.oauth.authentik.authorize_access_token")
async def test_oauth_callback_without_userinfo(
self, mock_authorize, oauth_enabled_app: TestClient
):
async def test_oauth_callback_without_userinfo(self, mock_authorize, oauth_enabled_app: TestClient):
"""Test OAuth callback when userinfo is missing."""
# Mock token without userinfo
mock_authorize.return_value = {
@@ -286,6 +274,7 @@ class TestRealOAuthIntegration:
pytest.skip("Real OAuth credentials not available")
import requests
response = requests.get(oauth_config["server_metadata_url"], timeout=10)
assert response.status_code == 200
@@ -300,6 +289,7 @@ class TestRealOAuthIntegration:
pytest.skip("Real OAuth credentials not available")
import requests
# Get well-known config first
response = requests.get(oauth_config["server_metadata_url"], timeout=10)
config = response.json()
-15
View File
@@ -5,7 +5,6 @@ These tests verify OCR processing logic with mocked external AI/ML services
(OpenAI, Azure Document Intelligence). Tests cover typical and edge cases.
"""
import os
from unittest.mock import MagicMock, Mock, patch
import pytest
@@ -95,7 +94,6 @@ startxref
patch("app.tasks.process_with_azure_document_intelligence.settings") as mock_settings,
patch("app.tasks.process_with_azure_document_intelligence.rotate_pdf_pages") as mock_rotate,
):
mock_settings.workdir = str(tmp_path)
mock_rotate.delay = MagicMock()
@@ -144,7 +142,6 @@ startxref
patch("app.tasks.process_with_azure_document_intelligence.settings") as mock_settings,
patch("app.tasks.process_with_azure_document_intelligence.os.path.getsize") as mock_getsize,
):
mock_settings.workdir = str(tmp_path)
# Mock file size to be larger than 500 MB
mock_getsize.return_value = AZURE_DOC_INTELLIGENCE_LIMITS["max_file_size_bytes"] + 1024
@@ -169,7 +166,6 @@ startxref
patch("app.tasks.process_with_azure_document_intelligence.settings") as mock_settings,
patch("app.tasks.process_with_azure_document_intelligence.get_pdf_page_count") as mock_page_count,
):
mock_settings.workdir = str(tmp_path)
# Mock page count to exceed limit
mock_page_count.return_value = AZURE_DOC_INTELLIGENCE_LIMITS["max_pages"] + 1
@@ -215,7 +211,6 @@ startxref
patch("app.tasks.process_with_azure_document_intelligence.get_pdf_page_count") as mock_page_count,
patch("app.tasks.process_with_azure_document_intelligence.rotate_pdf_pages"),
):
mock_settings.workdir = str(tmp_path)
# Return None to simulate page count determination failure
mock_page_count.return_value = None
@@ -267,7 +262,6 @@ startxref
patch("app.tasks.process_with_azure_document_intelligence.settings") as mock_settings,
patch("app.tasks.process_with_azure_document_intelligence.rotate_pdf_pages") as mock_rotate,
):
mock_settings.workdir = str(tmp_path)
mock_rotate.delay = MagicMock()
@@ -302,7 +296,6 @@ startxref
),
patch("app.tasks.process_with_azure_document_intelligence.settings") as mock_settings,
):
mock_settings.workdir = str(tmp_path)
# Should raise the exception
@@ -440,7 +433,6 @@ class TestRefineTextWithGPT:
patch.object(metadata_module, "extract_metadata_with_gpt") as mock_extract,
patch("app.tasks.refine_text_with_gpt.settings") as mock_settings,
):
mock_settings.openai_model = "gpt-4"
mock_extract.delay = MagicMock()
@@ -473,7 +465,6 @@ class TestRefineTextWithGPT:
patch("app.tasks.refine_text_with_gpt.client", mock_client),
patch("app.tasks.refine_text_with_gpt.settings") as mock_settings,
):
mock_settings.openai_model = "gpt-4"
# Should raise the exception
@@ -582,7 +573,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -617,7 +607,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -651,7 +640,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -676,7 +664,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -712,7 +699,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -783,7 +769,6 @@ startxref
patch("app.tasks.rotate_pdf_pages.settings") as mock_settings,
patch("app.tasks.rotate_pdf_pages.extract_metadata_with_gpt") as mock_extract,
):
mock_settings.workdir = str(tmp_path)
mock_extract.delay = MagicMock()
@@ -93,7 +93,6 @@ startxref
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.extract_metadata_with_gpt") as mock_extract,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
@@ -194,7 +193,6 @@ startxref
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.extract_metadata_with_gpt") as mock_extract,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
+1 -7
View File
@@ -4,11 +4,9 @@ Security tests for path traversal vulnerabilities.
Tests all file path operations to ensure they properly prevent path traversal attacks.
"""
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import MagicMock, patch
import pytest
@@ -357,7 +355,6 @@ class TestFileUploadSecurity:
def test_ui_upload_uses_basename(self):
"""Test that ui_upload extracts basename to prevent path traversal."""
import os
from app.utils.filename_utils import sanitize_filename
@@ -389,7 +386,6 @@ class TestFileUploadSecurity:
def test_sanitize_after_basename(self):
"""Test that sanitization happens after basename extraction."""
import os
from app.utils.filename_utils import sanitize_filename
@@ -443,7 +439,6 @@ class TestEndToEndPathTraversal:
def test_full_upload_flow_prevents_traversal(self, tmp_path):
"""Test complete upload flow prevents path traversal."""
import os
import uuid
from app.utils.filename_utils import sanitize_filename
@@ -474,7 +469,6 @@ class TestEndToEndPathTraversal:
def test_metadata_embedding_flow_prevents_traversal(self, tmp_path):
"""Test metadata embedding flow prevents path traversal."""
import os
from app.utils.filename_utils import sanitize_filename
-6
View File
@@ -5,11 +5,9 @@ These tests verify that the process_document task correctly handles file process
and doesn't cause DetachedInstanceError when accessing database objects.
"""
import os
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy.orm import Session
from app.models import FileRecord
from app.tasks.process_document import process_document
@@ -92,7 +90,6 @@ startxref
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.extract_metadata_with_gpt") as mock_extract,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
@@ -153,7 +150,6 @@ def test_process_document_duplicate_file(db_session, tmp_path):
patch("app.tasks.process_document.SessionLocal") as mock_session_local,
patch("app.tasks.process_document.log_task_progress"),
):
# Setup mocks
mock_session_local.return_value.__enter__.return_value = db_session
mock_session_local.return_value.__exit__.return_value = None
@@ -226,7 +222,6 @@ startxref
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
@@ -345,7 +340,6 @@ startxref
patch("app.tasks.process_document.log_task_progress"),
patch("app.tasks.process_document.extract_metadata_with_gpt") as mock_extract,
):
# Setup mocks
mock_settings.workdir = str(tmp_path)
mock_session_local.return_value.__enter__.return_value = db_session
+1 -2
View File
@@ -2,8 +2,7 @@
Tests for /processall endpoint throttling behavior.
"""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
from fastapi.testclient import TestClient
+12 -10
View File
@@ -4,9 +4,9 @@ Tests for app/middleware/rate_limit_decorators.py
This module tests the rate limiting decorators for API endpoints.
"""
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import MagicMock, patch, Mock
from fastapi import Request
@pytest.mark.unit
@@ -47,7 +47,7 @@ class TestRateLimitDecorators:
assert limiter1 is limiter2
assert limiter1 is mock_limiter
@patch('app.middleware.rate_limit_decorators.get_limiter')
@patch("app.middleware.rate_limit_decorators.get_limiter")
def test_limit_decorator(self, mock_get_limiter):
"""Test the limit decorator applies rate limit."""
from app.middleware.rate_limit_decorators import limit
@@ -65,7 +65,7 @@ class TestRateLimitDecorators:
# Verify limiter.limit was called with correct rate
mock_limiter.limit.assert_called_once_with("10/minute")
@patch('app.middleware.rate_limit_decorators.get_limiter')
@patch("app.middleware.rate_limit_decorators.get_limiter")
def test_limit_decorator_with_different_rates(self, mock_get_limiter):
"""Test limit decorator with various rate limit strings."""
from app.middleware.rate_limit_decorators import limit
@@ -87,7 +87,7 @@ class TestRateLimitDecorators:
mock_limiter.limit.assert_called_once_with(rate)
@patch('app.middleware.rate_limit_decorators.get_limiter')
@patch("app.middleware.rate_limit_decorators.get_limiter")
def test_exempt_decorator(self, mock_get_limiter):
"""Test the exempt decorator exempts endpoint from rate limiting."""
from app.middleware.rate_limit_decorators import exempt
@@ -105,7 +105,7 @@ class TestRateLimitDecorators:
# Verify limiter.exempt was called
mock_limiter.exempt.assert_called_once()
@patch('app.middleware.rate_limit_decorators.get_limiter')
@patch("app.middleware.rate_limit_decorators.get_limiter")
def test_limit_decorator_preserves_function(self, mock_get_limiter):
"""Test that limit decorator preserves the original function."""
from app.middleware.rate_limit_decorators import limit
@@ -126,10 +126,11 @@ class TestRateLimitDecorators:
# Function should still work
import asyncio
result = asyncio.run(decorated_function())
assert result == "original"
@patch('app.middleware.rate_limit_decorators.get_limiter')
@patch("app.middleware.rate_limit_decorators.get_limiter")
def test_exempt_decorator_preserves_function(self, mock_get_limiter):
"""Test that exempt decorator preserves the original function."""
from app.middleware.rate_limit_decorators import exempt
@@ -146,6 +147,7 @@ class TestRateLimitDecorators:
# Function should still work
import asyncio
result = asyncio.run(decorated_function())
assert result == "exempted"
@@ -153,9 +155,9 @@ class TestRateLimitDecorators:
"""Test that the module can be imported without errors."""
from app.middleware import rate_limit_decorators
assert hasattr(rate_limit_decorators, 'get_limiter')
assert hasattr(rate_limit_decorators, 'limit')
assert hasattr(rate_limit_decorators, 'exempt')
assert hasattr(rate_limit_decorators, "get_limiter")
assert hasattr(rate_limit_decorators, "limit")
assert hasattr(rate_limit_decorators, "exempt")
assert callable(rate_limit_decorators.get_limiter)
assert callable(rate_limit_decorators.limit)
assert callable(rate_limit_decorators.exempt)
-4
View File
@@ -7,10 +7,7 @@ These tests validate that rate limiting is properly applied to API endpoints
to prevent abuse and DoS attacks.
"""
import time
import pytest
from fastapi import status
@pytest.mark.unit
@@ -101,7 +98,6 @@ def test_rate_limit_exceeded_returns_429(client):
@pytest.mark.security
def test_rate_limiting_uses_correct_identifier():
"""Test that rate limiting uses IP or user ID as identifier."""
from fastapi import Request
from app.middleware.rate_limit import get_identifier
+1 -2
View File
@@ -1,7 +1,6 @@
"""Tests for app/tasks/upload_with_rclone.py module."""
import os
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
+3 -3
View File
@@ -142,9 +142,9 @@ def test_x_frame_options_valid_value(client):
x_frame_value = response.headers["X-Frame-Options"]
valid_values = ["DENY", "SAMEORIGIN"]
# Note: ALLOW-FROM is deprecated in modern browsers; use CSP frame-ancestors instead
assert x_frame_value in valid_values or x_frame_value.startswith(
"ALLOW-FROM"
), f"Invalid X-Frame-Options value: {x_frame_value}"
assert x_frame_value in valid_values or x_frame_value.startswith("ALLOW-FROM"), (
f"Invalid X-Frame-Options value: {x_frame_value}"
)
@pytest.mark.integration
-2
View File
@@ -6,7 +6,6 @@ import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.config import Settings
from app.models import ApplicationSettings
from app.utils.config_loader import convert_setting_value, load_settings_from_db
from app.utils.settings_service import (
@@ -224,7 +223,6 @@ class TestSettingsPrecedence:
def test_db_overrides_default(self, db_session: Session):
"""Test that database settings override default values"""
# Create a minimal test settings object
from typing import Optional
from pydantic_settings import BaseSettings
+20 -20
View File
@@ -4,19 +4,20 @@ Tests for app/utils/step_timeout.py
This module tests step timeout detection and handling logic.
"""
import pytest
from unittest.mock import MagicMock, patch, call
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch
import pytest
@pytest.mark.unit
class TestStepTimeout:
"""Test step timeout utilities."""
@patch('app.utils.step_timeout.settings')
@patch("app.utils.step_timeout.settings")
def test_get_step_timeout_default(self, mock_settings):
"""Test get_step_timeout returns default value."""
from app.utils.step_timeout import get_step_timeout, DEFAULT_STEP_TIMEOUT
from app.utils.step_timeout import DEFAULT_STEP_TIMEOUT, get_step_timeout
# No custom timeout in settings
del mock_settings.step_timeout
@@ -25,7 +26,7 @@ class TestStepTimeout:
assert timeout == DEFAULT_STEP_TIMEOUT
assert timeout == 600
@patch('app.utils.step_timeout.settings')
@patch("app.utils.step_timeout.settings")
def test_get_step_timeout_custom(self, mock_settings):
"""Test get_step_timeout returns custom value from settings."""
from app.utils.step_timeout import get_step_timeout
@@ -36,11 +37,10 @@ class TestStepTimeout:
timeout = get_step_timeout()
assert timeout == 300
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_no_steps(self, mock_logger):
"""Test mark_stalled_steps_as_failed when no stalled steps exist."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
# Mock database session with proper query chain
mock_db = MagicMock()
@@ -53,11 +53,11 @@ class TestStepTimeout:
# No steps should be marked
assert count == 0
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_with_stalled_steps(self, mock_logger):
"""Test mark_stalled_steps_as_failed marks stalled steps."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
from app.utils.step_timeout import mark_stalled_steps_as_failed
# Create mock stalled steps
step1 = MagicMock(spec=FileProcessingStep)
@@ -90,11 +90,11 @@ class TestStepTimeout:
assert "timeout" in step2.error_message.lower()
mock_db.commit.assert_called_once()
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_custom_timeout(self, mock_logger):
"""Test mark_stalled_steps_as_failed with custom timeout."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
from app.utils.step_timeout import mark_stalled_steps_as_failed
# Create mock step that's stalled with custom timeout
step = MagicMock(spec=FileProcessingStep)
@@ -116,11 +116,11 @@ class TestStepTimeout:
assert step.status == "failure"
assert "150 seconds" in step.error_message
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_for_specific_file(self, mock_logger):
"""Test mark_stalled_steps_as_failed for specific file."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
from app.utils.step_timeout import mark_stalled_steps_as_failed
# Create mock stalled step
step = MagicMock(spec=FileProcessingStep)
@@ -141,11 +141,11 @@ class TestStepTimeout:
assert count == 1
assert step.status == "failure"
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_error_message_format(self, mock_logger):
"""Test that error message includes all necessary details."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
from app.utils.step_timeout import mark_stalled_steps_as_failed
# Create mock stalled step
started_time = datetime.utcnow() - timedelta(seconds=700)
@@ -170,11 +170,11 @@ class TestStepTimeout:
assert "timeout" in error_msg.lower()
assert str(started_time) in error_msg
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_mark_stalled_steps_as_failed_logging(self, mock_logger):
"""Test that mark_stalled_steps_as_failed logs warnings and errors."""
from app.utils.step_timeout import mark_stalled_steps_as_failed
from app.models import FileProcessingStep
from app.utils.step_timeout import mark_stalled_steps_as_failed
# Create mock stalled step
step = MagicMock(spec=FileProcessingStep)
@@ -196,11 +196,11 @@ class TestStepTimeout:
mock_logger.warning.assert_called_once()
mock_logger.error.assert_called_once()
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_check_and_recover_stalled_file_found(self, mock_logger):
"""Test check_and_recover_stalled_file when stalled steps found."""
from app.utils.step_timeout import check_and_recover_stalled_file
from app.models import FileProcessingStep
from app.utils.step_timeout import check_and_recover_stalled_file
# Create mock stalled step
step = MagicMock(spec=FileProcessingStep)
@@ -220,7 +220,7 @@ class TestStepTimeout:
# Should return True when stalled steps found
assert result is True
@patch('app.utils.step_timeout.logger')
@patch("app.utils.step_timeout.logger")
def test_check_and_recover_stalled_file_not_found(self, mock_logger):
"""Test check_and_recover_stalled_file when no stalled steps."""
from app.utils.step_timeout import check_and_recover_stalled_file
-1
View File
@@ -10,7 +10,6 @@ import os
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy.orm import Session
from app.models import FileRecord
-2
View File
@@ -1,7 +1,5 @@
"""Tests for app/tasks/upload_to_email.py module."""
from unittest.mock import MagicMock, patch
import pytest
-2
View File
@@ -1,7 +1,5 @@
"""Additional tests for upload_to_ftp task."""
from unittest.mock import MagicMock, patch
import pytest
+1 -17
View File
@@ -3,7 +3,7 @@ Tests for upload tasks including OneDrive, S3, FTP, SFTP, WebDAV, Google Drive,
"""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
@@ -53,7 +53,6 @@ def test_upload_to_onedrive_accepts_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress"),
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
@@ -77,7 +76,6 @@ def test_upload_to_onedrive_without_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress"),
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
@@ -97,7 +95,6 @@ def test_upload_to_s3_accepts_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress"),
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
@@ -119,7 +116,6 @@ def test_upload_to_s3_without_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress"),
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
@@ -157,7 +153,6 @@ def test_upload_to_onedrive_logs_with_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_onedrive.upload_large_file") as mock_upload,
patch("app.tasks.upload_to_onedrive.log_task_progress") as mock_log,
):
# Setup mocks
mock_token.return_value = "test_access_token"
mock_session.return_value = "https://upload.url"
@@ -182,7 +177,6 @@ def test_upload_to_s3_logs_with_file_id(sample_text_file, mock_settings):
patch("app.tasks.upload_to_s3.boto3.client") as mock_boto_client,
patch("app.tasks.upload_to_s3.log_task_progress") as mock_log,
):
# Setup mock S3 client
mock_s3 = Mock()
mock_boto_client.return_value = mock_s3
@@ -211,7 +205,6 @@ def test_upload_to_ftp_accepts_file_id(sample_text_file):
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
patch("app.tasks.upload_to_ftp.log_task_progress"),
):
# Setup settings
mock_settings.ftp_host = "ftp.example.com"
mock_settings.ftp_port = 21
@@ -241,7 +234,6 @@ def test_upload_to_ftp_without_file_id(sample_text_file):
patch("app.tasks.upload_to_ftp.ftplib.FTP") as mock_ftp,
patch("app.tasks.upload_to_ftp.log_task_progress"),
):
# Setup settings
mock_settings.ftp_host = "ftp.example.com"
mock_settings.ftp_username = "test_user"
@@ -270,7 +262,6 @@ def test_upload_to_sftp_accepts_file_id(sample_text_file):
patch("app.tasks.upload_to_sftp.extract_remote_path") as mock_extract,
patch("app.tasks.upload_to_sftp.get_unique_filename") as mock_unique,
):
# Setup settings
mock_settings.sftp_host = "sftp.example.com"
mock_settings.sftp_port = 22
@@ -303,7 +294,6 @@ def test_upload_to_webdav_accepts_file_id(sample_text_file):
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
# Setup settings
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
@@ -334,7 +324,6 @@ def test_upload_to_google_drive_accepts_file_id(sample_text_file):
patch("app.tasks.upload_to_google_drive.settings") as mock_settings,
patch("app.tasks.upload_to_google_drive.log_task_progress"),
):
# Setup settings
mock_settings.google_drive_folder_id = "test_folder_id"
@@ -374,7 +363,6 @@ def test_upload_to_email_accepts_file_id(sample_text_file):
patch("app.tasks.upload_to_email._send_email_with_smtp") as mock_send,
patch("app.tasks.upload_to_email.attach_logo") as mock_logo,
):
# Setup settings
mock_settings.email_host = "smtp.example.com"
mock_settings.email_port = 587
@@ -406,7 +394,6 @@ def test_upload_to_email_accepts_file_id(sample_text_file):
def test_upload_to_ftp_file_not_found():
"""Test that upload_to_ftp raises error for missing file."""
with patch("app.tasks.upload_to_ftp.settings") as mock_settings, patch("app.tasks.upload_to_ftp.log_task_progress"):
mock_settings.ftp_host = "ftp.example.com"
with pytest.raises(FileNotFoundError):
@@ -420,7 +407,6 @@ def test_upload_to_sftp_file_not_found():
patch("app.tasks.upload_to_sftp.settings") as mock_settings,
patch("app.tasks.upload_to_sftp.log_task_progress"),
):
mock_settings.sftp_host = "sftp.example.com"
mock_settings.sftp_port = 22
mock_settings.sftp_username = "test_user"
@@ -436,7 +422,6 @@ def test_upload_to_webdav_file_not_found():
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
with pytest.raises(FileNotFoundError):
@@ -504,7 +489,6 @@ def test_send_to_all_calls_upload_tasks_with_keyword_argument():
patch("app.tasks.send_to_all.SessionLocal"),
patch("app.tasks.send_to_all.get_configured_services_from_validator") as mock_validator,
):
# Configure validator to return S3 as configured
mock_validator.return_value = {"s3": True}
-1
View File
@@ -1,6 +1,5 @@
"""Additional tests for upload task modules."""
import os
from unittest.mock import MagicMock, patch
import pytest
-3
View File
@@ -1,8 +1,5 @@
"""Tests to increase coverage for upload task modules."""
import os
from unittest.mock import MagicMock, patch
import pytest
from app.tasks.upload_to_ftp import upload_to_ftp
+2 -5
View File
@@ -5,8 +5,7 @@ Covers _validate_dropbox_settings, get_dropbox_access_token, get_dropbox_client,
and upload_to_dropbox Celery task.
"""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
from dropbox.exceptions import ApiError, AuthError
@@ -255,9 +254,7 @@ class TestUploadToDropbox:
@patch("app.tasks.upload_to_dropbox.get_dropbox_client")
@patch("app.tasks.upload_to_dropbox.log_task_progress")
@patch("app.tasks.upload_to_dropbox.settings")
def test_large_file_chunked_upload(
self, mock_settings, mock_log, mock_client, mock_extract, mock_unique, tmp_path
):
def test_large_file_chunked_upload(self, mock_settings, mock_log, mock_client, mock_extract, mock_unique, tmp_path):
"""Test chunked upload for large files (>10MB)."""
from app.tasks.upload_to_dropbox import upload_to_dropbox
+1 -2
View File
@@ -5,8 +5,7 @@ Covers the upload_to_nextcloud Celery task including configuration validation,
WebDAV upload, directory creation, and error handling.
"""
import os
from unittest.mock import MagicMock, Mock, call, patch
from unittest.mock import Mock, patch
import pytest
+1 -2
View File
@@ -5,8 +5,7 @@ Covers get_onedrive_token, create_upload_session, upload_large_file,
and upload_to_onedrive Celery task.
"""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
+4 -11
View File
@@ -6,8 +6,7 @@ get_custom_field_id, set_document_custom_fields) and the upload_to_paperless Cel
"""
import json
import os
from unittest.mock import MagicMock, Mock, mock_open, patch
from unittest.mock import Mock, patch
import pytest
import requests
@@ -152,9 +151,7 @@ class TestPollTaskForDocumentId:
mock_settings.http_request_timeout = 30
mock_response = Mock()
mock_response.json.return_value = {
"results": [{"status": "SUCCESS", "related_document": "99"}]
}
mock_response.json.return_value = {"results": [{"status": "SUCCESS", "related_document": "99"}]}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
@@ -188,9 +185,7 @@ class TestPollTaskForDocumentId:
mock_settings.http_request_timeout = 30
mock_response = Mock()
mock_response.json.return_value = [
{"status": "FAILURE", "result": "Not consuming duplicate document"}
]
mock_response.json.return_value = [{"status": "FAILURE", "result": "Not consuming duplicate document"}]
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
@@ -262,9 +257,7 @@ class TestGetCustomFieldId:
mock_settings.http_request_timeout = 30
mock_response = Mock()
mock_response.json.return_value = {
"results": [{"name": "sender", "id": 5}, {"name": "date", "id": 6}]
}
mock_response.json.return_value = {"results": [{"name": "sender", "id": 5}, {"name": "date", "id": 6}]}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
+2 -23
View File
@@ -1,10 +1,10 @@
"""Comprehensive tests for upload_to_webdav task."""
import os
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
from requests.exceptions import ConnectionError, RequestException, Timeout
from requests.exceptions import ConnectionError, Timeout
from app.tasks.upload_to_webdav import upload_to_webdav
@@ -23,7 +23,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
):
# Setup settings
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
@@ -65,7 +64,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -89,7 +87,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -111,7 +108,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = None
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -125,7 +121,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
with pytest.raises(FileNotFoundError, match="File not found"):
@@ -138,7 +133,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -162,7 +156,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -185,7 +178,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -208,7 +200,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -229,7 +220,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -250,7 +240,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -277,7 +266,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -303,7 +291,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -329,7 +316,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -352,7 +338,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -377,7 +362,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -402,7 +386,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "custom_user"
mock_settings.webdav_password = _TEST_CUSTOM_CREDENTIAL
@@ -427,7 +410,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -455,7 +437,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress") as mock_log,
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -482,7 +463,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
@@ -509,7 +489,6 @@ class TestUploadToWebDAV:
patch("app.tasks.upload_to_webdav.requests.put") as mock_put,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = "https://webdav.example.com/"
mock_settings.webdav_username = "test_user"
mock_settings.webdav_password = _TEST_CREDENTIAL
+3 -11
View File
@@ -7,7 +7,6 @@ actual file uploads against it, then verify the files were uploaded successfully
import os
import time
from pathlib import Path
from unittest.mock import patch
import pytest
@@ -87,7 +86,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
# Configure settings to point to real WebDAV server
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = webdav_server["username"]
@@ -124,7 +122,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
# Create a test folder first
folder_name = "test-uploads"
folder_url = f"{webdav_server['url']}/{folder_name}"
@@ -162,7 +159,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = webdav_server["username"]
mock_settings.webdav_password = webdav_server["password"]
@@ -193,7 +189,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = "wronguser"
mock_settings.webdav_password = _TEST_WRONG_CREDENTIAL
@@ -211,7 +206,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = webdav_server["username"]
mock_settings.webdav_password = webdav_server["password"]
@@ -253,7 +247,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = webdav_server["username"]
mock_settings.webdav_password = webdav_server["password"]
@@ -291,7 +284,6 @@ class TestWebDAVIntegration:
patch("app.tasks.upload_to_webdav.settings") as mock_settings,
patch("app.tasks.upload_to_webdav.log_task_progress"),
):
mock_settings.webdav_url = webdav_server["url"] + "/"
mock_settings.webdav_username = webdav_server["username"]
mock_settings.webdav_password = webdav_server["password"]
@@ -313,9 +305,9 @@ class TestWebDAVIntegration:
response = requests.get(file_url, auth=(webdav_server["username"], webdav_server["password"]), timeout=10)
assert response.status_code == 200
assert (
len(response.content) == 1024 * 1024
), f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
assert len(response.content) == 1024 * 1024, (
f"File size mismatch: expected 1MB, got {len(response.content)} bytes"
)
@pytest.mark.integration
+2 -5
View File
@@ -5,9 +5,8 @@ Extends existing tests with comprehensive coverage for upload_with_rclone
and send_to_all_rclone_destinations Celery tasks.
"""
import os
import subprocess
from unittest.mock import MagicMock, Mock, call, patch
from unittest.mock import Mock, patch
import pytest
@@ -117,9 +116,7 @@ class TestUploadWithRcloneExtended:
rclone_config = tmp_path / "rclone.conf"
rclone_config.write_text("[gdrive]\ntype = drive\n")
mock_run.side_effect = subprocess.CalledProcessError(
1, "rclone", stderr=b"mkdir failed"
)
mock_run.side_effect = subprocess.CalledProcessError(1, "rclone", stderr=b"mkdir failed")
with pytest.raises(RuntimeError, match="Rclone error"):
upload_with_rclone(str(test_file), "gdrive:uploads")
+1 -1
View File
@@ -4,7 +4,7 @@ Tests for app/tasks/uptime_kuma_tasks.py
Tests Uptime Kuma health check ping functionality.
"""
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import Mock, patch
import pytest
import requests
-1
View File
@@ -2,7 +2,6 @@
Tests for URL-based file upload functionality
"""
import os
from unittest.mock import Mock, patch
import pytest
-2
View File
@@ -1,7 +1,5 @@
"""Additional view tests to increase coverage."""
from unittest.mock import MagicMock, patch
import pytest
_TEST_CREDENTIAL = "test" # noqa: S105
+65 -61
View File
@@ -6,8 +6,7 @@ Target: Bring coverage from 8.77% to 70%+
"""
import json
import os
from unittest.mock import Mock, MagicMock, patch
from unittest.mock import Mock, patch
import pytest
from fastapi.testclient import TestClient
@@ -32,14 +31,14 @@ class TestFilesPage:
original_filename="test1.pdf",
local_filename="/tmp/test1.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="test2.pdf",
local_filename="/tmp/test2.pdf",
file_size=2048,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
@@ -57,7 +56,7 @@ class TestFilesPage:
original_filename=f"test{i}.pdf",
local_filename=f"/tmp/test{i}.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -77,14 +76,14 @@ class TestFilesPage:
original_filename="invoice.pdf",
local_filename="/tmp/invoice.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="receipt.pdf",
local_filename="/tmp/receipt.pdf",
file_size=2048,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
@@ -100,14 +99,14 @@ class TestFilesPage:
original_filename="doc.pdf",
local_filename="/tmp/doc.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="image.jpg",
local_filename="/tmp/image.jpg",
file_size=2048,
mime_type="image/jpeg"
mime_type="image/jpeg",
)
db_session.add(file1)
db_session.add(file2)
@@ -123,7 +122,7 @@ class TestFilesPage:
original_filename="test.pdf",
local_filename="/tmp/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -133,8 +132,20 @@ class TestFilesPage:
def test_files_page_sorting_by_filename_asc(self, client: TestClient, db_session):
"""Test sorting by filename ascending."""
file1 = FileRecord(filehash="hash1", original_filename="aaa.pdf", local_filename="/tmp/aaa.pdf", file_size=1024, mime_type="application/pdf")
file2 = FileRecord(filehash="hash2", original_filename="zzz.pdf", local_filename="/tmp/zzz.pdf", file_size=2048, mime_type="application/pdf")
file1 = FileRecord(
filehash="hash1",
original_filename="aaa.pdf",
local_filename="/tmp/aaa.pdf",
file_size=1024,
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="zzz.pdf",
local_filename="/tmp/zzz.pdf",
file_size=2048,
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
db_session.commit()
@@ -144,8 +155,20 @@ class TestFilesPage:
def test_files_page_sorting_by_size_desc(self, client: TestClient, db_session):
"""Test sorting by file size descending."""
file1 = FileRecord(filehash="hash1", original_filename="small.pdf", local_filename="/tmp/small.pdf", file_size=100, mime_type="application/pdf")
file2 = FileRecord(filehash="hash2", original_filename="large.pdf", local_filename="/tmp/large.pdf", file_size=10000, mime_type="application/pdf")
file1 = FileRecord(
filehash="hash1",
original_filename="small.pdf",
local_filename="/tmp/small.pdf",
file_size=100,
mime_type="application/pdf",
)
file2 = FileRecord(
filehash="hash2",
original_filename="large.pdf",
local_filename="/tmp/large.pdf",
file_size=10000,
mime_type="application/pdf",
)
db_session.add(file1)
db_session.add(file2)
db_session.commit()
@@ -177,7 +200,7 @@ class TestFileDetailPage:
local_filename=str(file_path),
original_file_path=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -200,7 +223,7 @@ class TestFileDetailPage:
original_filename="test.pdf",
local_filename=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -211,14 +234,10 @@ class TestFileDetailPage:
task_id="task1",
step_name="create_file_record",
status="success",
message="File record created"
message="File record created",
)
log2 = ProcessingLog(
file_id=file.id,
task_id="task2",
step_name="extract_text",
status="success",
message="Text extracted"
file_id=file.id, task_id="task2", step_name="extract_text", status="success", message="Text extracted"
)
db_session.add(log1)
db_session.add(log2)
@@ -247,7 +266,7 @@ class TestFileDetailPage:
local_filename=str(file_path),
processed_file_path=str(processed_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -264,7 +283,7 @@ class TestFileDetailPage:
local_filename="/nonexistent/local.pdf", # Required field
original_file_path="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -290,19 +309,9 @@ class TestComputeProcessingFlow:
logs = [
Mock(
step_name="create_file_record",
status="success",
message="Created",
timestamp=Mock(),
task_id="task1"
step_name="create_file_record", status="success", message="Created", timestamp=Mock(), task_id="task1"
),
Mock(
step_name="check_text",
status="success",
message="Checked",
timestamp=Mock(),
task_id="task2"
)
Mock(step_name="check_text", status="success", message="Checked", timestamp=Mock(), task_id="task2"),
]
flow = _compute_processing_flow(logs)
@@ -321,7 +330,7 @@ class TestComputeProcessingFlow:
status="success",
message="No duplicates",
timestamp=Mock(),
task_id="task1"
task_id="task1",
)
]
@@ -340,22 +349,18 @@ class TestComputeProcessingFlow:
status="success",
message="Sent",
timestamp=Mock(),
task_id="task1"
task_id="task1",
),
Mock(
step_name="upload_to_dropbox",
status="success",
message="Uploaded",
timestamp=Mock(),
task_id="task2"
step_name="upload_to_dropbox", status="success", message="Uploaded", timestamp=Mock(), task_id="task2"
),
Mock(
step_name="upload_to_google_drive",
status="failure",
message="Failed",
timestamp=Mock(),
task_id="task3"
)
task_id="task3",
),
]
flow = _compute_processing_flow(logs)
@@ -375,7 +380,7 @@ class TestComputeProcessingFlow:
status="failure",
message="Failed to extract",
timestamp=Mock(),
task_id="task1"
task_id="task1",
)
]
@@ -398,7 +403,7 @@ class TestComputeStepSummary:
logs = [
Mock(step_name="create_file_record", status="success", timestamp=Mock()),
Mock(step_name="check_text", status="success", timestamp=Mock()),
Mock(step_name="extract_text", status="success", timestamp=Mock())
Mock(step_name="extract_text", status="success", timestamp=Mock()),
]
summary = _compute_step_summary(logs)
@@ -413,7 +418,7 @@ class TestComputeStepSummary:
logs = [
Mock(step_name="create_file_record", status="success", timestamp=Mock()),
Mock(step_name="upload_to_dropbox", status="success", timestamp=Mock()),
Mock(step_name="upload_to_google_drive", status="failure", timestamp=Mock())
Mock(step_name="upload_to_google_drive", status="failure", timestamp=Mock()),
]
summary = _compute_step_summary(logs)
@@ -424,9 +429,7 @@ class TestComputeStepSummary:
"""Test that 'pending' status is normalized to 'queued'."""
from app.views.files import _compute_step_summary
logs = [
Mock(step_name="create_file_record", status="pending", timestamp=Mock())
]
logs = [Mock(step_name="create_file_record", status="pending", timestamp=Mock())]
summary = _compute_step_summary(logs)
# Should count as queued, not pending
@@ -434,13 +437,14 @@ class TestComputeStepSummary:
def test_compute_step_summary_order_independent(self):
"""Test that summary is order-independent (uses latest timestamp)."""
from app.views.files import _compute_step_summary
from datetime import datetime, timedelta
from app.views.files import _compute_step_summary
now = datetime.now()
logs = [
Mock(step_name="create_file_record", status="queued", timestamp=now),
Mock(step_name="create_file_record", status="success", timestamp=now + timedelta(seconds=10))
Mock(step_name="create_file_record", status="success", timestamp=now + timedelta(seconds=10)),
]
summary = _compute_step_summary(logs)
@@ -464,7 +468,7 @@ class TestPreviewOriginalFile:
local_filename=str(file_path), # Required field
original_file_path=str(file_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -487,7 +491,7 @@ class TestPreviewOriginalFile:
local_filename="/nonexistent/local.pdf", # Required field
original_file_path="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -511,7 +515,7 @@ class TestPreviewProcessedFile:
local_filename=str(processed_path), # Required field
processed_file_path=str(processed_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -533,7 +537,7 @@ class TestPreviewProcessedFile:
local_filename="/nonexistent/local.pdf", # Required field
processed_file_path="/nonexistent/test_processed.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -580,7 +584,7 @@ startxref
local_filename=str(pdf_path), # Required field
original_file_path=str(pdf_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -604,7 +608,7 @@ startxref
local_filename="/nonexistent/local.pdf", # Required field
original_file_path="/nonexistent/test.pdf",
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -650,7 +654,7 @@ startxref
local_filename=str(pdf_path), # local_filename is NOT NULL
processed_file_path=str(pdf_path),
file_size=1024,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
@@ -700,7 +704,7 @@ startxref
local_filename=str(pdf_path), # local_filename is NOT NULL
processed_file_path=str(pdf_path),
file_size=100,
mime_type="application/pdf"
mime_type="application/pdf",
)
db_session.add(file)
db_session.commit()
-3
View File
@@ -1,8 +1,5 @@
"""Tests for app/views/general.py module."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
+1 -1
View File
@@ -1,6 +1,6 @@
"""Tests for app/views/settings.py module."""
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock
import pytest