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