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 "