# Copyright (c) 2026 Krzysztof Rudnicki. MIT licence. """The stripper, driven against stub binaries rather than the real exiftool. Stubs, not mocks of ``subprocess``: the thing worth testing is that the gate reads the *output back* and refuses on what it finds there, and a stub that really writes a file and really prints a report exercises that end to end without needing exiftool installed. ``scripts/e2e.sh`` runs the real one. """ from __future__ import annotations import stat from typing import TYPE_CHECKING import pytest from signal_bot.media_sniff import JPEG, MP4, SVG, WEBM, MediaKind from signal_bot.media_strip import FALLBACK_PATHS, ExiftoolStripper, locate if TYPE_CHECKING: from pathlib import Path CLEAN_REPORT = """[ExifTool] ExifToolVersion : 13.55 [File] FileName : out.jpg [File] MIMEType : image/jpeg [Composite] ImageSize : 48x48 """ # A geotag as a phone actually writes it into an mp4: a QuickTime atom, which # a gate looking only at "GPS" would wave straight through. LEAKY_REPORT = """[File] FileName : out.mp4 [QuickTime] MajorBrand : MP4 Base Media v1 [QuickTime] GPSCoordinates : 52.2297 21.0122 0 """ # Lines the parser must simply skip rather than trip over. NOISY_REPORT = """Warning: something exiftool felt like saying [File] FileName : out.jpg ======== out.jpg [Composite] ImageSize : 48x48 """ STUB = """#!/usr/bin/env python3 import sys args = sys.argv[1:] if "-all=" in args: {strip} else: sys.stdout.write({report!r}) sys.exit(0) """ def _stub(tmp_path: Path, name: str, *, strip: str, report: str = CLEAN_REPORT) -> str: """Write an executable stand-in for exiftool and return its path.""" path = tmp_path / name path.write_text(STUB.format(strip=strip, report=report), encoding="utf-8") path.chmod(path.stat().st_mode | stat.S_IEXEC) return str(path) COPIES = 'open(args[args.index("-o") + 1], "wb").write(open(args[-1], "rb").read())' WRITES_NOTHING = "pass" FAILS = "sys.exit(3)" def test_a_clean_file_comes_back_stripped(tmp_path: Path) -> None: stripper = ExiftoolStripper(_stub(tmp_path, "ok", strip=COPIES)) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) == b"\xff\xd8\xffdata" def test_a_tag_that_survived_the_strip_refuses_the_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The whole point: the exit code was 0 and the file is still dirty.""" # flatten is stubbed out so this reaches the tag check. Without it the # re-mux refuses the fixture first -- it is twelve bytes, not a video -- # and the test would still pass, for the wrong reason. monkeypatch.setattr("signal_bot.media_strip.flatten", lambda payload, suffix: payload) stripper = ExiftoolStripper(_stub(tmp_path, "leaky", strip=COPIES, report=LEAKY_REPORT)) assert stripper.strip(b"\x00\x00\x00\x20ftypmp42", MP4) is None def test_lines_that_are_not_tags_are_skipped(tmp_path: Path) -> None: stripper = ExiftoolStripper(_stub(tmp_path, "noisy", strip=COPIES, report=NOISY_REPORT)) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) == b"\xff\xd8\xffdata" def test_a_strip_that_exits_nonzero_stores_nothing(tmp_path: Path) -> None: stripper = ExiftoolStripper(_stub(tmp_path, "fails", strip=FAILS)) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) is None def test_a_strip_that_writes_no_output_stores_nothing(tmp_path: Path) -> None: """Exit 0 and no file is exactly the shape of the ffmpeg regression.""" stripper = ExiftoolStripper(_stub(tmp_path, "silent", strip=WRITES_NOTHING)) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) is None def test_a_report_that_fails_to_run_stores_nothing(tmp_path: Path) -> None: path = tmp_path / "half" path.write_text( "#!/usr/bin/env python3\n" "import sys\n" "args = sys.argv[1:]\n" 'if "-all=" in args:\n' ' open(args[args.index("-o") + 1], "wb").write(open(args[-1], "rb").read())\n' " sys.exit(0)\n" "sys.exit(4)\n", encoding="utf-8", ) path.chmod(path.stat().st_mode | stat.S_IEXEC) assert ExiftoolStripper(str(path)).strip(b"\xff\xd8\xffdata", JPEG) is None def test_a_missing_binary_refuses_rather_than_raising(tmp_path: Path) -> None: stripper = ExiftoolStripper(str(tmp_path / "not-here")) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) is None def test_no_exiftool_at_all_refuses_the_upload(monkeypatch: pytest.MonkeyPatch) -> None: """Never stores it dirty instead: a strip that cannot run is a refusal. Both lookups have to be blinded, not just PATH -- on a machine that has perl-image-exiftool the fallback list finds it and this branch is never reached, which is exactly how it went uncovered until the live run. """ monkeypatch.setattr("signal_bot.media_strip.shutil.which", lambda _: None) monkeypatch.setattr("signal_bot.media_strip.Path.is_file", lambda self: False) assert ExiftoolStripper().strip(b"\xff\xd8\xffdata", JPEG) is None @pytest.mark.parametrize("kind", [SVG, WEBM]) def test_the_two_unwritable_formats_are_stored_as_they_arrived( tmp_path: Path, kind: MediaKind ) -> None: """kuhy's decision of 2026-08-31, and it must not need exiftool at all.""" stripper = ExiftoolStripper(str(tmp_path / "never-invoked")) payload = b"a name" assert stripper.strip(payload, kind) == payload def test_locate_prefers_path(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr("signal_bot.media_strip.shutil.which", lambda _: "/somewhere/exiftool") assert locate() == "/somewhere/exiftool" def test_locate_falls_back_to_the_known_install_locations( monkeypatch: pytest.MonkeyPatch, ) -> None: """The live bug of 2026-08-31, pinned. Arch's perl-image-exiftool installs to /usr/bin/vendor_perl, which is on an interactive shell's PATH and not on a systemd unit's -- so the stripper passed every test and refused every upload in production. """ monkeypatch.setattr("signal_bot.media_strip.shutil.which", lambda _: None) monkeypatch.setattr( "signal_bot.media_strip.Path.is_file", lambda self: str(self) == "/usr/bin/vendor_perl/exiftool", ) assert locate() == "/usr/bin/vendor_perl/exiftool" def test_locate_returns_none_when_it_really_is_absent( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr("signal_bot.media_strip.shutil.which", lambda _: None) monkeypatch.setattr("signal_bot.media_strip.Path.is_file", lambda self: False) assert locate() is None def test_the_arch_vendor_perl_path_is_on_the_fallback_list() -> None: """Named explicitly: this is the one that actually bit.""" assert "/usr/bin/vendor_perl/exiftool" in FALLBACK_PATHS RECORDS_ARGV = ( 'open("ARGV_PATH", "w").write(" ".join(args)); ' 'open(args[args.index("-o") + 1], "wb").write(open(args[-1], "rb").read())' ) def test_the_date_tags_are_zeroed_explicitly(tmp_path: Path) -> None: # media_tags allows the QuickTime date tags "by value", on the stated # grounds that -all= zeroes rather than deletes them. Measured on # 2026-08-31 that is false: -all= alone left QuickTime:CreateDate at its # real value and every mp4 in the live album was refused because of it. # -time:all= is what actually zeroes them, so it must stay in the argv. argv = tmp_path / "argv.txt" stripper = ExiftoolStripper( _stub(tmp_path, "records", strip=RECORDS_ARGV.replace("ARGV_PATH", str(argv))) ) stripper.strip(b"\xff\xd8\xffdata", JPEG) assert "-time:all=" in argv.read_text(encoding="utf-8") def test_a_video_has_its_metadata_tracks_dropped_first( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: # exiftool cannot see a mebx track at all -- see media_tracks for the # face-detection payload that survives a complete tag strip -- so the # re-mux has to happen here or not at all. seen: list[tuple[bytes, str]] = [] def fake_flatten(payload: bytes, suffix: str) -> bytes: seen.append((payload, suffix)) return b"flattened" monkeypatch.setattr("signal_bot.media_strip.flatten", fake_flatten) ExiftoolStripper(_stub(tmp_path, "ok1", strip=COPIES)).strip(b"payload", MP4) assert seen == [(b"payload", ".mp4")] def test_a_still_image_is_not_sent_through_the_remux( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: def explode(payload: bytes, suffix: str) -> bytes: raise AssertionError monkeypatch.setattr("signal_bot.media_strip.flatten", explode) stripper = ExiftoolStripper(_stub(tmp_path, "ok2", strip=COPIES)) assert stripper.strip(b"\xff\xd8\xffdata", JPEG) is not None def test_a_video_whose_tracks_cannot_be_dropped_is_refused( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr("signal_bot.media_strip.flatten", lambda payload, suffix: None) stripper = ExiftoolStripper(_stub(tmp_path, "ok3", strip=COPIES)) assert stripper.strip(b"payload", MP4) is None