# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """Shared scaffolding for the two !add_photo test modules. A module rather than a conftest fixture, for the reason tests/pack_fixture.py gives: several tests want packs and wirings of different shapes, and saying which one a test needs reads better in the test itself. The stripper is injected everywhere here, so no unit test shells out; scripts/e2e.sh runs the real exiftool against media it generates. """ from __future__ import annotations from dataclasses import dataclass, replace from typing import TYPE_CHECKING import pytest from signal_bot.models import parse_envelope from signal_bot.pack_add import AddResolver, register_add_command from signal_bot.pack_holder import PackHolder from signal_bot.pack_load import load from signal_bot.registry import CommandContext, CommandRegistry from signal_bot.transport_fake import FakeTransport from tests import pack_fixture from tests.conftest import WATCHED_GROUP, envelope if TYPE_CHECKING: from pathlib import Path from signal_bot.config import Config from signal_bot.media_sniff import MediaKind from signal_bot.store import Store TRIGGERS = ("!dodaj", "!add_photo") # Real magic bytes, because the sniffer reads them. Distinct payloads so the # content hashes differ. JPEG_ONE = b"\xff\xd8\xff\xe0one" JPEG_TWO = b"\xff\xd8\xff\xe0two" NOT_MEDIA = b"just a text file" class FakeStripper: """Returns the payload unchanged, or refuses everything.""" def __init__(self, *, accepts: bool = True) -> None: self.accepts = accepts self.seen: list[str] = [] def strip(self, payload: bytes, kind: MediaKind) -> bytes | None: self.seen.append(kind.suffix) return payload if self.accepts else None class UndeletableTransport(FakeTransport): """A container that hands the bytes over but refuses to forget them.""" async def delete_attachment(self, attachment_id: str) -> None: msg = f"the container will not delete {attachment_id}" raise RuntimeError(msg) @pytest.fixture def holder(tmp_path: Path) -> PackHolder: return PackHolder(load(pack_fixture.build(tmp_path / "pack"))) def _registry(holder: PackHolder, stripper: FakeStripper) -> tuple[CommandRegistry, AddResolver]: registry = CommandRegistry() resolver = register_add_command(registry, holder, stripper, TRIGGERS) return registry, resolver @dataclass(frozen=True, slots=True) class Wiring: """The objects a command needs around it, bundled to keep _run readable.""" holder: PackHolder transport: FakeTransport store: Store config: Config async def _run( wiring: Wiring, text: str, *, stripper: FakeStripper | None = None, attachments: tuple[str, ...] = (), group: str = WATCHED_GROUP, ) -> None: holder, transport, store, config = ( wiring.holder, wiring.transport, wiring.store, wiring.config, ) registry, _ = _registry(holder, stripper or FakeStripper()) resolved = registry.resolve_with_args(text, group) if resolved is None: return raw = envelope(text=text, attachments=("image/jpeg",) * len(attachments)) message = parse_envelope(raw) assert message is not None # The envelope builder numbers its ids from 0; point them at the # payloads this test put in the fake's store. message = replace(message, attachment_ids=attachments) await resolved.command.handler( CommandContext( message=message, group_id=group, transport=transport, store=store, config=config, command_name=resolved.command.name, now=1756600000.0, day="2026-08-31", args=resolved.args, ) )