# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Self-testing a command that needs a *photo*, not just text. Split out of test_selftest.py at the 250-line cap. This is the half that exists because of !add_photo: Signal never delivers an account its own sends -- measured against the live container on 2026-08-31, and a single-device account gets no sync copy either -- so the only way to put a photo in front of the bot is to name one the container's own attachment store already holds and let the handler fetch it back over the real API. """ from __future__ import annotations import json from typing import TYPE_CHECKING import httpx import pytest from signal_bot.bot import build_bot from signal_bot.liveness import Liveness from signal_bot.selftest import attachment_entry, build_frame, run from tests.conftest import WATCHED_GROUP if TYPE_CHECKING: from signal_bot.bot import Bot from signal_bot.config import Config from signal_bot.context import CommandContext from signal_bot.discovery import GroupDirectory from signal_bot.store import Store from signal_bot.transport_fake import FakeTransport GROUP_ID = "group.internal-id" OTHER = "+48000000000" def _bot(config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory) -> Bot: """The production object graph, which is the point of self-testing at all.""" client = httpx.AsyncClient(transport=httpx.MockTransport(lambda _: httpx.Response(200))) return build_bot(config, client, store, transport, directory, Liveness(config, client)) def _client(entries: list[dict[str, object]]) -> httpx.AsyncClient: def handler(_: httpx.Request) -> httpx.Response: return httpx.Response(200, json=entries) return httpx.AsyncClient(transport=httpx.MockTransport(handler)) def test_a_frame_can_carry_an_attachment() -> None: """What makes !add_photo verifiable without a second human. Signal never delivers an account its own sends -- measured against the live container on 2026-08-31, and a single-device account gets no sync copy either -- so this is the only way to put a photo in front of the bot. """ body = json.loads(build_frame(GROUP_ID, OTHER, "!dodaj alfa", ["abc.jpg"]))["envelope"] attachments = body["dataMessage"]["attachments"] assert attachments == [{"contentType": "image/jpeg", "id": "abc.jpg"}] def test_a_frame_without_attachments_has_no_attachments_key() -> None: """Byte-for-byte what it emitted before, for every existing caller.""" body = json.loads(build_frame(GROUP_ID, OTHER, "!papież"))["envelope"] assert "attachments" not in body["dataMessage"] def test_the_attachment_sits_at_the_top_level_not_in_a_quote() -> None: """quote.attachments is the *quoted* message's media; taking it would let a self-test re-file a photo somebody else posted.""" payload = json.loads(build_frame(GROUP_ID, OTHER, "!dodaj alfa", ["a.jpg"])) message = payload["envelope"]["dataMessage"] assert "attachments" in message assert "quote" not in message @pytest.mark.parametrize( ("attachment_id", "expected"), [ ("photo.jpg", "image/jpeg"), ("clip.mp4", "video/mp4"), ("shot.png", "image/png"), # No suffix to go on, and it does not matter: !add_photo sniffs the # magic bytes and ignores what the envelope claims to be carrying. ("no-suffix", "application/octet-stream"), ], ) def test_the_content_type_is_guessed_from_the_id(attachment_id: str, expected: str) -> None: assert attachment_entry(attachment_id)["contentType"] == expected async def test_run_passes_attachments_through_to_the_dispatcher( config: Config, store: Store, transport: FakeTransport, directory: GroupDirectory ) -> None: """End to end through the real wiring: the id reaches the handler.""" bot = _bot(config, store, transport, directory) entries = [ {"name": "testbot", "internal_id": WATCHED_GROUP, "members": [config.phone_number, OTHER]} ] seen: list[tuple[str, ...]] = [] async def capture(ctx: CommandContext) -> None: seen.append(ctx.message.attachment_ids) bot.dispatcher.registry.register("probe", ["!probe"], capture, "probe", takes_args=True) await run( bot, _client(entries), "testbot", "!probe", env={"SELFTEST_GROUPS": "testbot"}, attachments=["staged.jpg"], ) assert seen == [("staged.jpg",)]