import os import sys from pathlib import Path import pytest from fastapi.testclient import TestClient # Ensure required env vars for app import/config os.environ.setdefault("SECRET_KEY", "x" * 32) os.environ.setdefault("DATABASE_URL", "sqlite:////tmp/delphi_test.sqlite") # Ensure repository root on sys.path for direct test runs ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from app.main import app # noqa: E402 from app.auth.security import get_current_user # noqa: E402 from tests.helpers import assert_validation_error # noqa: E402 from app.config import settings # noqa: E402 @pytest.fixture(scope="module") def client(): # Override auth to bypass JWT for these tests class _User: def __init__(self): self.id = "test" self.username = "tester" self.is_admin = True self.is_active = True app.dependency_overrides[get_current_user] = lambda: _User() # Disable cache to make validation tests deterministic settings.cache_enabled = False try: yield TestClient(app) finally: app.dependency_overrides.pop(get_current_user, None) def test_advanced_search_invalid_search_types(client: TestClient): payload = { "query": "anything", "search_types": ["customer", "bogus"], } resp = client.post("/api/search/advanced", json=payload) assert_validation_error(resp, "search_types") def test_advanced_search_invalid_sort_options(client: TestClient): # Invalid sort_by payload = { "query": "x", "search_types": ["customer"], "sort_by": "nope", } resp = client.post("/api/search/advanced", json=payload) assert_validation_error(resp, "sort_by") # Invalid sort_order payload = { "query": "x", "search_types": ["customer"], "sort_order": "sideways", } resp = client.post("/api/search/advanced", json=payload) assert_validation_error(resp, "sort_order") def test_advanced_search_limit_bounds(client: TestClient): # Too low payload = { "query": "x", "search_types": ["customer"], "limit": 0, } resp = client.post("/api/search/advanced", json=payload) assert_validation_error(resp, "limit") # Too high payload["limit"] = 201 resp = client.post("/api/search/advanced", json=payload) assert_validation_error(resp, "limit") def test_advanced_search_conflicting_flags_exact_phrase_and_whole_words(client: TestClient): payload = { "query": "apple pie", "search_types": ["file"], "exact_phrase": True, "whole_words": True, } resp = client.post("/api/search/advanced", json=payload) # Cannot rely on field location for model-level validation, check message text in details assert resp.status_code == 422 body = resp.json() assert body.get("success") is False assert body.get("error", {}).get("code") == "validation_error" msgs = [d.get("msg", "") for d in body.get("error", {}).get("details", [])] assert any("exact_phrase and whole_words" in m for m in msgs) def test_advanced_search_inverted_date_range(client: TestClient): payload = { "search_types": ["file"], "date_field": "created", "date_from": "2024-02-01", "date_to": "2024-01-31", } resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 422 body = resp.json() assert body.get("success") is False assert body.get("error", {}).get("code") == "validation_error" msgs = [d.get("msg", "") for d in body.get("error", {}).get("details", [])] assert any("date_from must be less than or equal to date_to" in m for m in msgs) def test_advanced_search_inverted_amount_range(client: TestClient): payload = { "search_types": ["file"], "amount_field": "amount", "amount_min": 100.0, "amount_max": 50.0, } resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 422 body = resp.json() assert body.get("success") is False assert body.get("error", {}).get("code") == "validation_error" msgs = [d.get("msg", "") for d in body.get("error", {}).get("details", [])] assert any("amount_min must be less than or equal to amount_max" in m for m in msgs) def test_advanced_search_date_field_supported_per_type(client: TestClient): # 'opened' is only valid for files payload = { "search_types": ["customer", "ledger"], "date_field": "opened", "date_from": "2024-01-01", "date_to": "2024-12-31", } resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 422 body = resp.json() msgs = [d.get("msg", "") for d in body.get("error", {}).get("details", [])] assert any("date_field 'opened' is not supported" in m for m in msgs) # Valid when 'file' included payload["search_types"] = ["file"] resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 200 def test_advanced_search_amount_field_supported_per_type(client: TestClient): # 'amount' is only valid for ledger payload = { "search_types": ["file"], "amount_field": "amount", "amount_min": 1, "amount_max": 10, } resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 422 body = resp.json() msgs = [d.get("msg", "") for d in body.get("error", {}).get("details", [])] assert any("amount_field 'amount' is not supported" in m for m in msgs) # Valid when 'ledger' included payload["search_types"] = ["ledger"] resp = client.post("/api/search/advanced", json=payload) assert resp.status_code == 200