signal-bot/tests/test_pack_load.py
Krzysztof kuhy Rudnicki 80c0fa8e45 fix: take real people, numbers and a member's uuid out of a public repository
This repository is public, and five things in it were not invented.

The worst was mine, today: the tagger's help text used the group's actual
first names and nicknames as its worked example, in a file that is on
GitHub. It uses ala/basia/celina now, like every other example here.

The rest predate this work. DOCS-signal-envelopes.md carried the operator's
own phone number and the bot's, captured from a live envelope; conftest.py
had the same number as its trusted inviter; and test_trust.py pinned a group
member's full Signal uuid as a fixture. Numbers keep their shape so the
documents still show what a real envelope looks like, but no digit of them
belongs to anybody.

Comments that reached for a real first name to illustrate the diacritic case
now use an invented one.

None of this removes them from the history, which is a separate decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017w1HZ3cZ7neL3rFdQ6VMBA
2026-08-30 23:01:26 +02:00

192 lines
7.6 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
"""Everything a photo pack can get wrong, and the sentence it gets back.
Each of these fails startup rather than a command. A manifest that cannot be
honoured is an answer, not an outage: the alternative is a bot that comes up
healthy and answers "no such photo" forever because a file was renamed.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import pytest
from signal_bot.groups import ConfigError
from signal_bot.pack_load import load
from tests import pack_fixture
if TYPE_CHECKING:
from pathlib import Path
from signal_bot.pack import Pack
def _load(root: Path, **kwargs: object) -> Pack:
"""Build a pack of the requested shape and load it."""
return load(pack_fixture.build(root / "pack", **kwargs))
def test_a_pack_loads_its_people_its_combos_and_its_tags(tmp_path: Path) -> None:
pack = _load(tmp_path)
assert [entry.key for entry in pack.entries] == ["p1", "p2", "p3", "c1", "c2"]
assert pack.tags["both.png"] == frozenset({"p1", "p2"})
assert pack.groups
def test_a_missing_manifest_says_which_file(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="cannot be read"):
load(tmp_path / "nowhere")
def test_a_manifest_that_is_not_json_says_so(tmp_path: Path) -> None:
root = pack_fixture.build(tmp_path / "pack")
(root / "people.json").write_text("{oops", encoding="utf-8")
with pytest.raises(ConfigError, match="not valid JSON"):
load(root)
def test_a_manifest_that_is_not_an_object_is_refused(tmp_path: Path) -> None:
root = pack_fixture.build(tmp_path / "pack")
(root / "people.json").write_text("[]", encoding="utf-8")
with pytest.raises(ConfigError, match="must be an object"):
load(root)
def test_a_pack_with_no_groups_is_refused(tmp_path: Path) -> None:
"""Defaulting to "every group" would put private photos in a stranger's chat."""
with pytest.raises(ConfigError, match="non-empty 'groups'"):
_load(tmp_path, groups=[])
def test_a_blank_group_id_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="blank group id"):
_load(tmp_path, groups=[" "])
def test_people_have_to_be_objects(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="must be a list of objects"):
_load(tmp_path, people=["alfa"])
def test_a_pack_with_no_entries_at_all_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="defines no commands"):
_load(tmp_path, people=[], combos=[], tags={})
def test_a_key_that_reads_like_a_name_is_refused(tmp_path: Path) -> None:
"""The key is what gets logged; it exists precisely so a name is not."""
with pytest.raises(ConfigError, match="alphanumeric 'key'"):
_load(tmp_path, people=[{"key": "", "triggers": ["!alfa"]}], combos=[], tags={})
def test_a_non_ascii_key_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="alphanumeric 'key'"):
_load(tmp_path, people=[{"key": "gamma\u0105", "triggers": ["!alfa"]}], combos=[], tags={})
def test_an_entry_needs_at_least_one_trigger(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="non-empty 'triggers'"):
_load(tmp_path, people=[{"key": "p1", "triggers": []}], combos=[], tags={})
def test_a_trigger_without_a_bang_could_never_fire(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match='must start with "!"'):
_load(tmp_path, people=[{"key": "p1", "triggers": ["alfa"]}], combos=[], tags={})
def test_a_trigger_with_a_digit_is_refused(tmp_path: Path) -> None:
"""folding maps 0134578 onto letters, so "!alfa7" is not what you typed."""
with pytest.raises(ConfigError, match="must not contain a digit"):
_load(tmp_path, people=[{"key": "p1", "triggers": ["!alfa7"]}], combos=[], tags={})
def test_a_trigger_that_folds_away_to_nothing_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="folds away to nothing"):
_load(tmp_path, people=[{"key": "p1", "triggers": ["!‍"]}], combos=[], tags={})
def test_a_combo_needs_exactly_one_of_all_of_and_any_of(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="exactly one of"):
_load(
tmp_path,
combos=[{"key": "c1", "triggers": ["!oboje"], "all_of": ["p1"], "any_of": ["p2"]}],
)
def test_a_combo_with_neither_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="exactly one of"):
_load(tmp_path, combos=[{"key": "c1", "triggers": ["!oboje"]}])
def test_a_combo_naming_somebody_who_does_not_exist_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="unknown people"):
_load(tmp_path, combos=[{"key": "c1", "triggers": ["!oboje"], "all_of": ["p9"]}])
def test_a_key_used_twice_is_refused(tmp_path: Path) -> None:
"""Two entries on one key would share a cooldown row and a log line."""
people = [{"key": "p1", "triggers": ["!alfa"]}, {"key": "p1", "triggers": ["!beta"]}]
with pytest.raises(ConfigError, match="twice"):
_load(tmp_path, people=people, combos=[], tags={})
def test_two_entries_folding_onto_one_trigger_are_refused(tmp_path: Path) -> None:
"""The registry would catch it too, but could not say which entries."""
people = [{"key": "p1", "triggers": ["!gamma"]}, {"key": "p2", "triggers": ["!gammą"]}]
with pytest.raises(ConfigError, match="claimed by"):
_load(tmp_path, people=people, combos=[], tags={})
def test_one_entry_may_spell_itself_two_ways_that_fold_the_same(tmp_path: Path) -> None:
"""Which is the point: "!gamma" and "!gammą" are one person, written twice."""
pack = _load(
tmp_path,
people=[{"key": "p1", "triggers": ["!gamma", "!gammą"]}],
combos=[],
tags={},
)
assert len(pack.entries) == 1
def test_an_untagged_pack_loads_and_warns(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
"""Not an error: that is an album waiting for the tagger, not a broken one."""
pack = _load(tmp_path, write_tags=False)
assert pack.tags == {}
assert "tagger" in caplog.text
def test_a_tag_naming_a_file_that_is_gone_is_refused(tmp_path: Path) -> None:
"""A renamed file would otherwise drop out of the album in silence."""
with pytest.raises(ConfigError, match="not a file"):
_load(tmp_path, tags={"alfa.png": ["p1"]}, media={})
def test_a_tag_may_not_reach_outside_the_album(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="not a file"):
_load(tmp_path, tags={"../people.json": ["p1"]}, media={"alfa.png": pack_fixture.PIXEL})
def test_a_tag_on_a_type_signal_is_not_told_about_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match="not one Signal is told about"):
_load(tmp_path, tags={"notes.txt": ["p1"]}, media={"notes.txt": b"hello"})
def test_a_tag_naming_somebody_outside_the_manifest_is_refused(tmp_path: Path) -> None:
with pytest.raises(ConfigError, match=r"names somebody who is not in"):
_load(tmp_path, tags={"alfa.png": ["p9"]})
def test_a_tag_that_is_not_a_list_is_refused(tmp_path: Path) -> None:
root = pack_fixture.build(tmp_path / "pack")
(root / "tags.json").write_text(json.dumps({"alfa.png": "p1"}), encoding="utf-8")
with pytest.raises(ConfigError, match="names somebody who is not in"):
load(root)
def test_an_untagged_file_is_simply_not_in_the_album(tmp_path: Path) -> None:
"""The tagger writes an empty list for "seen, nobody in it"."""
pack = _load(tmp_path, tags={"alfa.png": [], "both.png": ["p1", "p2"]})
assert set(pack.tags) == {"both.png"}