| """End-to-end onboarding proof: a brand-new client, REAL extraction + REAL |
| embeddings (no index mock), answering only from ITS OWN uploaded document, |
| fully isolated from another client. This is the "can I onboard a real store |
| and trust it" guarantee. |
| |
| Marked slow because it loads the embedding model. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import shutil |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from app.models import ChatSession |
| from app.tenancy import get_tenant_by_slug |
| from app.tools import knowledge_tool |
| from app.tools.registry import ToolContext |
|
|
| AUTH = {"Authorization": "Bearer test-admin-token"} |
|
|
| |
| CLIENT_A = { |
| "slug": "mochilas-trekmax", |
| "brand": "Mochilas TrekMax", |
| "file": ("garantia.md", b"La garantia de la mochila TrekMax Summit es de siete anos. " |
| b"El material es Cordura 1000D totalmente impermeable.", "text/markdown"), |
| "ask": "cuanto dura la garantia de la mochila summit y de que material es", |
| "must_have": "TrekMax", |
| "must_not_have": "GotaFina", |
| } |
| CLIENT_B = { |
| "slug": "riego-gotafina", |
| "brand": "Riego GotaFina", |
| "file": ("kit.md", b"El kit de riego GotaFina incluye 30 metros de manguera de " |
| b"poliuretano y boquillas de laton macizo.", "text/markdown"), |
| "ask": "que incluye el kit de riego y de que son las boquillas", |
| "must_not_have": "TrekMax", |
| "must_have": "GotaFina", |
| } |
|
|
|
|
| async def _onboard(client, db, c): |
| |
| created = await client.post( |
| "/admin/tenants", headers=AUTH, |
| json={"slug": c["slug"], "brand_name": c["brand"]}, |
| ) |
| assert created.status_code == 201, created.text |
| assert created.json()["brand_name"] == c["brand"] |
| assert created.json()["has_shopify_secret"] is False |
|
|
| |
| upd = await client.put( |
| f"/admin/tenants/{c['slug']}", headers=AUTH, |
| json={"shopify_shop": f"{c['slug']}.myshopify.com", |
| "shopify_client_id": "id123", |
| "shopify_client_secret": "shpss_super_secret"}, |
| ) |
| assert upd.status_code == 200 |
| assert upd.json()["has_shopify_secret"] is True |
|
|
| |
| up = await client.post( |
| f"/admin/tenants/{c['slug']}/sources/file", headers=AUTH, |
| files={"file": c["file"]}, |
| ) |
| assert up.status_code == 201, up.text |
| assert up.json()["status"] == "indexed", up.json() |
| assert up.json().get("error") in (None, "") |
|
|
| tenant = await get_tenant_by_slug(db, c["slug"]) |
| return tenant.id |
|
|
|
|
| @pytest.mark.slow |
| async def test_full_client_onboarding_and_isolation(app_client, db_session): |
| _app, client = app_client |
| try: |
| a_id = await _onboard(client, db_session, CLIENT_A) |
| b_id = await _onboard(client, db_session, CLIENT_B) |
|
|
| async def ask(tenant_id, question): |
| ctx = ToolContext( |
| db=db_session, |
| session=ChatSession(shop="x", tenant_id=tenant_id), |
| tenant_id=tenant_id, |
| ) |
| return (await knowledge_tool.run({"query": question}, ctx))["context"] |
|
|
| |
| a_ctx = await ask(a_id, CLIENT_A["ask"]) |
| assert CLIENT_A["must_have"] in a_ctx, a_ctx |
| assert CLIENT_A["must_not_have"] not in a_ctx, f"LEAK from other tenant: {a_ctx}" |
|
|
| |
| b_ctx = await ask(b_id, CLIENT_B["ask"]) |
| assert CLIENT_B["must_have"] in b_ctx, b_ctx |
| assert CLIENT_B["must_not_have"] not in b_ctx, f"LEAK from other tenant: {b_ctx}" |
| finally: |
| shutil.rmtree(Path("uploads") / CLIENT_A["slug"], ignore_errors=True) |
| shutil.rmtree(Path("uploads") / CLIENT_B["slug"], ignore_errors=True) |
|
|