flexigo-support-bot / tests /test_handoff.py
victor34593993's picture
suite green
928c47a verified
Raw
History Blame
6.69 kB
from __future__ import annotations
from sqlalchemy import select
from app.handoff import create_handoff
from app.models import ChatSession, HandoffRequest, Tenant
from app.tools import escalate_tool
from app.tools.registry import ToolContext
AUTH = {"Authorization": "Bearer test-admin-token"}
async def _ctx(db, channel="web"):
t = Tenant(slug="t", brand_name="T")
db.add(t)
await db.flush()
s = ChatSession(shop="x", tenant_id=t.id)
db.add(s)
await db.flush()
return ToolContext(db=db, session=s, tenant_id=t.id, channel=channel), t
async def test_escalate_web_without_email_shows_form(db_session):
ctx, _t = await _ctx(db_session, channel="web")
out = await escalate_tool.run({}, ctx)
assert out["status"] == "show_form"
assert ctx.handoff is True # widget will render the form
# nothing stored yet (the form submit will store it)
n = (await db_session.execute(select(HandoffRequest))).scalars().all()
assert n == []
async def test_escalate_whatsapp_without_email_asks_for_it(db_session):
ctx, _t = await _ctx(db_session, channel="whatsapp")
out = await escalate_tool.run({}, ctx)
assert out["status"] == "need_info"
assert ctx.handoff is False # no form on WhatsApp
async def test_escalate_with_details_stores_request(db_session):
ctx, t = await _ctx(db_session, channel="whatsapp")
out = await escalate_tool.run(
{"email": "cliente@x.com", "name": "Ana", "question": "garantía"}, ctx
)
assert out["status"] == "received"
rows = (await db_session.execute(select(HandoffRequest))).scalars().all()
assert len(rows) == 1
assert rows[0].email == "cliente@x.com"
assert rows[0].name == "Ana"
assert rows[0].message == "garantía"
assert rows[0].channel == "whatsapp"
assert rows[0].tenant_id == t.id
async def test_create_handoff_isolated_per_tenant(db_session):
a = Tenant(slug="a")
b = Tenant(slug="b")
db_session.add_all([a, b])
await db_session.flush()
await create_handoff(db_session, a.id, email="a@x.com", channel="web")
await create_handoff(db_session, b.id, email="b@x.com", channel="web")
a_rows = (await db_session.execute(
select(HandoffRequest).where(HandoffRequest.tenant_id == a.id)
)).scalars().all()
assert len(a_rows) == 1 and a_rows[0].email == "a@x.com"
# --- HTTP: the widget form submit + admin listing -----------------------
async def test_handoff_endpoint_stores_and_admin_lists(app_client, db_session):
_app, client = app_client
await client.post("/admin/tenants", headers=AUTH, json={"slug": "tienda1"})
r = await client.post(
"/handoff?t=tienda1",
json={"name": "Víctor", "email": "v@x.com", "message": "quiero info"},
)
assert r.status_code == 200
assert r.json()["status"] == "received"
listed = await client.get("/admin/tenants/tienda1/handoffs", headers=AUTH)
data = listed.json()
assert len(data) == 1
assert data[0]["email"] == "v@x.com"
assert data[0]["channel"] == "web"
assert data[0]["status"] == "new"
hid = data[0]["id"]
done = await client.post(f"/admin/tenants/tienda1/handoffs/{hid}/done", headers=AUTH)
assert done.status_code == 200
again = (await client.get("/admin/tenants/tienda1/handoffs", headers=AUTH)).json()
assert again[0]["status"] == "done"
async def test_handoff_admin_requires_auth(app_client):
_app, client = app_client
assert (await client.get("/admin/tenants/x/handoffs")).status_code == 401
async def test_handoff_emails_the_tenants_own_inbox(db_session, monkeypatch):
"""The lead is emailed to THAT tenant's support_email (never another's)."""
from app.config import get_settings
a = Tenant(slug="empresa-a", brand_name="A", support_email="a@empresa-a.com")
b = Tenant(slug="empresa-b", brand_name="B", support_email="b@empresa-b.com")
db_session.add_all([a, b])
await db_session.flush()
sent = []
async def fake_send(to, subject, body):
sent.append({"to": to, "subject": subject, "body": body})
return True
s = get_settings()
monkeypatch.setattr(s, "smtp_host", "smtp.test") # pretend SMTP is configured
import app.mailer as mailer
monkeypatch.setattr(mailer, "default_sender", lambda _s: fake_send)
await create_handoff(db_session, a.id, name="Ana", email="lead@x.com", message="hola")
assert len(sent) == 1
assert sent[0]["to"] == "a@empresa-a.com" # NOT b@empresa-b.com
assert "lead@x.com" in sent[0]["body"]
async def test_handoff_uses_brevo_api_when_configured(db_session, monkeypatch):
"""When Brevo is configured, the lead emails the tenant's OWN inbox via the
Brevo HTTP API, with reply-to set to the customer."""
import app.mailer as mailer
from app.config import get_settings
t = Tenant(slug="bz", brand_name="BZ", support_email="dueno@bz.com")
db_session.add(t)
await db_session.flush()
s = get_settings()
monkeypatch.setattr(s, "brevo_api_key", "xkeys-test")
monkeypatch.setattr(s, "brevo_sender", "noreply@flexigobe.com")
monkeypatch.setattr(s, "smtp_host", "")
captured = {}
async def fake_send(self, to, subject, body, *, reply_to=None, sender_name=None, html=None):
captured.update(to=to, reply_to=reply_to, sender=self.sender_email, sender_name=sender_name)
return True
monkeypatch.setattr(mailer.BrevoMailer, "send", fake_send)
await create_handoff(db_session, t.id, email="cliente@x.com", message="hola")
assert captured["to"] == "dueno@bz.com" # tenant's own inbox, not anyone else's
assert captured["reply_to"] == "cliente@x.com" # merchant replies straight to the lead
assert captured["sender"] == "noreply@flexigobe.com"
assert "BZ" in captured["sender_name"] # the From name is the store's own brand
async def test_handoff_no_smtp_still_stores(db_session, monkeypatch):
from sqlalchemy import select
from app.config import get_settings
t = Tenant(slug="nosmtp", support_email="x@x.com")
db_session.add(t)
await db_session.flush()
monkeypatch.setattr(get_settings(), "smtp_host", "") # no SMTP
await create_handoff(db_session, t.id, email="lead@x.com")
rows = (await db_session.execute(select(HandoffRequest))).scalars().all()
assert len(rows) == 1 # stored even without email delivery
def test_widget_js_has_handoff_form():
from pathlib import Path
js = (Path(__file__).resolve().parent.parent / "app" / "static" / "widget.js").read_text()
assert "renderHandoffForm" in js
assert "/handoff" in js
assert "d.handoff" in js # triggered by the response flag