BuyingAgent/backend/tests/e2e/test_discovery_walkthrough.py
Lars b276758ee1
All checks were successful
Deploy Development / deploy (push) Successful in 1m19s
Test Suite / pytest-backend (push) Successful in 31s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-checks (push) Successful in 28s
Stelle den Frontend-MVP und die v0.3-API auf develop bereit, damit Dev API und PWA zusammen deployen.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 13:04:59 +02:00

200 lines
7.7 KiB
Python

import json
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
from app.main import app
from app.persistence.db import SessionLocal, engine
from scripts.bootstrap import run_bootstrap
from scripts.seed_notebook_pilot import seed_notebook_pilot
pytestmark = [pytest.mark.e2e, pytest.mark.integration]
ROOT = Path(__file__).resolve().parents[2]
PROFILE = json.loads((ROOT / "profiles" / "mobile-development-notebook.json").read_text(encoding="utf-8"))
DEFINITIONS = [
{"key": "ram_gb", "value_type": "number", "canonical_unit": "GB", "minimum": 0},
{"key": "storage_gb", "value_type": "number", "canonical_unit": "GB", "minimum": 0},
{"key": "weight_kg", "value_type": "number", "canonical_unit": "kg", "minimum": 0},
{"key": "battery_score", "value_type": "number", "minimum": 0, "maximum": 100},
{"key": "display_score", "value_type": "number", "minimum": 0, "maximum": 100},
{"key": "cpu_score", "value_type": "number", "minimum": 0, "maximum": 100},
]
CRITERIA = {
"items": [
{"id": "ram", "criterion": "ram_gb", "operator": ">=", "value": 24, "mandatory": True},
{"id": "price", "criterion": "price_eur", "operator": "<=", "value": 2000, "mandatory": True},
{"id": "weight", "criterion": "weight_kg", "operator": "<=", "value": 1.6, "mandatory": True},
]
}
@pytest.fixture(scope="module", autouse=True)
def require_postgres() -> None:
try:
with engine.connect() as connection:
connection.execute(text("SELECT 1"))
except Exception as exc:
pytest.skip(f"PostgreSQL not available: {exc}")
session = SessionLocal()
try:
run_bootstrap(session)
seed_notebook_pilot(session)
session.commit()
finally:
session.close()
@pytest.fixture
def client() -> TestClient:
return TestClient(app)
def _ok(response, allowed=(200, 201, 409)) -> None:
assert response.status_code in allowed, response.text
def _seed_profile(client: TestClient) -> None:
for definition in DEFINITIONS:
_ok(client.post("/api/v1/attribute-definitions", json=definition))
_ok(client.post("/api/v1/profiles", json=PROFILE))
def test_notebook_discovery_pilot(client: TestClient) -> None:
_seed_profile(client)
taxonomy = client.post("/api/v1/sources/geizhals/categories/nb/discover-taxonomy")
assert taxonomy.status_code == 201, taxonomy.text
category_id = taxonomy.json()["id"]
filters = client.get("/api/v1/sources/geizhals/categories/nb/filters")
assert filters.status_code == 200
assert {item["external_filter_key"] for item in filters.json()["filters"]} >= {"17023", "price"}
ram = client.post(
"/api/v1/criterion-mappings",
json={
"criterion_key": "ram_gb",
"source_id": "geizhals",
"source_category_id": category_id,
"external_filter_key": "17023",
"source_value": "ab 24GB",
"encoding": {"url_param": "xf"},
},
)
assert ram.status_code == 201, ram.text
assert ram.json()["status"] == "VERIFIED"
price = client.post(
"/api/v1/criterion-mappings",
json={
"criterion_key": "price_eur",
"source_id": "geizhals",
"source_category_id": category_id,
"external_filter_key": "price",
"source_value": None,
"encoding": {"url_param": "maxp"},
"probe_value": 2000,
"operator": "<=",
},
)
assert price.status_code == 201, price.text
assert price.json()["status"] == "VERIFIED"
project = client.post(
"/api/v1/buying-projects",
json={
"name": "Notebook-Pilot",
"profile_id": "mobile-development-notebook",
"profile_version": 1,
"shortlist_size": 5,
"schedule_interval_minutes": 180,
"criteria": CRITERIA,
},
)
assert project.status_code == 201, project.text
project_id = project.json()["id"]
assert project.json()["workspace_id"] is None
assert project.json()["criteria_version"] == 1
available = client.get(f"/api/v1/buying-projects/{project_id}/available-criteria")
assert available.status_code == 200, available.text
assert available.json()["product_category_id"] == "notebook"
scoring_keys = {item.get("scoring_attribute_key") for item in available.json()["criteria"]}
assert {"ram_gb", "price_eur"} <= scoring_keys
ram = next(item for item in available.json()["criteria"] if item.get("scoring_attribute_key") == "ram_gb")
assert ram["mapping_status"] == "VERIFIED"
search = client.post(
"/api/v1/discovery-searches",
json={
"buying_project_id": project_id,
"source_id": "geizhals",
"source_category_id": category_id,
"broad_result_threshold": 300,
},
)
assert search.status_code == 201, search.text
search_id = search.json()["id"]
assert "xf=17023_ab+24GB" in search.json()["generated_url"]
assert "maxp=2000" in search.json()["generated_url"]
assert search.json()["schedule_interval_minutes"] == 180
due = client.get("/api/v1/discovery-searches?due=true")
assert due.status_code == 200
assert any(item["id"] == search_id for item in due.json()["items"])
run = client.post(f"/api/v1/discovery-searches/{search_id}/run")
assert run.status_code == 201, run.text
assert run.json()["status"] == "SUCCESS"
assert run.json()["page_count"] == 2
assert run.json()["result_count"] == 3
assert {item["diff_status"] for item in run.json()["items"]} == {"NEW"}
shortlist = client.get(f"/api/v1/buying-projects/{project_id}/shortlist")
assert shortlist.status_code == 200
assert len(shortlist.json()["items"]) == 1
assert shortlist.json()["items"][0]["product_eligibility"] == "ELIGIBLE"
assert shortlist.json()["items"][0]["status"] == "SHORTLISTED"
candidates = client.get(f"/api/v1/buying-projects/{project_id}/candidates")
assert candidates.status_code == 200
eligibilities = {item["product_eligibility"] for item in candidates.json()["items"]}
assert eligibilities == {"ELIGIBLE", "INELIGIBLE", "UNKNOWN"}
assert all(item.get("deal_score") is None for item in candidates.json()["items"])
updated = client.post(
f"/api/v1/buying-projects/{project_id}/criteria",
json={"items": CRITERIA["items"][:2]},
)
assert updated.status_code == 201
assert updated.json()["criteria_version"] == 2
stale = client.get(f"/api/v1/discovery-searches/{search_id}")
assert stale.json()["status"] == "OUT_OF_SYNC"
blocked_run = client.post(f"/api/v1/discovery-searches/{search_id}/run")
assert blocked_run.status_code == 409
refreshed = client.post(f"/api/v1/discovery-searches/{search_id}/generate-query")
assert refreshed.status_code == 200
assert refreshed.json()["status"] == "ACTIVE"
assert "weight_kg" not in refreshed.json().get("internal_only", [])
assert "weight_kg" not in refreshed.json()["generated_url"]
client.patch(f"/api/v1/discovery-searches/{search_id}", json={"status": "DISABLED"})
broad = client.post(
"/api/v1/discovery-searches",
json={
"buying_project_id": project_id,
"source_id": "geizhals",
"source_category_id": category_id,
"broad_result_threshold": 2,
},
)
assert broad.status_code == 201, broad.text
broad_run = client.post(f"/api/v1/discovery-searches/{broad.json()['id']}/run")
assert broad_run.status_code == 201, broad_run.text
assert broad_run.json()["status"] == "TOO_BROAD"
assert broad_run.json()["result_count"] == 3
stored = client.get(f"/api/v1/discovery-searches/{broad.json()['id']}")
assert stored.json()["status"] == "TOO_BROAD"