efc2be5f8a
Completes TODO #6. The bot accepted photo, animation, video, voice and sticker; music files, documents and round videos were silently dropped. - Replace the five near-identical media handlers with one dispatch table. Animations are matched before documents because Telegram populates both fields for a GIF, so the old ordering would have filed every GIF as a document once document support existed. - Store video notes as "video". Telegram has no cached inline result for video notes, so this is what keeps round videos searchable; it costs the round presentation on playback. - Build inline results from a table keyed by media_type, and cover audio and document. A test asserts every storable type has a builder, so a new media type cannot be added without a way to return it. - Reuse one MEDIA_FILTER for both the idle and mid-keyword handlers; the second list had already drifted from the first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""Media handling tests. Run with: python test_bot.py
|
|
|
|
Covers the dispatch table that decides how an incoming message is stored and
|
|
how it comes back through inline search.
|
|
"""
|
|
import datetime
|
|
import os
|
|
import sys
|
|
|
|
os.environ.setdefault("BOT_TOKEN", "123456789:AAHtestTokenPlaceholder_ABCDEFGHIJKLMNOP")
|
|
os.environ.setdefault("DB_PATH", ":memory:")
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from aiogram.types import (Animation, Audio, Chat, Document, Message, PhotoSize,
|
|
Sticker, Video, VideoNote, Voice)
|
|
|
|
import bot as B
|
|
|
|
CHAT = Chat(id=1, type="private")
|
|
NOW = datetime.datetime.now()
|
|
|
|
|
|
def msg(**kw) -> Message:
|
|
return Message(message_id=1, date=NOW, chat=CHAT, **kw)
|
|
|
|
|
|
def sample_messages():
|
|
photo = PhotoSize(file_id="F_photo", file_unique_id="U_photo", width=1, height=1)
|
|
anim = Animation(file_id="F_gif", file_unique_id="U_gif", width=1, height=1, duration=1)
|
|
sticker = Sticker(file_id="F_st", file_unique_id="U_st", width=1, height=1,
|
|
type="regular", is_animated=False, is_video=False)
|
|
return [
|
|
("photo", msg(photo=[photo])),
|
|
# Telegram sets document alongside animation for backward compatibility.
|
|
("gif", msg(animation=anim, document=Document(file_id="F_gif", file_unique_id="U_gif"))),
|
|
("sticker", msg(sticker=sticker)),
|
|
("video", msg(video=Video(file_id="F_v", file_unique_id="U_v", width=1, height=1, duration=1))),
|
|
("video", msg(video_note=VideoNote(file_id="F_vn", file_unique_id="U_vn", length=1, duration=1))),
|
|
("voice", msg(voice=Voice(file_id="F_vo", file_unique_id="U_vo", duration=1))),
|
|
("audio", msg(audio=Audio(file_id="F_a", file_unique_id="U_a", duration=1))),
|
|
("document", msg(document=Document(file_id="F_doc", file_unique_id="U_doc"))),
|
|
]
|
|
|
|
|
|
def test_every_type_extracts():
|
|
for expected, message in sample_messages():
|
|
found = B.extract_media(message)
|
|
assert found is not None, f"{expected}: nothing extracted"
|
|
assert found[0] == expected, f"expected {expected}, got {found[0]}"
|
|
print(" every supported media type extracts to the right media_type")
|
|
|
|
|
|
def test_animation_beats_document():
|
|
gif = sample_messages()[1][1]
|
|
assert gif.document is not None, "test fixture should mimic Telegram's dual field"
|
|
assert B.extract_media(gif)[0] == "gif", "a GIF must not be filed as a document"
|
|
print(" animation wins over the backward-compat document field")
|
|
|
|
|
|
def test_video_note_stored_as_video():
|
|
note = sample_messages()[4][1]
|
|
assert B.extract_media(note) == ("video", "F_vn", "U_vn")
|
|
print(" video notes are stored as video so inline search can return them")
|
|
|
|
|
|
def test_non_media_ignored():
|
|
assert B.extract_media(msg(text="hello")) is None
|
|
print(" plain text messages are ignored")
|
|
|
|
|
|
def test_every_stored_type_renders_inline():
|
|
stored = {t for t, _ in B.MEDIA_EXTRACTORS}
|
|
missing = stored - set(B.INLINE_BUILDERS)
|
|
assert not missing, f"stored but not renderable inline: {sorted(missing)}"
|
|
print(" every stored media_type has a matching inline builder")
|
|
|
|
|
|
def test_builders_construct():
|
|
for media_type, build in B.INLINE_BUILDERS.items():
|
|
build("rid", f"FID_{media_type}", "cat, funny")
|
|
# Documents and videos require a non-empty title; keywords can be blank.
|
|
build("rid", f"FID_{media_type}", "")
|
|
print(" inline builders construct, including with empty keywords")
|
|
|
|
|
|
def main():
|
|
for test in (test_every_type_extracts, test_animation_beats_document,
|
|
test_video_note_stored_as_video, test_non_media_ignored,
|
|
test_every_stored_type_renders_inline, test_builders_construct):
|
|
test()
|
|
print("\nAll media tests passed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|