""" End-to-end API tests using FastAPI TestClient. Tests the full flow: context push → tick → reply. """ import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapi.testclient import TestClient from app.main import app client = TestClient(app) def test_healthz(): r = client.get("/v1/healthz") assert r.status_code == 200 data = r.json() assert data["status"] == "ok" assert "contexts_loaded" in data print("PASS: healthz") def test_metadata(): r = client.get("/v1/metadata") assert r.status_code == 200 data = r.json() assert "team_name" in data assert "version" in data print("PASS: metadata") def test_context_push(): r = client.post("/v1/context", json={ "scope": "category", "context_id": "dentists", "version": 1, "payload": {"slug": "dentists", "voice": {"tone": "peer_clinical", "vocab_taboo": ["cure"]}, "peer_stats": {"avg_ctr": 0.03}, "digest": [], "offer_catalog": []}, }) assert r.status_code == 200 assert r.json()["accepted"] is True print("PASS: context push (category)") def test_context_stale_version(): # Push v2, then try v1 client.post("/v1/context", json={ "scope": "merchant", "context_id": "m_test", "version": 2, "payload": {"merchant_id": "m_test", "category_slug": "dentists", "identity": {"name": "Test", "owner_first_name": "T", "languages": ["en"]}, "offers": [], "signals": [], "performance": {}, "customer_aggregate": {}}, }) r = client.post("/v1/context", json={ "scope": "merchant", "context_id": "m_test", "version": 1, "payload": {}, }) assert r.json()["accepted"] is False assert r.json()["reason"] == "stale_version" print("PASS: stale version rejected") def test_context_invalid_scope(): r = client.post("/v1/context", json={ "scope": "invalid", "context_id": "x", "version": 1, "payload": {}, }) assert r.json()["accepted"] is False assert r.json()["reason"] == "invalid_scope" print("PASS: invalid scope rejected") def test_tick_empty(): r = client.post("/v1/tick", json={"now": "2026-04-26T10:00:00Z"}) assert r.status_code == 200 assert "actions" in r.json() print("PASS: empty tick") def test_full_flow(): """Push category + merchant + trigger, then tick, verify action.""" # Teardown first client.post("/v1/teardown") # Push category client.post("/v1/context", json={ "scope": "category", "context_id": "dentists", "version": 1, "payload": {"slug": "dentists", "voice": {"tone": "peer_clinical", "vocab_taboo": []}, "peer_stats": {"avg_ctr": 0.03}, "digest": [{"id": "d1", "title": "Test research", "source": "Test J", "trial_n": 50}], "offer_catalog": []}, }) # Push merchant client.post("/v1/context", json={ "scope": "merchant", "context_id": "m_001", "version": 1, "payload": {"merchant_id": "m_001", "category_slug": "dentists", "identity": {"name": "Test Clinic", "owner_first_name": "Doc", "languages": ["en"], "locality": "Test"}, "offers": [{"title": "Cleaning @ ₹299", "status": "active"}], "signals": [], "performance": {"calls": 10, "directions": 20}, "customer_aggregate": {"total_unique_ytd": 200}}, }) # Push trigger client.post("/v1/context", json={ "scope": "trigger", "context_id": "trg_001", "version": 1, "payload": {"id": "trg_001", "scope": "merchant", "kind": "research_digest", "source": "external", "merchant_id": "m_001", "payload": {"top_item_id": "d1"}, "urgency": 2, "suppression_key": "test:flow:1", "expires_at": "2030-01-01T00:00:00Z"}, }) # Tick r = client.post("/v1/tick", json={ "now": "2026-04-26T10:00:00Z", "available_triggers": ["trg_001"], }) data = r.json() assert len(data["actions"]) >= 1, f"Expected actions, got: {data}" action = data["actions"][0] assert action["body"] assert action["merchant_id"] == "m_001" print(f"PASS: full flow — body: {action['body'][:60]}...") # Reply r = client.post("/v1/reply", json={ "conversation_id": action["conversation_id"], "merchant_id": "m_001", "from_role": "merchant", "message": "Yes, send me the details", }) reply_data = r.json() assert reply_data["action"] == "send" print(f"PASS: reply -- action={reply_data.get('action', '')}") def test_teardown(): r = client.post("/v1/teardown") assert r.status_code == 200 assert r.json()["wiped"] is True # Verify contexts cleared h = client.get("/v1/healthz").json() total = sum(h["contexts_loaded"].values()) assert total == 0 print("PASS: teardown") def test_root(): r = client.get("/") assert r.status_code == 200 assert "Vera Bot" in r.json()["service"] print("PASS: root endpoint") if __name__ == "__main__": test_root() test_healthz() test_metadata() test_context_push() test_context_stale_version() test_context_invalid_scope() test_tick_empty() test_full_flow() test_teardown() print("\nAll endpoint tests passed!")