345 lines
10 KiB
Python
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
|