ai did his job.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from favorite_stickers.media import ConvertedMedia, IncomingMedia
|
||||
from favorite_stickers.service import (
|
||||
FavoriteStickerService,
|
||||
RemoteSticker,
|
||||
ToggleAction,
|
||||
)
|
||||
from favorite_stickers.store import Store
|
||||
|
||||
|
||||
class FakeConverter:
|
||||
def __init__(self) -> None:
|
||||
self.calls = 0
|
||||
|
||||
async def convert(self, media: IncomingMedia) -> ConvertedMedia:
|
||||
self.calls += 1
|
||||
return ConvertedMedia(Path("result.webm"), media.content_hash or "hash")
|
||||
|
||||
|
||||
class FakeTelegram:
|
||||
def __init__(self) -> None:
|
||||
self.created: list[str] = []
|
||||
self.added: list[str] = []
|
||||
self.deleted: list[str] = []
|
||||
self.packs: dict[str, list[RemoteSticker]] = {}
|
||||
self.number = 0
|
||||
|
||||
async def create_pack(
|
||||
self,
|
||||
user_id: int,
|
||||
name: str,
|
||||
title: str,
|
||||
sticker_path: Path,
|
||||
emoji: str,
|
||||
) -> RemoteSticker:
|
||||
self.created.append(name)
|
||||
remote = self._remote()
|
||||
self.packs[name] = [remote]
|
||||
return remote
|
||||
|
||||
async def add_sticker(
|
||||
self, user_id: int, name: str, sticker_path: Path, emoji: str
|
||||
) -> RemoteSticker:
|
||||
self.added.append(name)
|
||||
remote = self._remote()
|
||||
self.packs[name].append(remote)
|
||||
return remote
|
||||
|
||||
async def delete_sticker(self, sticker_file_id: str) -> None:
|
||||
self.deleted.append(sticker_file_id)
|
||||
for stickers in self.packs.values():
|
||||
stickers[:] = [item for item in stickers if item.file_id != sticker_file_id]
|
||||
|
||||
async def get_pack(self, name: str) -> list[RemoteSticker] | None:
|
||||
stickers = self.packs.get(name)
|
||||
return list(stickers) if stickers is not None else None
|
||||
|
||||
def _remote(self) -> RemoteSticker:
|
||||
self.number += 1
|
||||
return RemoteSticker(
|
||||
file_id=f"file-{self.number}", unique_id=f"unique-{self.number}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_first_media_creates_video_pack_without_questions(tmp_path) -> None:
|
||||
store = Store(tmp_path / "bot.sqlite3")
|
||||
store.initialize()
|
||||
telegram = FakeTelegram()
|
||||
converter = FakeConverter()
|
||||
service = FavoriteStickerService(store, telegram, converter, "TestBot")
|
||||
|
||||
result = await service.toggle(
|
||||
7, IncomingMedia(Path("source.png"), "source-id", "static", "❤️", "hash")
|
||||
)
|
||||
|
||||
assert result.action is ToggleAction.ADDED
|
||||
assert result.pack_name == "favorites_7_by_testbot"
|
||||
assert telegram.created == [result.pack_name]
|
||||
pack = store.latest_pack(7)
|
||||
assert pack is not None
|
||||
assert pack.sticker_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resending_original_sticker_removes_it_without_conversion(
|
||||
tmp_path,
|
||||
) -> None:
|
||||
store = Store(tmp_path / "bot.sqlite3")
|
||||
store.initialize()
|
||||
telegram = FakeTelegram()
|
||||
converter = FakeConverter()
|
||||
service = FavoriteStickerService(store, telegram, converter, "TestBot")
|
||||
media = IncomingMedia(Path("source.webp"), "same-id", "static", "👍", "same-hash")
|
||||
await service.toggle(7, media)
|
||||
|
||||
result = await service.toggle(7, media)
|
||||
|
||||
assert result.action is ToggleAction.REMOVED
|
||||
assert telegram.deleted == ["file-1"]
|
||||
assert converter.calls == 1
|
||||
pack = store.latest_pack(7)
|
||||
assert pack is not None
|
||||
assert pack.sticker_count == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_pack_rolls_over_to_numbered_pack(tmp_path) -> None:
|
||||
store = Store(tmp_path / "bot.sqlite3")
|
||||
store.initialize()
|
||||
telegram = FakeTelegram()
|
||||
converter = FakeConverter()
|
||||
service = FavoriteStickerService(
|
||||
store, telegram, converter, "TestBot", pack_limit=2
|
||||
)
|
||||
|
||||
results = []
|
||||
for index in range(3):
|
||||
results.append(
|
||||
await service.toggle(
|
||||
7,
|
||||
IncomingMedia(
|
||||
Path(f"{index}.png"),
|
||||
f"source-{index}",
|
||||
"static",
|
||||
"❤️",
|
||||
f"hash-{index}",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
assert telegram.created == [
|
||||
"favorites_7_by_testbot",
|
||||
"favorites_7_2_by_testbot",
|
||||
]
|
||||
assert telegram.added == ["favorites_7_by_testbot"]
|
||||
assert results[-1].pack_name == "favorites_7_2_by_testbot"
|
||||
|
||||
|
||||
class FailOnceStore(Store):
|
||||
fail_add = False
|
||||
fail_remove = False
|
||||
|
||||
def complete_add(self, user_id: int, file_id: str, unique_id: str) -> None:
|
||||
if self.fail_add:
|
||||
self.fail_add = False
|
||||
raise RuntimeError("simulated SQLite add failure")
|
||||
super().complete_add(user_id, file_id, unique_id)
|
||||
|
||||
def complete_remove(self, user_id: int) -> None:
|
||||
if self.fail_remove:
|
||||
self.fail_remove = False
|
||||
raise RuntimeError("simulated SQLite remove failure")
|
||||
super().complete_remove(user_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_add_is_recovered_after_sqlite_failure(tmp_path) -> None:
|
||||
store = FailOnceStore(tmp_path / "bot.sqlite3")
|
||||
store.initialize()
|
||||
store.fail_add = True
|
||||
telegram = FakeTelegram()
|
||||
converter = FakeConverter()
|
||||
service = FavoriteStickerService(store, telegram, converter, "TestBot")
|
||||
media = IncomingMedia(Path("source.png"), "source", "static", "❤️", "hash")
|
||||
|
||||
with pytest.raises(RuntimeError, match="SQLite add"):
|
||||
await service.toggle(7, media)
|
||||
|
||||
assert store.pending_operation(7) is not None
|
||||
result = await service.toggle(7, media)
|
||||
assert store.pending_operation(7) is None
|
||||
assert result.action is ToggleAction.ADDED
|
||||
assert telegram.deleted == []
|
||||
assert converter.calls == 1
|
||||
assert store.find_sticker(7, telegram_unique_id="source") is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_remove_is_recovered_after_sqlite_failure(tmp_path) -> None:
|
||||
store = FailOnceStore(tmp_path / "bot.sqlite3")
|
||||
store.initialize()
|
||||
telegram = FakeTelegram()
|
||||
converter = FakeConverter()
|
||||
service = FavoriteStickerService(store, telegram, converter, "TestBot")
|
||||
media = IncomingMedia(Path("source.png"), "source", "static", "❤️", "hash")
|
||||
await service.toggle(7, media)
|
||||
store.fail_remove = True
|
||||
|
||||
with pytest.raises(RuntimeError, match="SQLite remove"):
|
||||
await service.toggle(7, media)
|
||||
|
||||
assert store.pending_operation(7) is not None
|
||||
result = await service.toggle(7, media)
|
||||
assert store.pending_operation(7) is None
|
||||
assert result.action is ToggleAction.REMOVED
|
||||
assert telegram.created == ["favorites_7_by_testbot"]
|
||||
assert telegram.added == []
|
||||
assert converter.calls == 1
|
||||
assert store.find_sticker(7, telegram_unique_id="source") is None
|
||||
Reference in New Issue
Block a user