mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:27:20 +01:00
Some checks are pending
CI / check (push) Waiting to run
Two things the first live run found, neither of which any test could have.
**exiftool was invisible to the unit.** Arch's perl-image-exiftool installs to
/usr/bin/vendor_perl/exiftool. That is on an interactive shell's PATH, because
perl's profile.d puts it there, and it is *not* on a systemd user unit's, whose
default is "/usr/local/bin:/usr/bin". So shutil.which found it in every test
and in every shell, and the first real !add_photo under the unit answered
"exiftool is not installed; refusing to store .jpg" -- with exiftool plainly
installed. media_strip.locate() now tries PATH and then the known install
locations, and install_service.sh verifies the result through the bot's own
resolver under the unit's PATH rather than through `command -v`, which answers
for the wrong shell. Fixing the lookup rather than setting PATH in the unit:
the latter is a note somebody has to remember on the next machine.
The absent-exiftool test had to blind *both* lookups to keep covering its
branch -- on a machine that has the package the fallback finds it, which is
precisely how the gap survived.
**The self-test could not carry a photo.** Signal never delivers an account
its own sends: measured against the live container, a single-device account
gets no sync copy either, and a sent attachment is not added to the container's
attachment store (only inbound ones are). So !add_photo could not be exercised
the way every other command is. selftest.build_frame and POST /_selftest now
take attachment ids that the container's store already holds, so the handler
fetches them back over the real GET /v1/attachments/{id} -- the ingest path
under test is the live one, not a hand-fed buffer. Still fail-closed on
SELFTEST_GROUPS.
Verified live, against real Signal and the real container, in the testbot
group, on a throwaway album scoped to that group so the private one was
neither read nor written:
- a geotagged JPEG (GPS + Make) went in as "!dodaj alfa wakacjelive"
- the stored file reports zero non-structural tags, and neither the GPS
marker nor the Make string survives in its bytes
- GET /v1/attachments/<id> now 404s and the file is gone from disk, so the
second copy really is deleted
- the typed label appears zero times in the journal
- !losowe wakacjelive, !alfa and an unknown label all answered without error
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqqE22jp94B1ypwxppdr1o
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
# 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.discovery import GroupDirectory
|
|
from signal_bot.registry import CommandContext
|
|
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",)]
|