mirror of
https://github.com/kuhyx/signal-bot.git
synced 2026-11-10 19:07:19 +01:00
A photo sent to the chat no longer enters the album by itself. pack_add strips it, checks it against the album and the queue, and writes it to pending/ with a sidecar naming the people and labels the sender proposed; the reply is "czeka na zatwierdzenie". The management page lists the queue above the tagger, with the proposal pre-ticked, and Approve moves the file in, mints the labels, records approved_at and asks the bot to rebuild its table through /_reload -- the same publish path every other data change takes, so PackHolder.reload is gone. A reload the bot refuses is reported on the page, not rolled back: the photo is in the album either way. The personal-data gate now reads pending/*.json too, so a label that is still waiting for approval is forbidden in the tree already. The e2e run boots the management page beside the bot with the same environment, proves the queue is stripped and never reaches media/, approves it through the real routes, and has !losowe answer with the photo afterwards without a restart; exiftool is found through media_strip.locate() there rather than PATH, which lacks vendor_perl on Arch. !gadaj is dropped from !odcisz: it was within a typo of !dodaj, and the nearest-command suggestion must never be a coin toss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
139 lines
5.1 KiB
Python
139 lines
5.1 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence.
|
|
"""Where the personal-data gate learns its forbidden words from.
|
|
|
|
Worth its own test because ``!add_photo`` made one of those sources
|
|
*writable from a group chat*, and the failure mode is silent and permanent:
|
|
a label that is also a word in this repository would put that word on the
|
|
forbidden list and refuse every commit from then on, fixable only by
|
|
hand-editing an album the committer cannot see.
|
|
|
|
Driven entirely through ``forbidden()``, which is what the gate itself calls.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from types import ModuleType
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
@pytest.fixture
|
|
def sources() -> ModuleType:
|
|
"""Import scripts/personal_data_sources.py, which is not a package."""
|
|
path = REPO / "scripts" / "personal_data_sources.py"
|
|
spec = importlib.util.spec_from_file_location("personal_data_sources", path)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def _album(
|
|
root: Path,
|
|
*,
|
|
triggers: list[str] | None = None,
|
|
custom: dict[str, Any] | None = None,
|
|
corrupt_custom: bool = False,
|
|
) -> Path:
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
people = [{"key": "p1", "triggers": triggers or ["!zenon"]}]
|
|
(root / "people.json").write_text(
|
|
json.dumps({"groups": ["g"], "people": people}), encoding="utf-8"
|
|
)
|
|
if corrupt_custom:
|
|
(root / "custom.json").write_text("{ not json", encoding="utf-8")
|
|
elif custom is not None:
|
|
(root / "custom.json").write_text(json.dumps(custom), encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def _words(sources: ModuleType, root: Path, monkeypatch: pytest.MonkeyPatch) -> set[str]:
|
|
monkeypatch.setenv("PHOTO_PACK_DIR", str(root))
|
|
words, _ = sources.forbidden(set())
|
|
return set(words)
|
|
|
|
|
|
def test_a_people_trigger_is_forbidden(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
assert "zenon" in _words(sources, _album(tmp_path / "pack"), monkeypatch)
|
|
|
|
|
|
def test_a_custom_label_is_forbidden_too(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A label can be a name -- "ciocia_hania" is one however it got there."""
|
|
root = _album(tmp_path / "pack", custom={"t1": ["ciocia_hania"]})
|
|
assert "ciocia_hania" in _words(sources, root, monkeypatch)
|
|
|
|
|
|
def test_a_label_that_is_a_command_word_is_not_forbidden(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""The wedge: "!add_photo kot" must not outlaw "kot" in aliases.json."""
|
|
root = _album(tmp_path / "pack", custom={"t1": ["kot"], "t2": ["ciocia_hania"]})
|
|
words = _words(sources, root, monkeypatch)
|
|
assert "kot" not in words
|
|
assert "ciocia_hania" in words
|
|
|
|
|
|
@pytest.mark.parametrize("label", ["losowe", "papiez", "komendy", "staty"])
|
|
def test_no_command_word_can_be_outlawed_by_a_label(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, label: str
|
|
) -> None:
|
|
"""Generalised: every alias in the table, not just the one that bit."""
|
|
root = _album(tmp_path / "pack", custom={"t1": [label]})
|
|
assert label not in _words(sources, root, monkeypatch)
|
|
|
|
|
|
def test_a_people_trigger_is_never_excused_by_the_vocabulary(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Only custom labels get the subtraction.
|
|
|
|
A curated real name turning up in this repository is precisely the leak
|
|
the gate exists to fail on, so it stays forbidden even when it collides.
|
|
"""
|
|
root = _album(tmp_path / "pack", triggers=["!kot"])
|
|
assert "kot" in _words(sources, root, monkeypatch)
|
|
|
|
|
|
def test_a_label_still_waiting_for_approval_is_forbidden_already(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""The queue's sidecar is the same typed text, one click before custom.json."""
|
|
root = _album(tmp_path / "pack")
|
|
pending = root / "pending"
|
|
pending.mkdir()
|
|
sidecar = {"name": "x.jpg", "keys": [], "labels": ["wujek_stefan"], "by": "u", "at": 1}
|
|
(pending / "x.jpg.json").write_text(json.dumps(sidecar), encoding="utf-8")
|
|
(pending / "broken.jpg.json").write_text("{ not json", encoding="utf-8")
|
|
assert "wujek_stefan" in _words(sources, root, monkeypatch)
|
|
|
|
|
|
def test_an_album_with_no_custom_file_still_works(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Other sources (the database, .env) contribute too, so this is membership."""
|
|
words = _words(sources, _album(tmp_path / "pack"), monkeypatch)
|
|
assert "zenon" in words
|
|
|
|
|
|
def test_a_corrupt_custom_file_is_ignored_rather_than_fatal(
|
|
sources: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""Refusing every commit because an album file got truncated is backwards."""
|
|
root = _album(tmp_path / "pack", corrupt_custom=True)
|
|
assert "zenon" in _words(sources, root, monkeypatch)
|