# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Shared fixtures and envelope builders.""" from __future__ import annotations import datetime as dt import json from typing import Any import httpx import pytest from signal_bot.config import Config from signal_bot.discovery import GroupDirectory from signal_bot.store import Store from signal_bot.transport_fake import FakeTransport from signal_bot.trust import InviteTrust # Canonical ``group.`` ids, the spelling /v1/groups reports and the # only one /v2/send accepts. The *_INTERNAL forms are what an envelope # carries: exactly what the canonical form decodes to. WATCHED_GROUP = "group.ZDJGMFkyaGxaQzFuY205MWNDMXBaQzB6TWkxaWVYUmxjeUVoSVNFaElRPT0=" WATCHED_INTERNAL = "d2F0Y2hlZC1ncm91cC1pZC0zMi1ieXRlcyEhISEhIQ==" SECOND_GROUP = "group.YzJWamIyNWtMV2R5YjNWd0xXbGtMVE15TFdKNWRHVnpJU0VoSVNFaA==" SECOND_INTERNAL = "c2Vjb25kLWdyb3VwLWlkLTMyLWJ5dGVzISEhISEh" OTHER_GROUP = "group.YjNSb1pYSXRaM0p2ZFhBdGFXUXRNekl0WW5sMFpYTWhJU0VoSVNFPQ==" # The operator, and somebody who is not. TRUSTED_NUMBER = "+48516501330" STRANGER_NUMBER = "+15555550123" @pytest.fixture def config() -> Config: """The production shape: no allowlist, groups discovered from the account.""" return Config( phone_number="+15550000000", api_base_url="http://localhost:9922", database_path=":memory:", cooldown_seconds=10, ) def group_entry(group_id: str, *, member: bool = True, name: str = "e2e") -> dict[str, Any]: """One /v1/groups row, in both spellings the real API uses.""" return {"id": group_id, "internal_id": group_id, "name": name, "member": member} def listing_client(entries: list[dict[str, Any]]) -> httpx.AsyncClient: """A client whose /v1/groups always answers with these rows.""" return httpx.AsyncClient( transport=httpx.MockTransport(lambda request: httpx.Response(200, json=entries)) ) @pytest.fixture async def directory(config: Config, store: Store) -> GroupDirectory: """A directory that has already discovered both test groups.""" client = listing_client([group_entry(WATCHED_GROUP), group_entry(SECOND_GROUP)]) found = GroupDirectory(config, client, store, InviteTrust(())) await found.refresh() return found @pytest.fixture def store() -> Store: with Store(":memory:") as opened: yield opened @pytest.fixture def transport() -> FakeTransport: return FakeTransport() @pytest.fixture def clock() -> dt.datetime: return dt.datetime(2026, 8, 29, 12, 0, tzinfo=dt.UTC) def envelope( *, text: str | None = "hello", group_id: str | None = WATCHED_GROUP, uuid: str = "uuid-alice", name: str = "Alice", sticker: bool = False, reaction: bool = False, sync: bool = False, ) -> str: """Build one raw websocket frame in either envelope shape.""" payload: dict[str, Any] = {"timestamp": 1719177917717} if text is not None: payload["message"] = text if group_id is not None: payload["groupInfo"] = {"groupId": group_id, "type": "DELIVER"} if sticker: payload["sticker"] = {"packId": "abc", "stickerId": 6} if reaction: payload["reaction"] = {"emoji": "👍", "targetAuthor": "x"} body: dict[str, Any] = { "source": name, "sourceNumber": STRANGER_NUMBER, "sourceUuid": uuid, "sourceName": name, "sourceDevice": 69, "timestamp": 1719177917717, } if sync: body["syncMessage"] = {"sentMessage": payload} else: body["dataMessage"] = payload return json.dumps({"envelope": body, "account": "+15550000000"}) def update_envelope( *, group_id: str = WATCHED_GROUP, number: str = TRUSTED_NUMBER, uuid: str = "uuid-operator", ) -> str: """Build a group-membership-change frame: no text, groupInfo type UPDATE. This is the shape signal-cli emits when somebody adds the bot to a group, and the only frame that names who did it. """ return json.dumps( { "envelope": { "source": number, "sourceNumber": number, "sourceUuid": uuid, "sourceName": "kuhy", "timestamp": 1719177917717, "dataMessage": { "timestamp": 1719177917717, "message": None, "groupInfo": {"groupId": group_id, "type": "UPDATE"}, }, }, "account": "+15550000000", } )