"""Test doubles for the Telegram bot and the CDN fetcher.""" from __future__ import annotations import asyncio from dataclasses import dataclass, field from typing import Any import pytest from telegram.error import BadRequest from sticker_downloader.config import DownloadConfig @dataclass class FakeFile: file_path: str @dataclass class FakeThumbnail: file_id: str @dataclass class FakeSticker: file_id: str file_unique_id: str = "uniq" emoji: str | None = "😀" is_animated: bool = False is_video: bool = False thumbnail: FakeThumbnail | None = None width: int = 512 height: int = 512 type: str = "regular" @dataclass class FakeStickerSet: name: str title: str = "A Pack" sticker_type: str = "regular" stickers: list[FakeSticker] = field(default_factory=list) thumbnail: FakeThumbnail | None = None class FakeBot: """Implements just the two coroutines the downloader calls.""" def __init__( self, sets: dict[str, FakeStickerSet] | None = None, *, extensions: dict[str, str] | None = None, ) -> None: self._sets = sets or {} #: file_id -> extension served by the fake CDN URL. self._extensions = extensions or {} self.get_sticker_set_calls: list[str] = [] self.get_file_calls: list[str] = [] async def get_sticker_set(self, name: str) -> FakeStickerSet: self.get_sticker_set_calls.append(name) try: return self._sets[name] except KeyError: raise BadRequest("Stickerset_invalid") from None async def get_file(self, file_id: str) -> FakeFile: self.get_file_calls.append(file_id) extension = self._extensions.get(file_id, "webp") return FakeFile(file_path=f"https://cdn.example/file/{file_id}.{extension}") class FakeFetcher: """Returns deterministic bytes and can be told to fail for some URLs.""" def __init__( self, *, payload: bytes = b"sticker-bytes", failures: dict[str, Exception] | None = None, ) -> None: self._payload = payload self._failures = failures or {} self.urls: list[str] = [] self.max_concurrent = 0 self._in_flight = 0 async def fetch(self, url: str) -> bytes: self.urls.append(url) self._in_flight += 1 self.max_concurrent = max(self.max_concurrent, self._in_flight) try: await asyncio.sleep(0) for needle, error in self._failures.items(): if needle in url: raise error return self._payload finally: self._in_flight -= 1 async def no_sleep(_delay: float) -> None: """Drop-in for ``asyncio.sleep`` so retry tests run instantly.""" return None def make_sticker(index: int, **overrides: Any) -> FakeSticker: """A static sticker with a PNG thumbnail, unless overridden.""" defaults: dict[str, Any] = { "file_id": f"file{index}", "file_unique_id": f"uniq{index}", "thumbnail": FakeThumbnail(file_id=f"thumb{index}"), } defaults.update(overrides) return FakeSticker(**defaults) @pytest.fixture def pack() -> FakeStickerSet: return FakeStickerSet( name="TestPack", title="Test Pack", stickers=[make_sticker(1), make_sticker(2)], thumbnail=FakeThumbnail(file_id="packthumb"), ) @pytest.fixture def bot(pack: FakeStickerSet) -> FakeBot: return FakeBot({pack.name: pack}) @pytest.fixture def fetcher() -> FakeFetcher: return FakeFetcher() @pytest.fixture def config(tmp_path) -> DownloadConfig: return DownloadConfig(output_dir=tmp_path / "downloads")