"""Test API del Memory Gateway: validazione, auth, idempotency, supersede, ricerca.""" import pytest from conftest import auth_headers, make_record # --------------------------------------------------------------------------- # Validazione input # --------------------------------------------------------------------------- def test_project_id_obbligatorio(client): body = make_record() del body["project_id"] r = client.post("/v1/memories", json=body, headers=auth_headers()) assert r.status_code == 422 def test_kind_invalido(client): r = client.post("/v1/memories", json=make_record(kind="boh"), headers=auth_headers()) assert r.status_code == 422 def test_expires_at_invalido(client): r = client.post("/v1/memories", json=make_record(expires_at="non-una-data"), headers=auth_headers()) assert r.status_code == 422 assert "ISO 8601" in r.text def test_expires_at_valido(client): r = client.post("/v1/memories", json=make_record(expires_at="2026-09-01T00:00:00Z"), headers=auth_headers()) assert r.status_code == 200 def test_confidence_invalido(client): r = client.post("/v1/memories", json=make_record(confidence="super"), headers=auth_headers()) assert r.status_code == 422 def test_confidence_default_medium(client): r = client.post("/v1/memories", json=make_record(), headers=auth_headers()) assert r.status_code == 200 mid = r.json()["memory_id"] g = client.get(f"/v1/memories/{mid}", headers=auth_headers()) assert g.json()["confidence"] == "medium" def test_text_troppo_lungo(client): r = client.post("/v1/memories", json=make_record(text="x" * 9000), headers=auth_headers()) assert r.status_code == 422 # --------------------------------------------------------------------------- # Auth e rate limit # --------------------------------------------------------------------------- def test_senza_chiave_422(client): # Header X-API-Key mancante → 422 (header richiesto da FastAPI) r = client.post("/v1/memories", json=make_record()) assert r.status_code == 422 def test_chiave_invalida_401(client): r = client.post("/v1/memories", json=make_record(), headers={"X-API-Key": "sbagliata"}) assert r.status_code == 401 def test_rate_limit_429(client, monkeypatch): import config monkeypatch.setattr(config, "RATE_LIMIT_PER_MIN", 3) for _ in range(3): r = client.post("/v1/memories", json=make_record(), headers=auth_headers()) assert r.status_code == 200 r = client.post("/v1/memories", json=make_record(), headers=auth_headers()) assert r.status_code == 429 # --------------------------------------------------------------------------- # Idempotency # --------------------------------------------------------------------------- def test_idempotency_replay_stessa_risposta(client): h = {**auth_headers(), "Idempotency-Key": "k-1"} r1 = client.post("/v1/memories", json=make_record(), headers=h) r2 = client.post("/v1/memories", json=make_record(), headers=h) assert r1.status_code == 200 and r2.status_code == 200 assert r1.json()["memory_id"] == r2.json()["memory_id"] assert client.fake_qdrant.upsert_calls == 1 def test_idempotency_payload_diverso_409(client): h = {**auth_headers(), "Idempotency-Key": "k-2"} client.post("/v1/memories", json=make_record(), headers=h) r = client.post("/v1/memories", json=make_record(text="diverso"), headers=h) assert r.status_code == 409 def test_idempotency_key_diverse_record_distinti(client): r1 = client.post("/v1/memories", json=make_record(), headers={**auth_headers(), "Idempotency-Key": "k-a"}) r2 = client.post("/v1/memories", json=make_record(), headers={**auth_headers(), "Idempotency-Key": "k-b"}) assert r1.json()["memory_id"] != r2.json()["memory_id"] # --------------------------------------------------------------------------- # Supersede # --------------------------------------------------------------------------- def test_supersede_target_inesistente_404(client): r = client.post( "/v1/memories", json=make_record(supersedes_id="00000000-0000-0000-0000-000000000000"), headers=auth_headers(), ) assert r.status_code == 404 def test_supersede_ok_e_lineage(client): r1 = client.post("/v1/memories", json=make_record(text="fatto falso"), headers=auth_headers()) old_id = r1.json()["memory_id"] r2 = client.post( "/v1/memories", json=make_record(text="fatto corretto", supersedes_id=old_id, supersede_reason="evidenza"), headers=auth_headers(), ) assert r2.status_code == 200 new_id = r2.json()["memory_id"] # il vecchio è marcato superseded_by old = client.get(f"/v1/memories/{old_id}", headers=auth_headers()).json() assert old["superseded_by"] == new_id # la ricerca di default esclude i superseduti s = client.post("/v1/memories:search", json={"query": "fatto", "top_k": 10, "min_score": 0.0}, headers=auth_headers()) ids = [x["memory_id"] for x in s.json()["results"]] assert old_id not in ids # include_superseded li mostra s2 = client.post( "/v1/memories:search", json={"query": "fatto", "top_k": 10, "min_score": 0.0, "include_superseded": True}, headers=auth_headers(), ) ids2 = [x["memory_id"] for x in s2.json()["results"]] assert old_id in ids2 def test_supersede_doppio_409(client): r1 = client.post("/v1/memories", json=make_record(text="falso"), headers=auth_headers()) old_id = r1.json()["memory_id"] client.post("/v1/memories", json=make_record(text="corretto", supersedes_id=old_id), headers=auth_headers()) r = client.post("/v1/memories", json=make_record(text="ancora", supersedes_id=old_id), headers=auth_headers()) assert r.status_code == 409 def test_supersede_root_ri_parenta_figli_attivi(client): # L1 root + figlio L2 r1 = client.post( "/v1/memories", json=make_record(text="root L1", level="L1_ROOT", topic="TEST-TOPIC/ROOT"), headers=auth_headers(), ) root_id = r1.json()["memory_id"] r2 = client.post( "/v1/memories", json=make_record(text="figlio L2", level="L2_SUBTOPIC", topic="TEST-TOPIC/SUB", parent_id=root_id), headers=auth_headers(), ) child_id = r2.json()["memory_id"] # supersede il root r3 = client.post( "/v1/memories", json=make_record( text="root L1 corretto", level="L1_ROOT", topic="TEST-TOPIC/ROOT", supersedes_id=root_id, supersede_reason="aggiornamento", ), headers=auth_headers(), ) assert r3.status_code == 200 new_root_id = r3.json()["memory_id"] assert r3.json()["reparented"] == 1 # il figlio attivo ora punta al nuovo root child = client.get(f"/v1/memories/{child_id}", headers=auth_headers()).json() assert child["parent_id"] == new_root_id # search per parent_id sul nuovo root trova il figlio s = client.post( "/v1/memories:search", json={"query": "*", "parent_id": new_root_id, "top_k": 10, "min_score": 0.0}, headers=auth_headers(), ) ids = [x["memory_id"] for x in s.json()["results"]] assert child_id in ids def test_supersede_root_ri_parenta_solo_figli_attivi(client): # L1 root + figlio L2 + figlio L2 già superseduto (versione attiva C1') r1 = client.post( "/v1/memories", json=make_record(text="root", level="L1_ROOT", topic="T2/ROOT"), headers=auth_headers(), ) root_id = r1.json()["memory_id"] c1 = client.post( "/v1/memories", json=make_record(text="figlio vecchio", level="L2_SUBTOPIC", topic="T2/SUB", parent_id=root_id), headers=auth_headers(), ) c1_id = c1.json()["memory_id"] c1p = client.post( "/v1/memories", json=make_record( text="figlio nuovo", level="L2_SUBTOPIC", topic="T2/SUB", parent_id=root_id, supersedes_id=c1_id, ), headers=auth_headers(), ) c1p_id = c1p.json()["memory_id"] r2 = client.post( "/v1/memories", json=make_record(text="root corretto", level="L1_ROOT", topic="T2/ROOT", supersedes_id=root_id), headers=auth_headers(), ) new_root_id = r2.json()["memory_id"] assert r2.json()["reparented"] == 1 # solo C1' (attivo) # C1' ri-parentato al nuovo root; C1 storico resta ancorato al vecchio assert client.get(f"/v1/memories/{c1p_id}", headers=auth_headers()).json()["parent_id"] == new_root_id assert client.get(f"/v1/memories/{c1_id}", headers=auth_headers()).json()["parent_id"] == root_id # --------------------------------------------------------------------------- # Ricerca e filtri # --------------------------------------------------------------------------- def test_search_filtro_project_id(client): client.post("/v1/memories", json=make_record(text="uno", project_id="proj-a"), headers=auth_headers()) client.post("/v1/memories", json=make_record(text="due", project_id="proj-b"), headers=auth_headers()) s = client.post( "/v1/memories:search", json={"query": "test", "project_id": "proj-a", "top_k": 10, "min_score": 0.0}, headers=auth_headers(), ) results = s.json()["results"] assert len(results) == 1 assert results[0]["project_id"] == "proj-a" def test_search_hybrid_param_accettato(client): client.post("/v1/memories", json=make_record(text="codice XYZ-123"), headers=auth_headers()) s = client.post( "/v1/memories:search", json={"query": "XYZ-123", "top_k": 5, "min_score": 0.0, "hybrid": True}, headers=auth_headers(), ) assert s.status_code == 200 assert "results" in s.json() def test_meta_overview(client): client.post("/v1/memories", json=make_record(project_id="proj-a"), headers=auth_headers()) r = client.get("/v1/meta/overview", headers=auth_headers()) assert r.status_code == 200 data = r.json() assert data["total"] >= 1 assert any(p["project_id"] == "proj-a" for p in data["projects"]) def test_status_pubblico(client): r = client.get("/v1/status") assert r.status_code == 200 assert r.json()["status"] == "ok" def test_metrics_auth(client): # Header mancante → 422; chiave invalida → 401; chiave valida → 200 assert client.get("/v1/metrics").status_code == 422 assert client.get("/v1/metrics", headers={"X-API-Key": "sbagliata"}).status_code == 401 r2 = client.get("/v1/metrics", headers=auth_headers()) assert r2.status_code == 200 assert "requests" in r2.json() # --------------------------------------------------------------------------- # Versione / metadata del codice # --------------------------------------------------------------------------- def test_version_endpoint_pubblico(client): """GET /v1/version è pubblico e espone git_commit e guardrail_version.""" r = client.get("/v1/version") assert r.status_code == 200 data = r.json() assert "git_commit" in data assert "version" in data assert "guardrail_version" in data assert data["guardrail_version"] == "similarity-v1" def test_status_espone_git_commit(client): """/v1/status include version, git_commit e guardrail_version.""" r = client.get("/v1/status") assert r.status_code == 200 data = r.json() assert "git_commit" in data assert "version" in data assert "guardrail_version" in data # --------------------------------------------------------------------------- # Struttura Gerarchica e Relazionale # --------------------------------------------------------------------------- def test_create_and_retrieve_hierarchical_record(client): """Crea un nodo Root L1 e un nodo Figlio L2 con links, parent_id, level, topic.""" # 1. Crea Root L1 r_root = client.post( "/v1/memories", json=make_record( text="Master Topic Alfa Romeo", level="L1_ROOT", topic="ALFA-ROMEO/ROOT", ), headers=auth_headers(), ) assert r_root.status_code == 200 root_id = r_root.json()["memory_id"] # 2. Crea Figlio L2 collegato r_child = client.post( "/v1/memories", json=make_record( text="Scheda Tecnica Bialbero 1.3", parent_id=root_id, level="L2_SUBTOPIC", topic="ALFA-ROMEO/SPECS", links=[{"target_id": root_id, "predicate": "part_of", "weight": 1.0}], ), headers=auth_headers(), ) assert r_child.status_code == 200 child_id = r_child.json()["memory_id"] # 3. Recupera e verifica payload strutturato g = client.get(f"/v1/memories/{child_id}", headers=auth_headers()) assert g.status_code == 200 data = g.json() assert data["parent_id"] == root_id assert data["level"] == "L2_SUBTOPIC" assert data["topic"] == "ALFA-ROMEO/SPECS" assert len(data["links"]) == 1 assert data["links"][0]["target_id"] == root_id def test_search_filters_hierarchical(client): """Filtra per parent_id, level e topic.""" r_root = client.post( "/v1/memories", json=make_record(text="Root doc", level="L1_ROOT", topic="TOPIC/ROOT"), headers=auth_headers(), ) root_id = r_root.json()["memory_id"] client.post( "/v1/memories", json=make_record(text="Child A", parent_id=root_id, level="L2_SUBTOPIC", topic="TOPIC/A"), headers=auth_headers(), ) client.post( "/v1/memories", json=make_record(text="Child B", parent_id=root_id, level="L2_SUBTOPIC", topic="TOPIC/B"), headers=auth_headers(), ) # Cerca solo L1_ROOT s1 = client.post( "/v1/memories:search", json={"query": "doc", "level": "L1_ROOT", "top_k": 5, "min_score": 0.0}, headers=auth_headers(), ) assert s1.status_code == 200 assert len(s1.json()["results"]) == 1 assert s1.json()["results"][0]["level"] == "L1_ROOT" # Cerca per parent_id s2 = client.post( "/v1/memories:search", json={"query": "Child", "parent_id": root_id, "top_k": 5, "min_score": 0.0}, headers=auth_headers(), ) assert s2.status_code == 200 assert len(s2.json()["results"]) == 2 # Cerca per topic specifico s3 = client.post( "/v1/memories:search", json={"query": "Child", "topic": "TOPIC/A", "top_k": 5, "min_score": 0.0}, headers=auth_headers(), ) assert s3.status_code == 200 assert len(s3.json()["results"]) == 1 assert s3.json()["results"][0]["topic"] == "TOPIC/A"