"""Unit tests for the OpenWA Python SDK — assert exact paths and bodies.""" from __future__ import annotations import pytest from openwa import OpenWAClient, OpenWAApiError, OpenWANotFoundError from conftest import MockBackend, make_client # ── Client core ──────────────────────────────────────────────────── class TestClientCore: def test_requires_base_url_and_api_key(self): with pytest.raises(ValueError): OpenWAClient(base_url="", api_key="k") with pytest.raises(ValueError): OpenWAClient(base_url="http://x", api_key="") def test_sends_api_key_header(self): backend = MockBackend().on("GET", "/api/sessions", body=[]) client = make_client(backend) client.sessions.list() assert backend.last_call.headers["x-api-key"] == "owa_k1_test" assert backend.last_call.headers["content-type"] == "application/json" def test_default_headers_cannot_override_api_key(self): # A caller-supplied default header must NEVER clobber the auth/JSON headers. backend = MockBackend().on("GET", "/api/sessions", body=[]) client = OpenWAClient( base_url="http://x", api_key="REAL_KEY", default_headers={"X-API-Key": "EVIL", "Content-Type": "text/plain", "X-Trace": "keep"}, transport=backend.as_transport(), ) client.sessions.list() assert backend.last_call.headers["x-api-key"] == "REAL_KEY" assert backend.last_call.headers["content-type"] == "application/json" assert backend.last_call.headers["x-trace"] == "keep" # benign custom headers still pass through def test_non_json_2xx_body_returns_text(self): import httpx def handler(_req: httpx.Request) -> httpx.Response: return httpx.Response(200, content=b"plain text", headers={"content-type": "text/plain"}) client = OpenWAClient(base_url="http://x", api_key="k", transport=httpx.MockTransport(handler)) # A non-JSON 2xx body must surface as text, not raise a raw JSONDecodeError. assert client.sessions.list() == "plain text" def test_path_segments_are_encoded(self): backend = MockBackend().on("GET", "/history", body=[]) make_client(backend).messages.history("s", "weird/id#x") assert "weird%2Fid%23x" in backend.last_call.url backend2 = MockBackend().on("GET", "/history", body=[]) make_client(backend2).messages.history("s", "a@c.us") assert "/messages/a@c.us/history" in backend2.last_call.url # @ preserved def test_raw_request_escape_hatch(self): backend = MockBackend().on("GET", "/api/anything", body={"ok": True}) result = make_client(backend).request("GET", "/api/anything", query={"a": 1}) assert result == {"ok": True} assert "a=1" in backend.last_call.url def test_treats_unfollowed_redirect_as_error(self): import httpx calls: list[httpx.URL] = [] def handler(req: httpx.Request) -> httpx.Response: calls.append(req.url) return httpx.Response( 302, headers={"location": "http://evil.example/x", "content-type": "application/json"}, content=b'{"redirected": true}', ) client = OpenWAClient(base_url="http://x", api_key="k", transport=httpx.MockTransport(handler)) # A redirect is NOT followed (which would re-send X-API-Key to the target). An unfollowed 3xx # is not a usable response, so it surfaces as an API error — matching the JS transport. with pytest.raises(OpenWAApiError): client.sessions.list() assert len(calls) == 1 # the redirect target was never requested def test_strips_trailing_slash(self): backend = MockBackend().on("GET", "/api/sessions", body=[]) client = OpenWAClient(base_url="http://localhost:2785/", api_key="k", transport=backend.as_transport()) client.sessions.list() assert backend.last_call.url == "http://localhost:2785/api/sessions" def test_query_params_skip_none(self): backend = MockBackend().on("GET", "/messages", body=[]) make_client(backend).messages.list("s1", {"chatId": "a@c.us", "limit": 10}) url = backend.last_call.url assert "chatId=a%40c.us" in url assert "limit=10" in url def test_204_is_none(self): backend = MockBackend().on("DELETE", "/api/sessions", status=204) assert make_client(backend).sessions.delete("x") is None def test_404_maps_to_not_found_error(self): backend = MockBackend().on("GET", "/api/sessions/missing", status=404, body={ "statusCode": 404, "message": "Session not found", "error": "Not Found" }) with pytest.raises(OpenWANotFoundError): make_client(backend).sessions.get("missing") def test_exposes_all_resources(self): client = make_client(MockBackend()) for r in ["sessions", "messages", "contacts", "groups", "webhooks", "chats", "status", "health", "search"]: assert hasattr(client, r) # ── Messages (the critical send-text fix) ────────────────────────── class TestMessages: def test_send_text_uses_send_text_path(self): backend = MockBackend().on("POST", "/send-text", body={"messageId": "m1", "timestamp": 1}) make_client(backend).messages.send_text("s1", {"chatId": "a@c.us", "text": "hi"}) assert backend.last_call.url == "http://localhost:2785/api/sessions/s1/messages/send-text" assert backend.last_call.body == {"chatId": "a@c.us", "text": "hi"} @pytest.mark.parametrize("method,segment", [ ("send_image", "send-image"), ("send_video", "send-video"), ("send_audio", "send-audio"), ("send_document", "send-document"), ("send_sticker", "send-sticker"), ]) def test_media_segments(self, method, segment): backend = MockBackend().on("POST", f"/{segment}", body={"messageId": "m", "timestamp": 2}) fn = getattr(make_client(backend).messages, method) fn("s", {"chatId": "a@c.us", "url": "u"}) assert f"/messages/{segment}" in backend.last_call.url def test_send_location_contact_template(self): backend = MockBackend() backend.on("POST", "/send-location", body={"messageId": "m", "timestamp": 1}) backend.on("POST", "/send-contact", body={"messageId": "m", "timestamp": 1}) backend.on("POST", "/send-template", body={"messageId": "m", "timestamp": 1}) client = make_client(backend) client.messages.send_location("s", {"chatId": "a@c.us", "latitude": -6.2, "longitude": 106.8}) assert "/messages/send-location" in backend.last_call.url client.messages.send_contact("s", {"chatId": "a@c.us", "contactName": "A", "contactNumber": "628"}) assert "/messages/send-contact" in backend.last_call.url # Server DTO field is `vars` (NOT `variables`); body forwarded verbatim. client.messages.send_template("s", {"chatId": "a@c.us", "templateId": "t", "vars": {"name": "Sam"}}) assert "/messages/send-template" in backend.last_call.url assert backend.last_call.body == {"chatId": "a@c.us", "templateId": "t", "vars": {"name": "Sam"}} def test_send_template_accepts_template_name(self): backend = MockBackend().on("POST", "/send-template", body={"messageId": "m", "timestamp": 1}) make_client(backend).messages.send_template("s", {"chatId": "a@c.us", "templateName": "welcome"}) assert backend.last_call.body == {"chatId": "a@c.us", "templateName": "welcome"} def test_list_returns_messages_total_wrapper(self): backend = MockBackend().on("GET", "/messages", body={"messages": [{"id": "1"}], "total": 1}) res = make_client(backend).messages.list("s") assert res["total"] == 1 assert len(res["messages"]) == 1 def test_reply_forwardReactDelete(self): backend = MockBackend() backend.on("POST", "/reply", body={"messageId": "m", "timestamp": 1}) backend.on("POST", "/forward", body={"messageId": "m", "timestamp": 1}) backend.on("POST", "/react", body={"success": True}) backend.on("POST", "/delete", body={"success": True}) client = make_client(backend) client.messages.reply("s", {"chatId": "a@c.us", "quotedMessageId": "q", "text": "r"}) client.messages.forward("s", {"fromChatId": "a@c.us", "toChatId": "b@c.us", "messageId": "m"}) client.messages.react("s", {"chatId": "a@c.us", "messageId": "m", "emoji": "👍"}) client.messages.delete("s", {"chatId": "a@c.us", "messageId": "m"}) assert "/messages/reply" in backend.calls[-4].url assert "/messages/forward" in backend.calls[-3].url assert "/messages/react" in backend.calls[-2].url assert "/messages/delete" in backend.calls[-1].url def test_history_and_reactions_path(self): backend = MockBackend() backend.on("GET", "/history", body=[]) backend.on("GET", "/reactions", body=[]) client = make_client(backend) client.messages.history("s", "a@c.us", {"limit": 5}) assert "/messages/a@c.us/history" in backend.calls[-1].url assert "limit=5" in backend.calls[-1].url client.messages.reactions("s", "a@c.us", "m1") assert "/messages/a@c.us/m1/reactions" in backend.calls[-1].url def test_history_boolean_query_serializes_lowercase(self): # Server reads `includeMedia === 'true'`; Python str(True) == 'True' would be ignored. backend = MockBackend().on("GET", "/history", body=[]) make_client(backend).messages.history("s", "a@c.us", {"limit": 5, "includeMedia": True, "deep": False}) url = backend.last_call.url assert "includeMedia=true" in url assert "deep=false" in url def test_bulk_and_batch_status(self): backend = MockBackend() backend.on("POST", "/send-bulk", body={ "batchId": "b", "status": "queued", "totalMessages": 1, "estimatedCompletionTime": "t", "statusUrl": "/u", }) backend.on("GET", "/batch/b", body={ "batchId": "b", "status": "done", "progress": {"total": 1, "sent": 1, "failed": 0, "pending": 0, "cancelled": 0}, "results": [], "startedAt": "s", "completedAt": "c", }) backend.on("POST", "/cancel", body={ "batchId": "b", "status": "cancelled", "progress": {"total": 1, "sent": 0, "failed": 0, "pending": 0, "cancelled": 1}, }) client = make_client(backend) client.messages.send_bulk("s", {"messages": [{"chatId": "a@c.us", "type": "text", "content": {"text": "x"}}]}) assert "/messages/send-bulk" in backend.calls[-1].url status = client.messages.batch_status("s", "b") assert status["progress"]["sent"] == 1 assert "/messages/batch/b" in backend.calls[-1].url cancelled = client.messages.cancel_batch("s", "b") assert cancelled["status"] == "cancelled" assert "/messages/batch/b/cancel" in backend.calls[-1].url assert backend.calls[-1].method == "POST" # ── Sessions ─────────────────────────────────────────────────────── class TestSessions: def test_lifecycle_paths(self): backend = MockBackend() backend.on("GET", "/api/sessions", body=[]) backend.on("GET", "/sessions/s1", body={"id": "s1", "name": "n", "status": "ready"}) backend.on("POST", "/api/sessions", body={"id": "s1", "name": "n", "status": "created"}) backend.on("DELETE", "/sessions/s1", status=204) backend.on("POST", "/start", body={"id": "s1", "status": "initializing"}) backend.on("POST", "/stop", body={"id": "s1", "status": "disconnected"}) backend.on("POST", "/force-kill", body={"id": "s1", "status": "disconnected"}) client = make_client(backend) client.sessions.list() assert backend.calls[-1].url == "http://localhost:2785/api/sessions" client.sessions.get("s1") assert "/sessions/s1" in backend.calls[-1].url client.sessions.create({"name": "n"}) assert backend.calls[-1].body == {"name": "n"} client.sessions.start("s1") assert "/sessions/s1/start" in backend.calls[-1].url client.sessions.stop("s1") client.sessions.force_kill("s1") assert "/sessions/s1/force-kill" in backend.calls[-1].url client.sessions.delete("s1") assert backend.calls[-1].method == "DELETE" def test_qr_pairing_stats(self): backend = MockBackend() backend.on("GET", "/qr", body={"qrCode": "data:image/png;base64,xxx", "status": "qr_ready"}) backend.on("POST", "/pairing-code", body={"pairingCode": "ABCD1234", "status": "qr_ready"}) backend.on("GET", "/stats/overview", body={"total": 1, "active": 1, "ready": 1, "disconnected": 0, "byStatus": {"ready": 1}}) client = make_client(backend) client.sessions.get_qr_code("s1") assert "/sessions/s1/qr" in backend.calls[-1].url client.sessions.request_pairing_code("s1", {"phoneNumber": "628123456789"}) assert backend.calls[-1].body == {"phoneNumber": "628123456789"} client.sessions.stats() assert "/sessions/stats/overview" in backend.calls[-1].url # ── Groups, Contacts, Webhooks, Chats, Health ────────────────────── class TestGroups: def test_list_get_create(self): backend = MockBackend() backend.on("GET", "/groups", body=[]) backend.on("GET", "/groups/g1@g.us", body={"id": "g1@g.us", "subject": "G", "participants": []}) backend.on("POST", "/groups", body={"id": "g1@g.us", "subject": "G", "participants": []}) client = make_client(backend) client.groups.list("s") client.groups.get("s", "g1@g.us") assert "/groups/g1@g.us" in backend.calls[-1].url client.groups.create("s", {"name": "G", "participants": ["a@c.us"]}) assert backend.calls[-1].body == {"name": "G", "participants": ["a@c.us"]} def test_participant_ops(self): backend = MockBackend() backend.on("POST", "/participants", body={"success": True}) backend.on("DELETE", "/participants", body={"success": True}) backend.on("POST", "/promote", body={"success": True}) backend.on("POST", "/demote", body={"success": True}) client = make_client(backend) client.groups.add_participants("s", "g", ["a@c.us", "b@c.us"]) assert backend.calls[-1].body == {"participants": ["a@c.us", "b@c.us"]} assert backend.calls[-1].method == "POST" client.groups.remove_participants("s", "g", ["a@c.us"]) assert backend.calls[-1].method == "DELETE" client.groups.promote_participants("s", "g", ["a@c.us"]) assert "/promote" in backend.calls[-1].url client.groups.demote_participants("s", "g", ["a@c.us"]) assert "/demote" in backend.calls[-1].url def test_subject_description_invite(self): backend = MockBackend() backend.on("PUT", "/subject", body={"success": True}) backend.on("PUT", "/description", body={"success": True}) backend.on("POST", "/leave", body={"success": True}) backend.on("GET", "/invite-code", body={"inviteCode": "c", "inviteLink": "l"}) backend.on("POST", "/revoke", body={"inviteCode": "c2", "inviteLink": "l2"}) client = make_client(backend) client.groups.set_subject("s", "g", "New") assert backend.calls[-1].body == {"subject": "New"} assert backend.calls[-1].method == "PUT" client.groups.set_description("s", "g", "desc") assert backend.calls[-1].body == {"description": "desc"} client.groups.leave("s", "g") client.groups.invite_code("s", "g") client.groups.revoke_invite_code("s", "g") assert "/revoke" in backend.calls[-1].url class TestContacts: def test_paths(self): backend = MockBackend() backend.on("GET", "/contacts", body=[]) backend.on("GET", "/contacts/a@c.us", body={"id": "a@c.us"}) backend.on("GET", "/check/628123", body={"number": "628123", "exists": True, "whatsappId": "628123@c.us"}) backend.on("GET", "/profile-picture", body={"url": "http://p"}) backend.on("GET", "/phone", body={"contactId": "x@lid", "phone": "628123"}) client = make_client(backend) client.contacts.list("s", {"limit": 10}) assert "limit=10" in backend.calls[-1].url client.contacts.get("s", "a@c.us") client.contacts.check("s", "628123") assert "/check/628123" in backend.calls[-1].url client.contacts.profile_picture("s", "a@c.us") client.contacts.phone("s", "x@lid") def test_block_unblock(self): backend = MockBackend() backend.on("POST", "/block", body={"success": True}) backend.on("DELETE", "/block", body={"success": True}) client = make_client(backend) client.contacts.block("s", "a@c.us") assert backend.calls[-1].method == "POST" client.contacts.unblock("s", "a@c.us") assert backend.calls[-1].method == "DELETE" class TestWebhooks: def test_crud_test(self): wh = {"id": "w1", "sessionId": "s", "url": "u", "events": ["*"], "active": True, "createdAt": "", "updatedAt": ""} backend = MockBackend() backend.on("GET", "/webhooks", body=[wh]) backend.on("GET", "/webhooks/w1", body=wh) backend.on("POST", "/webhooks", body=wh) backend.on("PUT", "/webhooks/w1", body={**wh, "active": False}) backend.on("DELETE", "/webhooks/w1", status=204) backend.on("POST", "/test", body={"success": True}) client = make_client(backend) client.webhooks.list("s") client.webhooks.get("s", "w1") # Server DTO field is `retryCount` (NOT `retries`); body forwarded verbatim. client.webhooks.create("s", {"url": "u", "events": ["*"], "retryCount": 5}) assert backend.calls[-1].body == {"url": "u", "events": ["*"], "retryCount": 5} client.webhooks.update("s", "w1", {"active": False}) assert backend.calls[-1].method == "PUT" client.webhooks.delete("s", "w1") client.webhooks.test("s", "w1") assert "/webhooks/w1/test" in backend.calls[-1].url class TestStatus: def test_send_image_video_forward_nested_media_body(self): backend = MockBackend() backend.on("POST", "/status/send-image", body={"statusId": "s1"}) backend.on("POST", "/status/send-video", body={"statusId": "s2"}) client = make_client(backend) # Server requires a nested {image|video:{...}} body, not flat media fields, # plus a required recipients list. client.status.send_image("s", {"image": {"url": "http://img"}, "recipients": ["a@c.us"], "caption": "hi"}) assert backend.calls[-1].body == {"image": {"url": "http://img"}, "recipients": ["a@c.us"], "caption": "hi"} client.status.send_video("s", {"video": {"url": "http://vid"}, "recipients": ["a@c.us"]}) assert backend.calls[-1].body == {"video": {"url": "http://vid"}, "recipients": ["a@c.us"]} class TestChatsAndHealth: def test_chats(self): backend = MockBackend() backend.on("GET", "/chats", body=[]) backend.on("POST", "/read", body={"success": True}) backend.on("POST", "/unread", body={"success": True}) backend.on("POST", "/delete", body={"success": True}) backend.on("POST", "/typing", body={"success": True}) client = make_client(backend) client.chats.list("s") client.chats.mark_read("s", {"chatId": "a@c.us"}) assert "/chats/read" in backend.calls[-1].url client.chats.mark_unread("s", {"chatId": "a@c.us"}) client.chats.delete("s", {"chatId": "a@c.us"}) client.chats.send_state("s", {"chatId": "a@c.us", "state": "typing"}) assert "/chats/typing" in backend.calls[-1].url def test_health_and_auth(self): backend = MockBackend() backend.on("GET", "/api/health", body={"status": "ok", "version": "0.7.2"}) backend.on("GET", "/live", body={"status": "ok"}) backend.on("GET", "/ready", body={"status": "ok", "details": {}}) backend.on("POST", "/validate", body={"valid": True, "role": "admin"}) client = make_client(backend) client.health.check() assert backend.calls[-1].url == "http://localhost:2785/api/health" client.health.live() client.health.ready() client.auth() assert backend.calls[-1].method == "POST" assert "/auth/validate" in backend.calls[-1].url class TestLabelsChannelsCatalog: def test_labels(self): backend = MockBackend() backend.on("GET", "/labels", body=[{"id": "l1", "name": "VIP"}]) backend.on("GET", "/labels/l1", body={"id": "l1", "name": "VIP"}) backend.on("GET", "/labels/chat/a@c.us", body=[{"id": "l1", "name": "VIP"}]) backend.on("POST", "/labels/chat/a@c.us", body={"success": True}) backend.on("DELETE", "/labels/chat/a@c.us/l1", body={"success": True}) client = make_client(backend) client.labels.list("s") assert "/sessions/s/labels" in backend.calls[-1].url client.labels.get("s", "l1") client.labels.for_chat("s", "a@c.us") client.labels.add_to_chat("s", "a@c.us", {"labelId": "l1"}) assert backend.calls[-1].method == "POST" assert backend.calls[-1].body == {"labelId": "l1"} client.labels.remove_from_chat("s", "a@c.us", "l1") assert backend.calls[-1].method == "DELETE" def test_channels(self): backend = MockBackend() backend.on("GET", "/channels", body=[{"id": "123@newsletter", "name": "News"}]) backend.on("GET", "/channels/123@newsletter", body={"id": "123@newsletter", "name": "News"}) backend.on("GET", "/channels/123@newsletter/messages", body=[]) backend.on("POST", "/channels/subscribe", body={"id": "123@newsletter", "name": "News"}) backend.on("DELETE", "/channels/123@newsletter", body={"success": True}) client = make_client(backend) client.channels.list("s") assert "/sessions/s/channels" in backend.calls[-1].url client.channels.get("s", "123@newsletter") client.channels.messages("s", "123@newsletter", {"limit": 10}) assert "limit=10" in backend.calls[-1].url client.channels.subscribe("s", {"inviteCode": "ABCxyz"}) assert backend.calls[-1].method == "POST" assert backend.calls[-1].body == {"inviteCode": "ABCxyz"} client.channels.unsubscribe("s", "123@newsletter") assert backend.calls[-1].method == "DELETE" def test_catalog(self): backend = MockBackend() backend.on("GET", "/catalog", body={"id": "c1", "name": "My Shop", "productCount": 5, "url": "http://shop"}) backend.on("GET", "/catalog/products", body={ "products": [{"id": "p1", "name": "Widget"}], "pagination": {"page": 1, "limit": 20, "total": 1, "totalPages": 1}, }) backend.on("GET", "/catalog/products/p1", body={"id": "p1", "name": "Widget"}) backend.on("POST", "/messages/send-product", body={"messageId": "m", "timestamp": 1}) backend.on("POST", "/messages/send-catalog", body={"messageId": "m", "timestamp": 1}) client = make_client(backend) client.catalog.info("s") assert "/sessions/s/catalog" in backend.calls[-1].url page = client.catalog.products("s", {"page": 1, "limit": 20}) assert page["pagination"]["total"] == 1 assert "page=1" in backend.calls[-1].url client.catalog.product("s", "p1") client.catalog.send_product("s", {"chatId": "a@c.us", "productId": "p1", "body": "x"}) assert "/messages/send-product" in backend.calls[-1].url assert backend.calls[-1].body == {"chatId": "a@c.us", "productId": "p1", "body": "x"} client.catalog.send_catalog("s", {"chatId": "a@c.us", "body": "cat"}) assert "/messages/send-catalog" in backend.calls[-1].url def test_templates_crud(self): tpl = {"id": "t1", "sessionId": "s", "name": "welcome", "body": "Hi {{name}}", "createdAt": "", "updatedAt": ""} backend = MockBackend() backend.on("GET", "/templates/t1", body=tpl) backend.on("GET", "/templates", body=[tpl]) backend.on("POST", "/templates", body=tpl) backend.on("PUT", "/templates/t1", body={**tpl, "body": "Hello {{name}}"}) backend.on("DELETE", "/templates/t1", status=204) client = make_client(backend) client.templates.list("s") assert "/api/sessions/s/templates" in backend.last_call.url client.templates.get("s", "t1") assert backend.last_call.url.endswith("/api/sessions/s/templates/t1") client.templates.create("s", {"name": "welcome", "body": "Hi {{name}}"}) assert backend.last_call.method == "POST" assert backend.last_call.body == {"name": "welcome", "body": "Hi {{name}}"} client.templates.update("s", "t1", {"body": "Hello {{name}}"}) assert backend.last_call.method == "PUT" assert backend.last_call.body == {"body": "Hello {{name}}"} client.templates.delete("s", "t1") assert backend.last_call.method == "DELETE" def test_client_exposes_all_resources(self): client = make_client(MockBackend()) for r in ["sessions", "messages", "contacts", "groups", "webhooks", "chats", "status", "health", "labels", "channels", "catalog", "templates", "search"]: assert hasattr(client, r) # ── Search ───────────────────────────────────────────────────────── class TestSearch: def test_search_minimal_required_q(self): backend = MockBackend().on("GET", "/api/search", body={ "hits": [], "total": 0, "tookMs": 3, "provider": "builtin-fts", }) res = make_client(backend).search.search({"q": "hello"}) assert res["total"] == 0 assert res["provider"] == "builtin-fts" assert backend.last_call.url == "http://localhost:2785/api/search?q=hello" def test_search_forwards_all_optional_filters(self): backend = MockBackend().on("GET", "/api/search", body={ "hits": [], "total": 0, "tookMs": 1, "provider": "builtin-fts", }) make_client(backend).search.search({ "q": "invoice", "sessionId": "s1", "chatId": "628123@c.us", "direction": "incoming", "type": "chat", "from": "628123456789@c.us", "dateFrom": 1720000000000, "dateTo": 1720100000000, "limit": 20, "offset": 40, }) url = backend.last_call.url assert url.startswith("http://localhost:2785/api/search?") assert "q=invoice" in url assert "sessionId=s1" in url assert "chatId=628123%40c.us" in url # @ percent-encoded in query assert "direction=incoming" in url assert "type=chat" in url assert "from=628123456789%40c.us" in url assert "dateFrom=1720000000000" in url assert "dateTo=1720100000000" in url assert "limit=20" in url assert "offset=40" in url def test_search_returns_hits_shape(self): hit = { "messageId": "m1", "waMessageId": "wam1", "sessionId": "s1", "chatId": "628123@c.us", "body": "please send the invoice", "snippet": "please send the invoice", "timestamp": 1720000000, "type": "chat", "direction": "incoming", "from": "628123456789@c.us", "score": 0.42, } backend = MockBackend().on("GET", "/api/search", body={ "hits": [hit], "total": 1, "tookMs": 7, "provider": "builtin-fts", }) res = make_client(backend).search.search({"q": "invoice"}) assert res["total"] == 1 assert res["hits"][0]["messageId"] == "m1" assert res["hits"][0]["snippet"] == "please send the invoice" assert res["hits"][0]["score"] == 0.42 assert res["hits"][0]["direction"] == "incoming" def test_search_none_values_skipped_in_query(self): # None-typed optional filters must be omitted, never sent as the literal "None". backend = MockBackend().on("GET", "/api/search", body={ "hits": [], "total": 0, "tookMs": 0, "provider": "builtin-fts", }) make_client(backend).search.search({"q": "x", "sessionId": None, "limit": 5}) url = backend.last_call.url assert "q=x" in url assert "limit=5" in url assert "sessionId" not in url