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