import asyncio from types import SimpleNamespace import pytest from evals import eval_v1 def test_health_ok(client): r = client.get("/health") assert r.status_code == 200 data = r.json() assert "status" in data assert "active_db" in data def test_chat_missing_question(client): r = client.post("/chat", json={}) assert r.status_code == 400 def test_widget_key_routing_invalid_key_401(client): r = client.post("/chat", headers={"X-Widget-Key": "bad"}, json={"question": "hi", "history": [], "stream": True}) assert r.status_code == 401 def test_widget_key_routing_valid_key_200(app_module, client, two_tenants, monkeypatch): # Avoid any LLM/RAG work: patch the streaming generator to emit a deterministic response. async def _fake_stream(*args, **kwargs): yield 'data: {"type":"chunk","content":"ok"}\n\n' yield 'data: {"type":"metadata","capture_lead":false,"sources":[]}\n\n' yield 'data: {"type":"done"}\n\n' monkeypatch.setattr(app_module, "chat_stream_generator", _fake_stream, raising=True) r = client.post( "/chat", headers={"X-Widget-Key": two_tenants["a"]["wk"]}, json={"question": "hi", "history": [], "stream": True}, ) assert r.status_code == 200, r.text assert "data:" in r.text def test_set_embedding_model_does_not_crash_for_valid_admin(app_module, client, two_tenants): token_resp = client.get("/admin/csrf-token", headers={"Authorization": "Bearer clientA", "X-Admin-DB": "a"}) assert token_resp.status_code == 200, token_resp.text token = token_resp.json()["csrf_token"] r = client.post( "/admin/embedding-model", headers={"X-Admin-DB": "a", "X-CSRF-Token": token}, json={ "password": two_tenants["a"]["pw"], "db_name": "a", "embedding_model": "bge", }, ) assert r.status_code == 200, r.text assert r.json()["embedding_model"] == "bge" def test_shared_key_health_publishes_merges_and_prunes(monkeypatch): import time as _t import services.llm_keys as L class _FakeDict(dict): def __bool__(self): # a real modal.Dict handle is truthy even when empty return True fake = _FakeDict() monkeypatch.setattr(L, "_key_health_handle", fake, raising=False) monkeypatch.setattr(L, "_key_health_last_sync", 0.0, raising=False) L._org_cooldown.clear(); L._key_status.clear() now = _t.time() L._publish_cooldown("org:groq_acct1", now + 3600) L._publish_cooldown("key:deadkey", now + 86400) L._publish_cooldown("org:groq_acct1", now + 10) # lower value must NOT override (max wins) assert abs(fake["org:groq_acct1"] - (now + 3600)) < 1 fake["org:expired"] = now - 10 # must be pruned on sync L._key_health_last_sync = 0.0 L._sync_key_health_from_shared(force=True) assert abs(L._org_cooldown.get("groq_acct1", 0) - (now + 3600)) < 1 assert abs(L._key_status.get("deadkey", {}).get("cooldown_until", 0) - (now + 86400)) < 1 assert "org:expired" not in fake # No-Modal runtime (HF/local): handle False => everything no-ops, no crash. monkeypatch.setattr(L, "_key_health_handle", False, raising=False) L._publish_cooldown("org:x", now + 1) L._sync_key_health_from_shared(force=True) def test_config_invalid_widget_key_401(client, two_tenants): # Supplied-but-invalid key must not silently serve the active tenant's branding. r = client.get("/config", headers={"X-Widget-Key": "totally-bogus"}) assert r.status_code == 401, r.text def test_config_valid_widget_key_200(client, two_tenants): r = client.get("/config", headers={"X-Widget-Key": two_tenants["a"]["wk"]}) assert r.status_code == 200, r.text def test_widget_key_rotation_rejects_stale_cached_key(app_module, client, two_tenants): app_module._widget_key_cache["old-key"] = {"db": "a", "expires_at": ""} app_module._save_db_secrets("a", {"widget_key": "new-key", "widget_key_expires_at": ""}) r = client.get("/config", headers={"X-Widget-Key": "old-key"}) assert r.status_code == 401, r.text assert "old-key" not in app_module._widget_key_cache def test_config_no_widget_key_falls_back_ok(client, two_tenants): # No key at all stays the hosted-demo behaviour (active DB) — must not 401. r = client.get("/config") assert r.status_code == 200, r.text def test_history_invalid_widget_key_401(client, two_tenants): r = client.get("/history/visitor123", headers={"X-Widget-Key": "bogus"}) assert r.status_code == 401, r.text def test_public_write_rate_limited(app_module, client, two_tenants): app_module._public_write_rate.clear() last = None for _ in range(25): last = client.post("/csat", json={"rating": 5, "session_id": "s"}) assert last.status_code == 429, last.text def test_lead_email_html_is_escaped(app_module, monkeypatch): captured = {} async def _capture(subject, html_body, cfg, reply_to=""): captured["subject"] = subject captured["html"] = html_body return True monkeypatch.setattr(app_module, "send_notification_email", _capture, raising=True) asyncio.run(app_module.send_lead_email( {"name": "", "email": "x@y.z", "whatsapp": "", "message": "hi"}, {"contact_email": "o@o.o", "smtp_password": "p"}, )) assert "