big upgrade UwU
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
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
|
||||
Reference in New Issue
Block a user