Files
telegram-sticker-downloader/tests/test_cli.py
T
2026-07-27 15:16:01 +02:00

345 lines
10 KiB
Python

from __future__ import annotations
from pathlib import Path
import pytest
from sticker_downloader import cli
from sticker_downloader.cli import (
EXIT_FAILURES,
EXIT_OK,
EXIT_USAGE,
UsageError,
build_parser,
collect_interactively,
main,
)
from sticker_downloader.results import FileOutcome, FileStatus, PackResult
@pytest.fixture(autouse=True)
def isolated_cwd(tmp_path, monkeypatch):
"""Run each test in a clean directory with no ambient token."""
monkeypatch.chdir(tmp_path)
monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
return tmp_path
@pytest.fixture
def captured_run(monkeypatch):
"""Replace the network layer and record what the CLI asked it to do."""
recorded: dict = {}
def _install(results: list[PackResult]) -> dict:
async def fake_download(token, references, config, progress):
recorded["token"] = token
recorded["references"] = list(references)
recorded["config"] = config
return results
monkeypatch.setattr(cli, "_download", fake_download)
return recorded
return _install
def ok_result(name: str = "Pack") -> PackResult:
return PackResult(
reference=name,
name=name,
outcomes=[FileOutcome(Path(f"{name}/001.webp"), FileStatus.DOWNLOADED, size=10)],
)
def failed_result(name: str = "Pack") -> PackResult:
return PackResult(reference=name, name=name, error="sticker pack not found")
# -- parser ---------------------------------------------------------------
def test_parser_defaults():
args = build_parser().parse_args([])
assert args.packs == []
assert args.from_file is None
assert args.types is None
assert args.output == Path("downloads")
assert args.concurrency == 8
assert args.retries == 3
assert args.write_metadata is True
assert args.pack_thumbnail is True
assert args.overwrite is False
assert args.dry_run is False
def test_version_exits_cleanly(capsys):
with pytest.raises(SystemExit) as excinfo:
main(["--version"])
assert excinfo.value.code == EXIT_OK
assert "1.0.0" in capsys.readouterr().out
def test_verbose_and_quiet_are_mutually_exclusive():
with pytest.raises(SystemExit):
build_parser().parse_args(["-v", "-q", "Pack"])
# -- argument validation --------------------------------------------------
def test_packs_and_from_file_conflict(capsys):
assert main(["Pack", "--from-file", "urls.txt"]) == EXIT_USAGE
assert "not both" in capsys.readouterr().err
def test_missing_from_file_is_a_usage_error(capsys):
assert main(["--from-file", "nope.txt"]) == EXIT_USAGE
assert "not found" in capsys.readouterr().err
def test_invalid_file_type_is_reported_before_connecting(capsys):
assert main(["Pack", "--types", "gif"]) == EXIT_USAGE
assert "gif" in capsys.readouterr().err
def test_all_references_invalid_is_a_usage_error(capsys):
assert main(["https://example.com/x"]) == EXIT_USAGE
assert "no valid sticker pack references" in capsys.readouterr().err
def test_missing_token_explains_how_to_set_one(capsys):
assert main(["Pack"]) == EXIT_USAGE
assert "BotFather" in capsys.readouterr().err
def test_no_packs_and_no_tty_is_a_usage_error(capsys, monkeypatch):
monkeypatch.setattr(cli.sys.stdin, "isatty", lambda: False)
assert main([]) == EXIT_USAGE
assert "no packs given" in capsys.readouterr().err
# -- wiring ---------------------------------------------------------------
def test_successful_run_returns_zero(captured_run, monkeypatch):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "token-from-env")
recorded = captured_run([ok_result()])
assert main(["https://t.me/addstickers/Pack", "--types", "webp,png"]) == EXIT_OK
assert recorded["token"] == "token-from-env"
assert recorded["references"] == ["https://t.me/addstickers/Pack"]
assert recorded["config"].file_types == frozenset({"webp", "png"})
def test_failing_pack_returns_one(captured_run, monkeypatch):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
captured_run([ok_result("A"), failed_result("B")])
assert main(["A", "B"]) == EXIT_FAILURES
def test_token_flag_beats_environment(captured_run, monkeypatch):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "from-env")
recorded = captured_run([ok_result()])
assert main(["Pack", "--token", "from-flag"]) == EXIT_OK
assert recorded["token"] == "from-flag"
def test_token_is_read_from_env_file(captured_run, isolated_cwd):
(isolated_cwd / "custom.env").write_text("TELEGRAM_BOT_TOKEN=from-file\n")
recorded = captured_run([ok_result()])
assert main(["Pack", "--env-file", "custom.env"]) == EXIT_OK
assert recorded["token"] == "from-file"
def test_flags_reach_the_config(captured_run, monkeypatch, tmp_path):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
recorded = captured_run([ok_result()])
exit_code = main(
[
"Pack",
"--output",
str(tmp_path / "out"),
"--concurrency",
"4",
"--retries",
"5",
"--overwrite",
"--emoji-names",
"--no-metadata",
"--no-pack-thumbnail",
"--dry-run",
]
)
assert exit_code == EXIT_OK
config = recorded["config"]
assert config.output_dir == tmp_path / "out"
assert config.concurrency == 4
assert config.retries == 5
assert config.overwrite is True
assert config.emoji_names is True
assert config.write_metadata is False
assert config.pack_thumbnail is False
assert config.dry_run is True
def test_default_types_are_all_when_not_interactive(captured_run, monkeypatch):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
recorded = captured_run([ok_result()])
assert main(["Pack"]) == EXIT_OK
assert recorded["config"].sorted_file_types == ["webp", "tgs", "webm", "png"]
def test_references_come_from_file(captured_run, monkeypatch, isolated_cwd):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
(isolated_cwd / "packs.txt").write_text(
"# mine\nhttps://t.me/addstickers/A\n\nB\n", encoding="utf-8"
)
recorded = captured_run([ok_result()])
assert main(["--from-file", "packs.txt"]) == EXIT_OK
assert recorded["references"] == ["https://t.me/addstickers/A", "B"]
def test_references_come_from_stdin(captured_run, monkeypatch):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
monkeypatch.setattr(cli.sys, "stdin", _FakeStdin("A\nB\n"))
recorded = captured_run([ok_result()])
assert main(["--from-file", "-"]) == EXIT_OK
assert recorded["references"] == ["A", "B"]
def test_invalid_lines_are_warned_about_but_do_not_stop_the_run(
captured_run, monkeypatch, isolated_cwd, capsys
):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
(isolated_cwd / "packs.txt").write_text("Good\nme/addstickers/Broken\n")
captured_run([ok_result()])
assert main(["--from-file", "packs.txt", "--quiet"]) == EXIT_OK
class _FakeStdin:
def __init__(self, text: str) -> None:
self._text = text
def read(self) -> str:
return self._text
def isatty(self) -> bool:
return False
# -- interactive mode -----------------------------------------------------
def _scripted(answers: list[str]):
remaining = list(answers)
def ask(_prompt: str) -> str:
return remaining.pop(0)
return ask
def test_interactive_single_url():
types, references = collect_interactively(
types_given=None,
ask=_scripted(["webp,png", "1", "https://t.me/addstickers/A t.me/addstickers/B"]),
say=lambda _msg: None,
)
assert types == "webp,png"
assert references == ["https://t.me/addstickers/A", "t.me/addstickers/B"]
def test_interactive_defaults_to_all_types_and_option_one():
types, references = collect_interactively(
types_given=None,
ask=_scripted(["", "", "A"]),
say=lambda _msg: None,
)
assert types == ""
assert references == ["A"]
def test_interactive_skips_the_type_prompt_when_given(tmp_path):
types, references = collect_interactively(
types_given="webp",
ask=_scripted(["1", "A"]),
say=lambda _msg: None,
)
assert types == "webp"
assert references == ["A"]
def test_interactive_reads_the_url_file(tmp_path):
url_file = tmp_path / "urls.txt"
url_file.write_text("https://t.me/addstickers/A\nB\n", encoding="utf-8")
_types, references = collect_interactively(
types_given="all",
url_file=url_file,
ask=_scripted(["2"]),
say=lambda _msg: None,
)
assert references == ["https://t.me/addstickers/A", "B"]
def test_interactive_missing_url_file(tmp_path):
with pytest.raises(UsageError):
collect_interactively(
types_given="all",
url_file=tmp_path / "absent.txt",
ask=_scripted(["2"]),
say=lambda _msg: None,
)
def test_interactive_rejects_a_bad_choice():
with pytest.raises(UsageError):
collect_interactively(
types_given="all", ask=_scripted(["9"]), say=lambda _msg: None
)
def test_interactive_rejects_an_empty_url():
with pytest.raises(UsageError):
collect_interactively(
types_given="all", ask=_scripted(["1", " "]), say=lambda _msg: None
)
def test_interactive_validates_types_before_anything_else():
with pytest.raises(Exception, match="gif"):
collect_interactively(
types_given=None, ask=_scripted(["gif"]), say=lambda _msg: None
)
def test_interactive_path_is_used_when_stdin_is_a_tty(
captured_run, monkeypatch, tmp_path
):
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "t")
monkeypatch.setattr(cli.sys.stdin, "isatty", lambda: True)
monkeypatch.setattr(
cli, "collect_interactively", lambda **_kwargs: ("webp", ["Pack"])
)
recorded = captured_run([ok_result()])
assert main([]) == EXIT_OK
assert recorded["references"] == ["Pack"]
assert recorded["config"].file_types == frozenset({"webp"})
def test_ctrl_d_during_prompts_aborts_cleanly(monkeypatch, capsys):
monkeypatch.setattr(cli.sys.stdin, "isatty", lambda: True)
def raise_eof(**_kwargs):
raise EOFError
monkeypatch.setattr(cli, "collect_interactively", raise_eof)
assert main([]) == EXIT_USAGE
assert "Aborted" in capsys.readouterr().err