178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
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
|
|
|
|
|