From 43bc58770dbf412648ecaaa204f090afe0612ea2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:10:52 +0000 Subject: [PATCH] refactor: consolidate linting tools into Ruff Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> --- .github/workflows/tests.yaml | 91 +-------- .pre-commit-config.yaml | 34 +--- app/api/files.py | 4 +- app/api/url_upload.py | 2 +- app/config.py | 2 +- app/database.py | 3 +- app/tasks/convert_to_pdf.py | 4 +- app/tasks/embed_metadata_into_pdf.py | 3 +- app/tasks/monitor_stalled_steps.py | 2 +- ...rocess_with_azure_document_intelligence.py | 6 +- app/tasks/rotate_pdf_pages.py | 6 +- app/tasks/upload_to_onedrive.py | 4 +- .../config_validator/settings_display.py | 4 +- app/utils/file_splitting.py | 1 - app/utils/migrate_logs_to_steps.py | 2 +- app/utils/step_timeout.py | 2 +- pyproject.toml | 107 +++-------- requirements-dev.txt | 9 +- tests/conftest.py | 5 +- tests/conftest_oauth.py | 55 +++--- tests/fixtures_integration.py | 1 - tests/mock_oauth_server.py | 38 ++-- tests/test_api_azure_extended.py | 10 +- tests/test_api_diagnostic_extended.py | 7 +- tests/test_api_dropbox.py | 3 +- tests/test_api_dropbox_extended.py | 8 +- tests/test_api_files_comprehensive.py | 146 ++++++++------ tests/test_api_google_drive_comprehensive.py | 154 ++++++--------- tests/test_api_google_drive_extended.py | 7 +- tests/test_api_logs.py | 2 - tests/test_api_onedrive_comprehensive.py | 174 ++++++----------- tests/test_api_onedrive_extended.py | 9 +- tests/test_api_openai_extended.py | 5 +- tests/test_api_settings.py | 2 +- tests/test_api_settings_extended.py | 20 +- tests/test_auth_integration.py | 5 +- tests/test_auth_module.py | 4 +- tests/test_bulk_operations.py | 2 +- tests/test_celery_worker.py | 56 +++--- tests/test_check_credentials.py | 2 +- tests/test_config.py | 2 - tests/test_config_validator_reexport.py | 30 +-- tests/test_config_validators.py | 2 - tests/test_convert_pdf.py | 2 +- tests/test_convert_to_pdf.py | 1 - tests/test_coverage_boost.py | 2 +- tests/test_coverage_final.py | 3 +- tests/test_database.py | 4 +- tests/test_e2e_full_stack.py | 17 -- tests/test_embed_metadata.py | 1 - tests/test_embed_pdf_metadata.py | 9 +- tests/test_endpoint_registration.py | 10 +- tests/test_external_integrations.py | 12 +- tests/test_extract_metadata_gpt.py | 59 +++--- tests/test_file_detail_endpoints.py | 1 - tests/test_file_detail_enhancements.py | 10 - tests/test_file_listing.py | 2 - tests/test_file_splitting.py | 6 +- tests/test_file_status_fix.py | 4 +- tests/test_file_upload.py | 1 - tests/test_filename_utils.py | 4 +- tests/test_finalize_storage.py | 4 +- tests/test_imap_extended.py | 2 - tests/test_imap_tasks.py | 1 - tests/test_monitor_stalled_steps.py | 90 ++++----- tests/test_notification.py | 2 +- tests/test_notification_utils.py | 3 +- tests/test_oauth_integration_flows.py | 102 +++++----- tests/test_ocr_processing.py | 15 -- tests/test_original_filename_preservation.py | 2 - tests/test_path_traversal_security.py | 8 +- tests/test_process_document.py | 6 - tests/test_processall_throttling.py | 3 +- tests/test_rate_limit_decorators.py | 70 +++---- tests/test_rate_limiting.py | 4 - tests/test_rclone_tasks.py | 3 +- tests/test_security_headers.py | 6 +- tests/test_settings.py | 2 - tests/test_step_timeout.py | 112 +++++------ tests/test_storage_reorganization.py | 1 - tests/test_upload_email.py | 2 - tests/test_upload_ftp_additional.py | 2 - tests/test_upload_tasks.py | 18 +- tests/test_upload_tasks_additional.py | 1 - tests/test_upload_tasks_coverage.py | 3 - tests/test_upload_to_dropbox.py | 7 +- tests/test_upload_to_nextcloud.py | 3 +- tests/test_upload_to_onedrive.py | 3 +- tests/test_upload_to_paperless.py | 15 +- tests/test_upload_webdav_comprehensive.py | 25 +-- tests/test_upload_webdav_integration.py | 14 +- tests/test_upload_with_rclone.py | 7 +- tests/test_uptime_kuma.py | 2 +- tests/test_url_upload.py | 1 - tests/test_views_coverage.py | 2 - tests/test_views_files_comprehensive.py | 178 +++++++++--------- tests/test_views_general.py | 3 - tests/test_views_settings.py | 2 +- 98 files changed, 739 insertions(+), 1168 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2211e12d..de7366b9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 035bec2b..1416b15c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/app/api/files.py b/app/api/files.py index 80eb558d..d852e65a 100644 --- a/app/api/files.py +++ b/app/api/files.py @@ -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", diff --git a/app/api/url_upload.py b/app/api/url_upload.py index 7367138b..bfc82dab 100644 --- a/app/api/url_upload.py +++ b/app/api/url_upload.py @@ -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 diff --git a/app/config.py b/app/config.py index 391e2b22..9f6a411f 100644 --- a/app/config.py +++ b/app/config.py @@ -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 diff --git a/app/database.py b/app/database.py index 6451ae21..cd17bcf3 100644 --- a/app/database.py +++ b/app/database.py @@ -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 diff --git a/app/tasks/convert_to_pdf.py b/app/tasks/convert_to_pdf.py index edb43599..29d404ce 100644 --- a/app/tasks/convert_to_pdf.py +++ b/app/tasks/convert_to_pdf.py @@ -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}") diff --git a/app/tasks/embed_metadata_into_pdf.py b/app/tasks/embed_metadata_into_pdf.py index 9b2ce884..82bdb8bb 100644 --- a/app/tasks/embed_metadata_into_pdf.py +++ b/app/tasks/embed_metadata_into_pdf.py @@ -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 diff --git a/app/tasks/monitor_stalled_steps.py b/app/tasks/monitor_stalled_steps.py index 9a20d5d1..5fbc8dba 100644 --- a/app/tasks/monitor_stalled_steps.py +++ b/app/tasks/monitor_stalled_steps.py @@ -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} diff --git a/app/tasks/process_with_azure_document_intelligence.py b/app/tasks/process_with_azure_document_intelligence.py index 3713ac45..792cfd15 100644 --- a/app/tasks/process_with_azure_document_intelligence.py +++ b/app/tasks/process_with_azure_document_intelligence.py @@ -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 diff --git a/app/tasks/rotate_pdf_pages.py b/app/tasks/rotate_pdf_pages.py index a7178b61..73982396 100644 --- a/app/tasks/rotate_pdf_pages.py +++ b/app/tasks/rotate_pdf_pages.py @@ -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( diff --git a/app/tasks/upload_to_onedrive.py b/app/tasks/upload_to_onedrive.py index 80601df2..20ba17e0 100644 --- a/app/tasks/upload_to_onedrive.py +++ b/app/tasks/upload_to_onedrive.py @@ -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)) diff --git a/app/utils/config_validator/settings_display.py b/app/utils/config_validator/settings_display.py index e7771ea1..24b75024 100644 --- a/app/utils/config_validator/settings_display.py +++ b/app/utils/config_validator/settings_display.py @@ -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)) ] ) diff --git a/app/utils/file_splitting.py b/app/utils/file_splitting.py index f4d08d37..71b630de 100644 --- a/app/utils/file_splitting.py +++ b/app/utils/file_splitting.py @@ -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): diff --git a/app/utils/migrate_logs_to_steps.py b/app/utils/migrate_logs_to_steps.py index 14dd40fc..85cd9a9a 100644 --- a/app/utils/migrate_logs_to_steps.py +++ b/app/utils/migrate_logs_to_steps.py @@ -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 diff --git a/app/utils/step_timeout.py b/app/utils/step_timeout.py index fecfea38..35961b44 100644 --- a/app/utils/step_timeout.py +++ b/app/utils/step_timeout.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 5b76bed4..545ed4e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 622252c7..7698c84c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 6c968a1e..3cc01f87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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", diff --git a/tests/conftest_oauth.py b/tests/conftest_oauth.py index 6c7dbd6e..a5ff8f92 100644 --- a/tests/conftest_oauth.py +++ b/tests/conftest_oauth.py @@ -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 diff --git a/tests/fixtures_integration.py b/tests/fixtures_integration.py index 4b911c27..74e6dc84 100644 --- a/tests/fixtures_integration.py +++ b/tests/fixtures_integration.py @@ -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 diff --git a/tests/mock_oauth_server.py b/tests/mock_oauth_server.py index d39410c7..780d165f 100644 --- a/tests/mock_oauth_server.py +++ b/tests/mock_oauth_server.py @@ -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 diff --git a/tests/test_api_azure_extended.py b/tests/test_api_azure_extended.py index 00ed3e9e..cb3e6da8 100644 --- a/tests/test_api_azure_extended.py +++ b/tests/test_api_azure_extended.py @@ -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/"): diff --git a/tests/test_api_diagnostic_extended.py b/tests/test_api_diagnostic_extended.py index 27692b63..2721fb0f 100644 --- a/tests/test_api_diagnostic_extended.py +++ b/tests/test_api_diagnostic_extended.py @@ -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 diff --git a/tests/test_api_dropbox.py b/tests/test_api_dropbox.py index 67bb01f3..d496115d 100644 --- a/tests/test_api_dropbox.py +++ b/tests/test_api_dropbox.py @@ -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 diff --git a/tests/test_api_dropbox_extended.py b/tests/test_api_dropbox_extended.py index 22364142..5c4a7ee8 100644 --- a/tests/test_api_dropbox_extended.py +++ b/tests/test_api_dropbox_extended.py @@ -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() diff --git a/tests/test_api_files_comprehensive.py b/tests/test_api_files_comprehensive.py index 602bbc4f..c6e19a31 100644 --- a/tests/test_api_files_comprehensive.py +++ b/tests/test_api_files_comprehensive.py @@ -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() diff --git a/tests/test_api_google_drive_comprehensive.py b/tests/test_api_google_drive_comprehensive.py index 2e010851..360416a1 100644 --- a/tests/test_api_google_drive_comprehensive.py +++ b/tests/test_api_google_drive_comprehensive.py @@ -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 diff --git a/tests/test_api_google_drive_extended.py b/tests/test_api_google_drive_extended.py index 3b6f87c0..3526b6ed 100644 --- a/tests/test_api_google_drive_extended.py +++ b/tests/test_api_google_drive_extended.py @@ -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 diff --git a/tests/test_api_logs.py b/tests/test_api_logs.py index 3dda8f8d..e08e8cf2 100644 --- a/tests/test_api_logs.py +++ b/tests/test_api_logs.py @@ -1,7 +1,5 @@ """Tests for app/api/logs.py module.""" -from datetime import datetime - import pytest from app.models import FileRecord, ProcessingLog diff --git a/tests/test_api_onedrive_comprehensive.py b/tests/test_api_onedrive_comprehensive.py index 83a9318d..1da7d30d 100644 --- a/tests/test_api_onedrive_comprehensive.py +++ b/tests/test_api_onedrive_comprehensive.py @@ -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): diff --git a/tests/test_api_onedrive_extended.py b/tests/test_api_onedrive_extended.py index e919eb17..3a4085a5 100644 --- a/tests/test_api_onedrive_extended.py +++ b/tests/test_api_onedrive_extended.py @@ -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 diff --git a/tests/test_api_openai_extended.py b/tests/test_api_openai_extended.py index 3b6c3a2f..f08912e0 100644 --- a/tests/test_api_openai_extended.py +++ b/tests/test_api_openai_extended.py @@ -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: diff --git a/tests/test_api_settings.py b/tests/test_api_settings.py index f607e0cd..0dab6b13 100644 --- a/tests/test_api_settings.py +++ b/tests/test_api_settings.py @@ -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 diff --git a/tests/test_api_settings_extended.py b/tests/test_api_settings_extended.py index ace9a88d..7e773dab 100644 --- a/tests/test_api_settings_extended.py +++ b/tests/test_api_settings_extended.py @@ -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" diff --git a/tests/test_auth_integration.py b/tests/test_auth_integration.py index 786d33b1..97925de7 100644 --- a/tests/test_auth_integration.py +++ b/tests/test_auth_integration.py @@ -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 diff --git a/tests/test_auth_module.py b/tests/test_auth_module.py index 3dbcd98d..8c0a7735 100644 --- a/tests/test_auth_module.py +++ b/tests/test_auth_module.py @@ -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() diff --git a/tests/test_bulk_operations.py b/tests/test_bulk_operations.py index 554aab08..153f19c8 100644 --- a/tests/test_bulk_operations.py +++ b/tests/test_bulk_operations.py @@ -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 diff --git a/tests/test_celery_worker.py b/tests/test_celery_worker.py index 531b0175..3b8bff74 100644 --- a/tests/test_celery_worker.py +++ b/tests/test_celery_worker.py @@ -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" diff --git a/tests/test_check_credentials.py b/tests/test_check_credentials.py index aad1bfe4..692f582f 100644 --- a/tests/test_check_credentials.py +++ b/tests/test_check_credentials.py @@ -2,7 +2,7 @@ import json import os -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest diff --git a/tests/test_config.py b/tests/test_config.py index 8efb2102..86701feb 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,8 +2,6 @@ Unit tests for configuration and security validation. """ -import os - import pytest from pydantic import ValidationError diff --git a/tests/test_config_validator_reexport.py b/tests/test_config_validator_reexport.py index 4b96cffb..a485b7d3 100644 --- a/tests/test_config_validator_reexport.py +++ b/tests/test_config_validator_reexport.py @@ -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 diff --git a/tests/test_config_validators.py b/tests/test_config_validators.py index 2a0e5f68..4e029cb5 100644 --- a/tests/test_config_validators.py +++ b/tests/test_config_validators.py @@ -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 ( diff --git a/tests/test_convert_pdf.py b/tests/test_convert_pdf.py index 99aa87bb..84830015 100644 --- a/tests/test_convert_pdf.py +++ b/tests/test_convert_pdf.py @@ -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 diff --git a/tests/test_convert_to_pdf.py b/tests/test_convert_to_pdf.py index 841ada96..87899ba6 100644 --- a/tests/test_convert_to_pdf.py +++ b/tests/test_convert_to_pdf.py @@ -1,6 +1,5 @@ """Tests for app/tasks/convert_to_pdf.py module.""" -import os from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_coverage_boost.py b/tests/test_coverage_boost.py index 07de1db6..2de90f31 100644 --- a/tests/test_coverage_boost.py +++ b/tests/test_coverage_boost.py @@ -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 diff --git a/tests/test_coverage_final.py b/tests/test_coverage_final.py index 187872b6..9e374a9f 100644 --- a/tests/test_coverage_final.py +++ b/tests/test_coverage_final.py @@ -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 diff --git a/tests/test_database.py b/tests/test_database.py index bd91b7c0..2582e087 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -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 diff --git a/tests/test_e2e_full_stack.py b/tests/test_e2e_full_stack.py index a08d5b69..1efa1279 100644 --- a/tests/test_e2e_full_stack.py +++ b/tests/test_e2e_full_stack.py @@ -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"] diff --git a/tests/test_embed_metadata.py b/tests/test_embed_metadata.py index aef902b0..83a45f35 100644 --- a/tests/test_embed_metadata.py +++ b/tests/test_embed_metadata.py @@ -1,7 +1,6 @@ """Tests for app/tasks/embed_metadata_into_pdf.py module.""" import os -from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_embed_pdf_metadata.py b/tests/test_embed_pdf_metadata.py index 69af9833..d38a3bb3 100644 --- a/tests/test_embed_pdf_metadata.py +++ b/tests/test_embed_pdf_metadata.py @@ -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") diff --git a/tests/test_endpoint_registration.py b/tests/test_endpoint_registration.py index 589e7338..3c0476b1 100644 --- a/tests/test_endpoint_registration.py +++ b/tests/test_endpoint_registration.py @@ -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") diff --git a/tests/test_external_integrations.py b/tests/test_external_integrations.py index 30c0166d..5fab2016 100644 --- a/tests/test_external_integrations.py +++ b/tests/test_external_integrations.py @@ -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) diff --git a/tests/test_extract_metadata_gpt.py b/tests/test_extract_metadata_gpt.py index eac6d83d..fbe466d0 100644 --- a/tests/test_extract_metadata_gpt.py +++ b/tests/test_extract_metadata_gpt.py @@ -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" diff --git a/tests/test_file_detail_endpoints.py b/tests/test_file_detail_endpoints.py index 5b1b747b..1f02fb47 100644 --- a/tests/test_file_detail_endpoints.py +++ b/tests/test_file_detail_endpoints.py @@ -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 diff --git a/tests/test_file_detail_enhancements.py b/tests/test_file_detail_enhancements.py index 944f7c27..c6645628 100644 --- a/tests/test_file_detail_enhancements.py +++ b/tests/test_file_detail_enhancements.py @@ -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", diff --git a/tests/test_file_listing.py b/tests/test_file_listing.py index e6deefd1..2cab1921 100644 --- a/tests/test_file_listing.py +++ b/tests/test_file_listing.py @@ -2,8 +2,6 @@ Tests for file listing, pagination, filtering, and detail endpoints. """ -from datetime import datetime - import pytest from fastapi.testclient import TestClient diff --git a/tests/test_file_splitting.py b/tests/test_file_splitting.py index f88460da..9682e6df 100644 --- a/tests/test_file_splitting.py +++ b/tests/test_file_splitting.py @@ -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: diff --git a/tests/test_file_status_fix.py b/tests/test_file_status_fix.py index 8c36e583..8a6122b0 100644 --- a/tests/test_file_status_fix.py +++ b/tests/test_file_status_fix.py @@ -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 diff --git a/tests/test_file_upload.py b/tests/test_file_upload.py index f0b8b3a3..c5970b52 100644 --- a/tests/test_file_upload.py +++ b/tests/test_file_upload.py @@ -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" diff --git a/tests/test_filename_utils.py b/tests/test_filename_utils.py index f7da0d82..9100ec1c 100644 --- a/tests/test_filename_utils.py +++ b/tests/test_filename_utils.py @@ -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 diff --git a/tests/test_finalize_storage.py b/tests/test_finalize_storage.py index d8b8b50d..d7b54f5c 100644 --- a/tests/test_finalize_storage.py +++ b/tests/test_finalize_storage.py @@ -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" diff --git a/tests/test_imap_extended.py b/tests/test_imap_extended.py index 528fb971..a050e1a1 100644 --- a/tests/test_imap_extended.py +++ b/tests/test_imap_extended.py @@ -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, ) diff --git a/tests/test_imap_tasks.py b/tests/test_imap_tasks.py index 059fc353..09fac6e2 100644 --- a/tests/test_imap_tasks.py +++ b/tests/test_imap_tasks.py @@ -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 diff --git a/tests/test_monitor_stalled_steps.py b/tests/test_monitor_stalled_steps.py index fef334d0..fb9222ec 100644 --- a/tests/test_monitor_stalled_steps.py +++ b/tests/test_monitor_stalled_steps.py @@ -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" diff --git a/tests/test_notification.py b/tests/test_notification.py index b6d61157..a828b7a9 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -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 diff --git a/tests/test_notification_utils.py b/tests/test_notification_utils.py index 2e688907..7e2bfe9f 100644 --- a/tests/test_notification_utils.py +++ b/tests/test_notification_utils.py @@ -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, diff --git a/tests/test_oauth_integration_flows.py b/tests/test_oauth_integration_flows.py index 197a29f1..dc4c63be 100644 --- a/tests/test_oauth_integration_flows.py +++ b/tests/test_oauth_integration_flows.py @@ -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 diff --git a/tests/test_ocr_processing.py b/tests/test_ocr_processing.py index fa722036..29d3af20 100644 --- a/tests/test_ocr_processing.py +++ b/tests/test_ocr_processing.py @@ -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() diff --git a/tests/test_original_filename_preservation.py b/tests/test_original_filename_preservation.py index 010ad750..09efeab1 100644 --- a/tests/test_original_filename_preservation.py +++ b/tests/test_original_filename_preservation.py @@ -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 diff --git a/tests/test_path_traversal_security.py b/tests/test_path_traversal_security.py index 49dffc3c..ef56a12f 100644 --- a/tests/test_path_traversal_security.py +++ b/tests/test_path_traversal_security.py @@ -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 diff --git a/tests/test_process_document.py b/tests/test_process_document.py index e0459bb3..e93ed444 100644 --- a/tests/test_process_document.py +++ b/tests/test_process_document.py @@ -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 diff --git a/tests/test_processall_throttling.py b/tests/test_processall_throttling.py index 5b2d65b8..e87c1d5d 100644 --- a/tests/test_processall_throttling.py +++ b/tests/test_processall_throttling.py @@ -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 diff --git a/tests/test_rate_limit_decorators.py b/tests/test_rate_limit_decorators.py index 761ffcfa..eccb0ab4 100644 --- a/tests/test_rate_limit_decorators.py +++ b/tests/test_rate_limit_decorators.py @@ -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) diff --git a/tests/test_rate_limiting.py b/tests/test_rate_limiting.py index 4b3f10ce..78deb612 100644 --- a/tests/test_rate_limiting.py +++ b/tests/test_rate_limiting.py @@ -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 diff --git a/tests/test_rclone_tasks.py b/tests/test_rclone_tasks.py index 799cfe6a..83f120f0 100644 --- a/tests/test_rclone_tasks.py +++ b/tests/test_rclone_tasks.py @@ -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 diff --git a/tests/test_security_headers.py b/tests/test_security_headers.py index 821e0f86..5e4be4e7 100644 --- a/tests/test_security_headers.py +++ b/tests/test_security_headers.py @@ -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 diff --git a/tests/test_settings.py b/tests/test_settings.py index 5f178b81..9de8b8dd 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -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 diff --git a/tests/test_step_timeout.py b/tests/test_step_timeout.py index bc6eaa5b..93341fef 100644 --- a/tests/test_step_timeout.py +++ b/tests/test_step_timeout.py @@ -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) diff --git a/tests/test_storage_reorganization.py b/tests/test_storage_reorganization.py index 0bd94d36..cb6bc7c6 100644 --- a/tests/test_storage_reorganization.py +++ b/tests/test_storage_reorganization.py @@ -10,7 +10,6 @@ import os from unittest.mock import MagicMock, patch import pytest -from sqlalchemy.orm import Session from app.models import FileRecord diff --git a/tests/test_upload_email.py b/tests/test_upload_email.py index 856b04ec..f495fde3 100644 --- a/tests/test_upload_email.py +++ b/tests/test_upload_email.py @@ -1,7 +1,5 @@ """Tests for app/tasks/upload_to_email.py module.""" -from unittest.mock import MagicMock, patch - import pytest diff --git a/tests/test_upload_ftp_additional.py b/tests/test_upload_ftp_additional.py index e0592963..5c8a3f0a 100644 --- a/tests/test_upload_ftp_additional.py +++ b/tests/test_upload_ftp_additional.py @@ -1,7 +1,5 @@ """Additional tests for upload_to_ftp task.""" -from unittest.mock import MagicMock, patch - import pytest diff --git a/tests/test_upload_tasks.py b/tests/test_upload_tasks.py index c0ca775c..4c4c63e3 100644 --- a/tests/test_upload_tasks.py +++ b/tests/test_upload_tasks.py @@ -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} diff --git a/tests/test_upload_tasks_additional.py b/tests/test_upload_tasks_additional.py index f79d2a66..6cbe3294 100644 --- a/tests/test_upload_tasks_additional.py +++ b/tests/test_upload_tasks_additional.py @@ -1,6 +1,5 @@ """Additional tests for upload task modules.""" -import os from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_upload_tasks_coverage.py b/tests/test_upload_tasks_coverage.py index 6f5fe027..2d04fbb9 100644 --- a/tests/test_upload_tasks_coverage.py +++ b/tests/test_upload_tasks_coverage.py @@ -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 diff --git a/tests/test_upload_to_dropbox.py b/tests/test_upload_to_dropbox.py index 58f95963..0533fde8 100644 --- a/tests/test_upload_to_dropbox.py +++ b/tests/test_upload_to_dropbox.py @@ -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 diff --git a/tests/test_upload_to_nextcloud.py b/tests/test_upload_to_nextcloud.py index 21105127..2c1c68c2 100644 --- a/tests/test_upload_to_nextcloud.py +++ b/tests/test_upload_to_nextcloud.py @@ -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 diff --git a/tests/test_upload_to_onedrive.py b/tests/test_upload_to_onedrive.py index d9acb459..79fe941a 100644 --- a/tests/test_upload_to_onedrive.py +++ b/tests/test_upload_to_onedrive.py @@ -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 diff --git a/tests/test_upload_to_paperless.py b/tests/test_upload_to_paperless.py index 4667ff3c..c4042f93 100644 --- a/tests/test_upload_to_paperless.py +++ b/tests/test_upload_to_paperless.py @@ -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 diff --git a/tests/test_upload_webdav_comprehensive.py b/tests/test_upload_webdav_comprehensive.py index 2c30a9c9..a17b9f9f 100644 --- a/tests/test_upload_webdav_comprehensive.py +++ b/tests/test_upload_webdav_comprehensive.py @@ -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 diff --git a/tests/test_upload_webdav_integration.py b/tests/test_upload_webdav_integration.py index 62376f2e..dd7fa627 100644 --- a/tests/test_upload_webdav_integration.py +++ b/tests/test_upload_webdav_integration.py @@ -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 diff --git a/tests/test_upload_with_rclone.py b/tests/test_upload_with_rclone.py index 565545fd..679f44c9 100644 --- a/tests/test_upload_with_rclone.py +++ b/tests/test_upload_with_rclone.py @@ -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") diff --git a/tests/test_uptime_kuma.py b/tests/test_uptime_kuma.py index 1c368e7f..806003fc 100644 --- a/tests/test_uptime_kuma.py +++ b/tests/test_uptime_kuma.py @@ -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 diff --git a/tests/test_url_upload.py b/tests/test_url_upload.py index 2610a4ec..5fdf5400 100644 --- a/tests/test_url_upload.py +++ b/tests/test_url_upload.py @@ -2,7 +2,6 @@ Tests for URL-based file upload functionality """ -import os from unittest.mock import Mock, patch import pytest diff --git a/tests/test_views_coverage.py b/tests/test_views_coverage.py index 34534fc4..f8c10794 100644 --- a/tests/test_views_coverage.py +++ b/tests/test_views_coverage.py @@ -1,7 +1,5 @@ """Additional view tests to increase coverage.""" -from unittest.mock import MagicMock, patch - import pytest _TEST_CREDENTIAL = "test" # noqa: S105 diff --git a/tests/test_views_files_comprehensive.py b/tests/test_views_files_comprehensive.py index bff22e15..832ef1b4 100644 --- a/tests/test_views_files_comprehensive.py +++ b/tests/test_views_files_comprehensive.py @@ -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() diff --git a/tests/test_views_general.py b/tests/test_views_general.py index 7154613c..028d10ab 100644 --- a/tests/test_views_general.py +++ b/tests/test_views_general.py @@ -1,8 +1,5 @@ """Tests for app/views/general.py module.""" -from pathlib import Path -from unittest.mock import MagicMock, patch - import pytest diff --git a/tests/test_views_settings.py b/tests/test_views_settings.py index ad89a42b..8b74a508 100644 --- a/tests/test_views_settings.py +++ b/tests/test_views_settings.py @@ -1,6 +1,6 @@ """Tests for app/views/settings.py module.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest