0a8c81db90
Completes TODO #3. Submissions are gated behind ACCEPT_SUBMISSIONS, which defaults to off: is_allowed() treats everyone as allowed when ALLOWED_USERS is empty, so opening submissions implicitly would have accepted media from anyone who found the bot. - Schema v3 adds status, reviewed_by and reviewed_at. Existing rows default to approved, so upgrading changes nothing for a running bot. - Search, /list and counts only return approved items; callers that need everything pass status=None. - /review shows the oldest pending item with approve, reject, fix keywords and stop. No cursor is stored anywhere: each action just asks for the next pending item, which is what lets a moderator stop and resume later, and lets several moderators share one queue. - set_status only updates rows still pending, so if two moderators open the same item the first decision wins and the second is told so. - A failed send no longer wedges the queue; the item's details are shown anyway so it can still be rejected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
4.1 KiB
Python
104 lines
4.1 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_every_stored_type_can_be_sent_back():
|
|
stored = {t for t, _ in B.MEDIA_EXTRACTORS}
|
|
missing = stored - set(B.MEDIA_SENDERS)
|
|
assert not missing, f"stored but not sendable for review: {sorted(missing)}"
|
|
print(" every stored media_type can be sent back for review")
|
|
|
|
|
|
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_every_stored_type_can_be_sent_back, test_builders_construct):
|
|
test()
|
|
print("\nAll media tests passed.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|