Spaces:
Running
Running
File size: 42,222 Bytes
58f00c9 84e63a1 58f00c9 c3c6394 58f00c9 6c3a31d d55a0af 6c3a31d 58f00c9 1753544 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a d55a0af b737d7a 6c3a31d b737d7a 58f00c9 b737d7a d55a0af 6c3a31d 58f00c9 d55a0af 58f00c9 d55a0af b737d7a 6c3a31d b737d7a 6c3a31d 1753544 58f00c9 b737d7a 58f00c9 b737d7a 6c3a31d b737d7a 6c3a31d b737d7a 6c3a31d 58f00c9 b737d7a 58f00c9 5f400f8 58f00c9 c3c6394 d55a0af c3c6394 d55a0af c3c6394 5f400f8 c3c6394 5f400f8 c3c6394 5f400f8 c3c6394 58f00c9 a00abaf 58f00c9 b737d7a 58f00c9 a00abaf 58f00c9 6c3a31d 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 b737d7a 58f00c9 84e63a1 58f00c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 | """Handler-level tests: real Telethon event builders, fake events and client."""
import asyncio
import shutil
import time
from pathlib import Path
import pytest
from telethon import functions
from bot import app as appmod
from bot import errorlog, panels
from bot.app import COMMANDS, OWNER_COMMANDS, PANEL_NAMES, App, Pending, cmd_pattern, is_renameable
from bot.config import load_config
from bot.db import Database
from bot.queue import JobQueue
from .conftest import needs_ffmpeg
from .appkit import (
ADMIN_CFG, ADMIN_DB, EPISODE, EPISODE_NEW, LOG, OWNER, STRANGER, auto_thumb, autofmt, click, data_of, edit_auto, env_for, jpeg,
prefs, say, send_doc, set_auto_thumb,
)
from .tgfakes import (
FakeCallback, FakeClient, FakeEvent, FakeMessage, dispatch, document_message, flat_buttons, make_file, variables_line,
)
# ---------------------------------------------------------------------------
# access control & routing
# ---------------------------------------------------------------------------
async def test_strangers_are_ignored_and_reported_once(ctx):
for text in ("/start", "hello", "/rename x", "/cancel"):
assert (await say(ctx, STRANGER, text)).out == []
assert (await send_doc(ctx, STRANGER)).out == []
cb = await click(ctx, STRANGER, "m:home")
assert cb.edits == [] and not ctx.app.queue.jobs
reports = [t for chat, t, _kw in ctx.client.sent if chat == LOG]
assert len(reports) == 1 and str(STRANGER) in reports[0] and f"/addadmin {STRANGER}" in reports[0]
async def test_a_plain_message_from_a_stranger_is_also_reported(ctx):
await say(ctx, STRANGER, "hi there")
assert len([1 for chat, _t, _k in ctx.client.sent if chat == LOG]) == 1
async def test_owner_and_admins_from_config_and_db_are_allowed(ctx):
await ctx.db.add_admin(ADMIN_DB, OWNER)
ctx.app.admins |= set(await ctx.db.list_admins())
for uid in (OWNER, ADMIN_CFG, ADMIN_DB):
assert (await say(ctx, uid, "/help")).out, uid
assert ctx.client.sent == [] # nobody was reported
async def test_group_chats_are_ignored(ctx):
assert (await say(ctx, OWNER, "/start", private=False)).out == []
assert (await say(ctx, STRANGER, "/start", private=False)).out == []
assert ctx.client.sent == []
async def test_owner_only_commands_are_refused_for_admins(ctx):
for text in ("/addadmin 123456", "/deladmin 123456", "/admins"):
ev = await say(ctx, ADMIN_CFG, text)
assert ev.texts == ["⛔ Only the owner can do that."], text
assert 123456 not in ctx.app.admins
assert (await click(ctx, ADMIN_CFG, "m:admin")).alerts == ["Owner only."]
assert (await click(ctx, ADMIN_CFG, "adm:stats")).alerts == ["Owner only."]
assert (await click(ctx, ADMIN_CFG, "adm:clear")).alerts == ["Owner only."]
assert ctx.client.sent == []
def test_cmd_pattern_accepts_args_bot_suffix_and_any_case():
p = cmd_pattern("rename")
assert p.match("/rename New Name").group(1) == "New Name"
assert p.match("/rename@MyBot New Name").group(1) == "New Name"
assert p.match("/RENAME").group(1) is None
assert p.match("/rename\nline one\nline two").group(1) == "line one\nline two"
assert not p.match("/renamex") and not p.match("rename") and not p.match("/re")
def test_every_menu_command_has_a_handler_and_telegram_compatible_text(ctx):
table = ctx.app._command_table()
for name, description in COMMANDS:
assert name in table
assert name.islower() and 1 <= len(name) <= 32
assert 3 <= len(description) <= 256
assert OWNER_COMMANDS <= set(table)
assert {"settitle", "setauthor", "setartist", "setaudio", "setsubtitle", "setvideo", "removewords", "setchannel"} <= set(table)
assert dict(COMMANDS)["status"] == "Show basic bot uptime, ping, and version information."
assert dict(COMMANDS)["stats"] == "Show detailed system stats such as users, CPU, RAM, and disk usage."
async def test_setup_registers_command_menu_loads_admins_and_survives_log_channel_errors(tmp_path):
cfg = load_config(env_for(tmp_path))
db = Database(f"sqlite+aiosqlite:///{tmp_path}/setup.db")
await db.init()
await db.add_admin(ADMIN_DB, OWNER)
client = FakeClient()
client.bad_entities.add(LOG)
app = App(client, cfg, db)
await app.setup()
try:
assert client.parse_mode == "html"
assert ADMIN_DB in app.admins and ADMIN_CFG in app.admins
(request,) = [r for r in client.requests if isinstance(r, functions.bots.SetBotCommandsRequest)]
assert [c.command for c in request.commands] == [c for c, _ in COMMANDS]
assert len(client.handlers) > 10
finally:
await app.shutdown()
await db.close()
# ---------------------------------------------------------------------------
# /start, /help and the one-line settings commands
# ---------------------------------------------------------------------------
async def test_start_shows_the_home_panel_and_hides_admin_from_admins(ctx):
ev = await say(ctx, OWNER, "/start")
text, buttons, kw = ev.out[0]
assert "Auto Rename Bot" in text and kw["parse_mode"] == "html"
assert {"m:auto", "m:rename", "m:meta", "m:ftype", "m:formats", "m:help", "m:admin"} <= set(data_of(buttons))
assert "m:cap" not in data_of(buttons) # the caption and the thumbnail are edited in /formats
assert "m:admin" not in data_of((await say(ctx, ADMIN_CFG, "/start")).out[0][1])
@pytest.mark.parametrize("text", ["/start payload", "/start@SomeBot", "/START"])
async def test_start_variants(ctx, text):
assert (await say(ctx, OWNER, text)).out
async def test_start_and_help(ctx):
ctx.app.pending[OWNER] = Pending("remove", OWNER, 1, "rename", False, time.time() + 100)
await say(ctx, OWNER, "/start")
assert OWNER not in ctx.app.pending
ev = await say(ctx, OWNER, "/help")
assert "How to use" in ev.texts[0] and "x:close" in data_of(ev.out[0][1])
async def test_mode_command(ctx):
ev = await say(ctx, OWNER, "/mode caption")
assert (await prefs(ctx))["mode"] == "caption" and "CAPTION" in ev.texts[0]
for text in ("/mode", "/mode nonsense"):
ev = await say(ctx, OWNER, text)
assert "RENAME MODE" in ev.texts[0] and "set:mode:filename" in data_of(ev.out[0][1])
assert (await prefs(ctx))["mode"] == "caption"
await say(ctx, OWNER, "/mode FILENAME")
assert (await prefs(ctx))["mode"] == "filename"
async def test_filetype_command_and_aliases(ctx):
for arg, expected in (("video", "video"), ("audio", "audio"), ("document", "document"), ("original", "same"),
("doc", "document"), ("file", "document"), ("same", "same")):
await say(ctx, OWNER, f"/filetype {arg}")
assert (await prefs(ctx))["file_type"] == expected, arg
ev = await say(ctx, OWNER, "/filetype")
assert "FILE TYPE" in ev.texts[0]
ev = await say(ctx, OWNER, "/filetype bogus")
assert "FILE TYPE" in ev.texts[0] and (await prefs(ctx))["file_type"] == "same"
async def test_metadata_command(ctx):
await say(ctx, OWNER, "/metadata on")
assert (await prefs(ctx))["metadata_on"] is True
await say(ctx, OWNER, "/metadata off")
assert (await prefs(ctx))["metadata_on"] is False
ev = await say(ctx, OWNER, "/metadata")
assert "currently: Off" in ev.texts[0]
async def test_the_template_and_caption_of_the_auto_format_are_edited_in_its_formats_screen(ctx):
auto = await ctx.db.create_format(OWNER, "Mine", kind="auto")
fid = auto["id"]
await click(ctx, OWNER, f"fm:in:{fid}:template", msg_id=500)
await say(ctx, OWNER, "[S{season}E{episode}] {title} [{quality}]", mid=60)
assert "Template saved" in ctx.client.edited[-1][1]
assert (await autofmt(ctx))["template"] == "[S{season}E{episode}] {title} [{quality}]"
assert "template" not in await prefs(ctx) and "caption" not in await prefs(ctx)
assert "[S{season}E{episode}] {title} [{quality}]" in (await click(ctx, OWNER, "m:rename")).edits[-1][0] # shown there, not edited
await click(ctx, OWNER, f"fm:in:{fid}:caption", msg_id=501)
await say(ctx, OWNER, "<b>{filename}</b> {size}", mid=61)
assert (await autofmt(ctx))["caption"] == "<b>{filename}</b> {size}"
assert await ctx.db.count_formats(OWNER) == 1 # both went into the same format
@pytest.mark.parametrize("command", ["template", "caption", "thumb", "delthumb"])
async def test_the_template_caption_and_thumbnail_commands_are_gone(ctx, command):
assert command not in [name for name, _description in COMMANDS] and command not in ctx.app._command_table()
await set_auto_thumb(ctx, b"\xff\xd8thumb")
before = await autofmt(ctx)
ev = await say(ctx, OWNER, f"/{command} something {{title}}")
assert ev.out == [] # not a command any more: nothing answers it
assert await autofmt(ctx) == before and await auto_thumb(ctx) == b"\xff\xd8thumb"
async def test_removewords_command(ctx):
await say(ctx, OWNER, "/removewords @Chan, [Group]")
assert (await prefs(ctx))["remove_words"] == ["@Chan", "[Group]"]
assert "@Chan, [Group]" in (await say(ctx, OWNER, "/removewords")).texts[0]
await say(ctx, OWNER, "/removewords off")
assert (await prefs(ctx))["remove_words"] == []
async def test_template_and_caption_prompts_list_each_variable_as_its_own_copyable_block(ctx):
fid = (await ctx.db.create_format(OWNER, "Mine", kind="auto"))["id"]
line, blocks = variables_line((await click(ctx, OWNER, f"fm:in:{fid}:template")).edits[-1][0])
assert blocks == line.split(": ", 1)[1].split() and "{quality}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 11
line, blocks = variables_line((await click(ctx, OWNER, f"fm:in:{fid}:caption")).edits[-1][0])
assert blocks == line.split(": ", 1)[1].split() and "{size}" in blocks and "{3episode}" in blocks and "{4episode}" not in blocks and len(blocks) == 13
async def test_settings_are_per_user(ctx):
await say(ctx, OWNER, "/mode caption")
await say(ctx, ADMIN_CFG, "/filetype audio")
assert (await prefs(ctx, OWNER))["mode"] == "caption" and (await prefs(ctx, OWNER))["file_type"] == "same"
assert (await prefs(ctx, ADMIN_CFG))["mode"] == "filename" and (await prefs(ctx, ADMIN_CFG))["file_type"] == "audio"
@pytest.mark.parametrize("command,field", [
("settitle", "title"), ("setauthor", "author"), ("setartist", "artist"),
("setaudio", "audio"), ("setsubtitle", "subtitle"), ("setvideo", "video"),
])
async def test_metadata_field_commands(ctx, command, field):
ev = await say(ctx, OWNER, f"/{command} {{lang}} Value")
assert (await prefs(ctx))["meta"][field] == "{lang} Value" and "✅" in ev.texts[0]
assert "{lang} Value" in (await say(ctx, OWNER, f"/{command}")).texts[0]
await say(ctx, OWNER, f"/{command} off")
assert (await prefs(ctx))["meta"][field] == ""
async def test_keepaudio_by_language_and_by_number(ctx):
ev = await say(ctx, OWNER, "/keepaudio English Japanese")
s = await prefs(ctx)
assert s["keep_audio"] == ["english", "japanese"] and s["metadata_on"] is True and "turned it On" in ev.texts[0]
await say(ctx, OWNER, "/keepaudio 2 1")
assert (await prefs(ctx))["keep_audio"] == ["2", "1"]
assert "2, 1" in (await say(ctx, OWNER, "/keepaudio")).texts[0]
await say(ctx, OWNER, "/keepaudio off")
assert (await prefs(ctx))["keep_audio"] == []
ev = await say(ctx, OWNER, "/keepaudio 0")
assert ev.texts[0].startswith("❌")
async def test_keepsubtitle_is_independent_of_keepaudio(ctx):
await say(ctx, OWNER, "/keepaudio 2 1")
await say(ctx, OWNER, "/keepsubtitle english")
s = await prefs(ctx)
assert s["keep_audio"] == ["2", "1"] and s["keep_subtitle"] == ["english"]
await say(ctx, OWNER, "/keepsubtitle off")
s = await prefs(ctx)
assert s["keep_audio"] == ["2", "1"] and s["keep_subtitle"] == []
async def test_setchannel(ctx):
assert "not set" in (await say(ctx, OWNER, "/setchannel")).texts[0]
ev = await say(ctx, OWNER, "/setchannel -1002223334445")
assert (await prefs(ctx))["target_channel"] == -1002223334445 and "✅" in ev.texts[0]
ev = await say(ctx, OWNER, "/setchannel @name")
assert ev.texts[0].startswith("❌") and (await prefs(ctx))["target_channel"] == -1002223334445
ctx.client.bad_entities.add(-1005556667778)
ev = await say(ctx, OWNER, "/setchannel -1005556667778")
assert "can't access" in ev.texts[0] and (await prefs(ctx))["target_channel"] == -1002223334445
await say(ctx, OWNER, "/setchannel off")
assert (await prefs(ctx))["target_channel"] is None
# ---------------------------------------------------------------------------
# thumbnails
# ---------------------------------------------------------------------------
async def send_photo(ctx, mid=20, sender=OWNER, data=None, **kw):
ev = FakeEvent(sender, message=FakeMessage("", mid=mid, photo=True, **kw), photo_bytes=jpeg() if data is None else data)
await dispatch(ctx.client, ev)
return ev
async def test_a_photo_sent_or_forwarded_to_the_bot_is_not_made_a_thumbnail(ctx):
ev = await send_photo(ctx)
assert ev.out == [] and ctx.client.files == [] and ctx.client.edited == [] # no answer, nothing saved
assert await ctx.db.count_formats(OWNER) == 0 # not even a format was made for it
auto = await ctx.db.create_format(OWNER, "Auto", kind="auto")
manual = await ctx.db.create_format(OWNER, "Manual", kind="manual")
assert (await send_photo(ctx, 21)).out == []
assert (await send_photo(ctx, 22, fwd=True)).out == [] # a forwarded one is no different
assert await ctx.db.get_format_thumb(OWNER, auto["id"]) is None and await ctx.db.get_format_thumb(OWNER, manual["id"]) is None
assert not (await autofmt(ctx))["has_thumb"] and ctx.client.files == []
async def test_only_the_thumbnail_prompt_of_a_format_takes_a_photo(ctx):
auto = await ctx.db.create_format(OWNER, "Auto", kind="auto")
for kind in ("remove", "mt_title", "keep_audio"): # no other prompt takes it
ctx.app.pending[OWNER] = Pending(kind, OWNER, 1, "rename", False, time.time() + 100)
assert (await send_photo(ctx)).out == [] and ctx.app.pending[OWNER].kind == kind
ctx.app.pending[OWNER] = Pending("fmt:template", OWNER, 1, "formats", False, time.time() + 100, fid=auto["id"])
assert (await send_photo(ctx)).out == [] and ctx.app.pending[OWNER].kind == "fmt:template"
ctx.app.pending[OWNER] = Pending("fmt:thumb", OWNER, 1, "formats", False, time.time() - 1, fid=auto["id"]) # one that ran out
assert (await send_photo(ctx)).out == []
assert await ctx.db.get_format_thumb(OWNER, auto["id"]) is None
ctx.app.pending[OWNER] = Pending("fmt:thumb", OWNER, 500, "formats", False, time.time() + 100, fid=auto["id"])
await send_photo(ctx, 30) # the one that is waiting for it does
assert (await ctx.db.get_format_thumb(OWNER, auto["id"]))[:2] == b"\xff\xd8" and OWNER not in ctx.app.pending
async def test_unreadable_photos_are_reported_while_a_format_waits_for_its_thumbnail(ctx):
auto = await ctx.db.create_format(OWNER, "Auto", kind="auto")
for data, said in ((b"not an image", "couldn't read"), (None, "couldn't download")):
ctx.app.pending[OWNER] = Pending("fmt:thumb", OWNER, 500, "formats", False, time.time() + 100, fid=auto["id"])
ev = FakeEvent(OWNER, message=FakeMessage("", photo=True), photo_bytes=data)
await dispatch(ctx.client, ev)
assert said in ev.texts[0] and OWNER in ctx.app.pending # and the prompt stays open for another try
assert await ctx.db.get_format_thumb(OWNER, auto["id"]) is None
stranger = await send_photo(ctx, sender=STRANGER)
assert stranger.out == [] and await autofmt(ctx, STRANGER) is None
async def test_the_buttons_of_the_old_caption_template_and_thumbnail_screens_do_nothing(ctx):
auto = await set_auto_thumb(ctx, b"\xff\xd8keep")
await edit_auto(ctx, template="{title}", caption="<i>{filename}</i>")
before = await autofmt(ctx)
old = ["m:cap", "in:template", "in:caption", "clr:caption"] + [f"th:{a}{b}" for a in ("view", "del", "rm") for b in ("", f":{auto['id']}")]
for data in old:
cb = await click(ctx, OWNER, data)
assert cb.edits == [] and cb.alerts == [] and not cb.deleted, data
assert not ctx.app.pending and ctx.client.files == []
assert await autofmt(ctx) == before and await auto_thumb(ctx) == b"\xff\xd8keep" # nothing was changed
# ---------------------------------------------------------------------------
# renaming
# ---------------------------------------------------------------------------
async def test_a_file_is_queued_with_the_template_name_and_a_cancel_button(ctx):
ev = await send_doc(ctx, OWNER, mid=10)
(job,) = ctx.app.queue.jobs.values()
assert job.new_name == EPISODE_NEW and (job.user_id, job.chat_id, job.msg_id) == (OWNER, OWNER, 10)
assert job.file_name == EPISODE and job.size == 50 * 1024 * 1024 and job.fmt is None and job.state == "queued"
text, buttons, kw = ev.out[0]
assert "Queued" in text and "starting soon" in text and EPISODE_NEW in text
assert flat_buttons(buttons) == [("✖ Cancel", f"job:x:{job.id}")]
assert job.status_msg is not None
ev = await send_doc(ctx, OWNER, "Another.File.mp4", mid=11)
assert "1 ahead of it" in ev.texts[0]
async def test_caption_mode_reads_the_caption(ctx):
await say(ctx, OWNER, "/mode caption")
await send_doc(ctx, OWNER, "xyz123.mkv", caption="Tokyo Revengers S02E07 720p", mid=12)
(job,) = ctx.app.queue.jobs.values()
assert job.new_name.endswith(".mkv") and "Tokyo Revengers" in job.new_name and "S02E07" in job.new_name
async def test_remove_words_are_applied_to_the_new_name(ctx):
await say(ctx, OWNER, "/removewords @SomeChannel")
await send_doc(ctx, OWNER, "[@SomeChannel] Tokyo Revengers - 05 (1080p).mkv")
(job,) = ctx.app.queue.jobs.values()
assert "SomeChannel" not in job.new_name and "Tokyo Revengers" in job.new_name
async def test_the_one_piece_style_name_gets_its_episode_number(ctx):
await edit_auto(ctx, template="One Piece - E{episode}")
ev = await send_doc(ctx, OWNER, "1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv", mid=21)
(job,) = ctx.app.queue.jobs.values()
assert job.new_name == "One Piece - E1147.mkv" and job.notes == []
assert "One Piece - E1147.mkv" in ev.texts[0] and "⚠️" not in ev.texts[0]
async def test_a_template_variable_that_cannot_be_filled_is_flagged(ctx):
await edit_auto(ctx, template="One Piece - E{episode}")
ev = await send_doc(ctx, OWNER, "Plain Movie.mkv")
(job,) = ctx.app.queue.jobs.values()
assert job.new_name == "One Piece - E.mkv"
assert "⚠️" in ev.texts[0] and "{episode}" in ev.texts[0] and "file name" in ev.texts[0] and "change the template" in ev.texts[0]
assert job.notes and "{episode}" in job.notes[0]
await say(ctx, OWNER, "/mode caption")
ev = await send_doc(ctx, OWNER, "Plain Movie.mkv", mid=22)
assert "in the caption" in ev.texts[0]
async def test_the_default_template_never_warns(ctx):
ev = await send_doc(ctx, OWNER, "Plain Movie.mkv")
assert "⚠️" not in ev.texts[0] # bracket groups vanish on purpose
class SlowEvent(FakeEvent):
"""A message whose reply takes a while, like a slow round trip to Telegram."""
def __init__(self, *a, delay=0.0, **kw):
super().__init__(*a, **kw)
self.delay = delay
async def reply(self, text=None, **kw):
await asyncio.sleep(self.delay)
return await super().respond(text, **kw)
async def test_jobs_are_ordered_by_the_message_that_triggered_them(ctx):
await send_doc(ctx, OWNER, "B.mkv", mid=31)
await send_doc(ctx, OWNER, "A.mkv", mid=30)
await send_doc(ctx, OWNER, "old.mkv", 1024, mid=34)
assert sorted(j.order for j in ctx.app.queue.jobs.values()) == [30, 31, 34]
assert [j.file_name for j in ctx.app.queue.for_user(OWNER)] == ["A.mkv", "B.mkv", "old.mkv"]
async def test_a_forwarded_batch_comes_out_in_the_order_it_was_sent(ctx):
"""Telethon handles messages that arrive together concurrently; the results must still be in order."""
ran = []
async def runner(job):
ran.append(job.file_name)
ctx.app.queue = JobQueue(runner, workers=1, settle=0.3)
ctx.app.queue.start()
await asyncio.sleep(0.02) # the worker is idle, waiting for files
names = {
1145: "1145 - One Piece [Dual] 720p @Anime_X_Universe.mkv",
1146: "1146 - One Piece [Dual] 720p @Anime_X_Universe.mkv",
1147: "1147 - One Piece [Dual] 720p @Anime_X_Universe.mkv",
}
delays = {1145: 0.10, 1146: 0.00, 1147: 0.05} # the replies finish in a different order than the files arrived
events = [
SlowEvent(OWNER, message=document_message(name, 1024, mid=mid), delay=delays[mid]) for mid, name in names.items()
]
await asyncio.gather(*(dispatch(ctx.client, ev) for ev in reversed(events)))
await asyncio.sleep(0.7)
assert ran == [names[1145], names[1146], names[1147]]
async def test_oversized_files_are_refused(ctx):
ev = await send_doc(ctx, OWNER, size=101 * 1024 * 1024)
assert "limit is 100.00 MB" in ev.texts[0] and not ctx.app.queue.jobs
ev = await send_doc(ctx, OWNER, size=100 * 1024 * 1024) # exactly at the limit is fine
assert "Queued" in ev.texts[0]
async def test_stickers_voice_notes_and_gifs_are_not_renamed(ctx):
for flags in ({"sticker": True}, {"voice": True}, {"gif": True}, {"video_note": True}):
ev = FakeEvent(OWNER, message=FakeMessage("", file=make_file("x.webm"), **flags))
await dispatch(ctx.client, ev)
assert ev.out == [], flags
assert not ctx.app.queue.jobs
assert is_renameable(document_message()) and not is_renameable(FakeMessage("text"))
async def test_a_file_without_a_name_gets_a_generated_one(ctx):
ev = FakeEvent(OWNER, message=FakeMessage("", mid=77, file=make_file(None, 1024, "video/mp4", ext=".mp4"), video=True))
await dispatch(ctx.client, ev)
(job,) = ctx.app.queue.jobs.values()
assert job.file_name == "video_77.mp4" and job.new_name.endswith(".mp4")
# ---------------------------------------------------------------------------
# queue and cancel
# ---------------------------------------------------------------------------
async def test_queue_listing_is_scoped_to_the_user_unless_owner(ctx):
assert "Nothing in the queue" in (await say(ctx, OWNER, "/queue")).texts[0]
await send_doc(ctx, OWNER, "Owner.File.mkv")
await send_doc(ctx, ADMIN_CFG, "Admin.File.mkv")
admin_view = (await say(ctx, ADMIN_CFG, "/queue")).texts[0]
assert "Admin File" in admin_view and "Owner File" not in admin_view
owner_view = (await say(ctx, OWNER, "/queue")).texts[0]
assert "Admin File" in owner_view and "Owner File" in owner_view and str(ADMIN_CFG) in owner_view
async def test_cancel_commands_respect_ownership(ctx):
await send_doc(ctx, OWNER, "Owner.File.mkv") # job 1
await send_doc(ctx, ADMIN_CFG, "Admin.File.mkv") # job 2
await send_doc(ctx, ADMIN_CFG, "Admin.Two.mkv") # job 3
assert "couldn't find" in (await say(ctx, ADMIN_CFG, "/cancel 1")).texts[0] # not theirs
assert ctx.app.queue.jobs[1].state == "queued"
assert "Cancelling 2 job(s)" in (await say(ctx, ADMIN_CFG, "/cancel all")).texts[0] # only their own
assert set(ctx.app.queue.jobs) == {1}
assert "no active jobs" in (await say(ctx, ADMIN_CFG, "/cancel")).texts[0]
await send_doc(ctx, ADMIN_CFG, "Admin.Three.mkv") # job 4
assert "Cancelling 2 job(s)" in (await say(ctx, OWNER, "/cancel all")).texts[0] # owner: jobs 1 and 4
assert not ctx.app.queue.jobs
async def test_cancelled_queued_jobs_update_their_status_message(ctx):
await send_doc(ctx, OWNER)
await say(ctx, OWNER, "/cancel 1")
for _ in range(5):
await asyncio.sleep(0)
(entity, text, kw), = ctx.client.edited
assert "Cancelled" in text and kw["buttons"] is None
async def test_cancel_button_on_a_queued_file(ctx):
await send_doc(ctx, OWNER)
await send_doc(ctx, OWNER, "Second.mkv")
cb = await click(ctx, ADMIN_CFG, "job:x:1") # somebody else's job
assert cb.toasts == ["That job already finished."] and 1 in ctx.app.queue.jobs
cb = await click(ctx, OWNER, "job:x:1")
assert cb.toasts == ["Cancelling…"] and 1 not in ctx.app.queue.jobs
assert (await click(ctx, OWNER, "job:x:1")).toasts == ["That job already finished."]
admin_doc = await send_doc(ctx, ADMIN_CFG, "Admin.mkv")
assert admin_doc.out
assert (await click(ctx, OWNER, "job:x:3")).toasts == ["Cancelling…"] # the owner may cancel anything
async def test_cancel_clears_a_pending_prompt_first(ctx):
ctx.app.pending[OWNER] = Pending("template", OWNER, 1, "rename", False, time.time() + 100)
await send_doc(ctx, OWNER)
ev = await say(ctx, OWNER, "/cancel")
assert "nothing was changed" in ev.texts[0] and OWNER not in ctx.app.pending
assert ctx.app.queue.jobs # the job is untouched
assert "Cancelling 1 job(s)" in (await say(ctx, OWNER, "/cancel")).texts[0]
# ---------------------------------------------------------------------------
# admin management, status and stats
# ---------------------------------------------------------------------------
async def test_owner_manages_admins(ctx):
ev = await say(ctx, OWNER, "/addadmin 555555")
assert "can now use the bot" in ev.texts[0] and 555555 in ctx.app.admins
assert 555555 in await ctx.db.list_admins()
assert (await say(ctx, 555555, "/help")).out # works immediately
assert "already have access" in (await say(ctx, OWNER, "/addadmin 555555")).texts[0]
assert "already have access" in (await say(ctx, OWNER, f"/addadmin {OWNER}")).texts[0]
assert "already have access" in (await say(ctx, OWNER, f"/addadmin {ADMIN_CFG}")).texts[0]
for bad in ("/addadmin", "/addadmin abc", "/addadmin 12", "/addadmin @user"):
assert "Usage" in (await say(ctx, OWNER, bad)).texts[0], bad
listing = (await say(ctx, OWNER, "/admins")).texts[0]
assert str(OWNER) in listing and "555555" in listing and str(ADMIN_CFG) in listing
assert "was removed" in (await say(ctx, OWNER, "/deladmin 555555")).texts[0]
assert 555555 not in ctx.app.admins and 555555 not in await ctx.db.list_admins()
assert (await say(ctx, 555555, "/help")).out == [] # access gone
assert "wasn't in the list" in (await say(ctx, OWNER, "/deladmin 555555")).texts[0]
assert "ADMIN_IDS" in (await say(ctx, OWNER, f"/deladmin {ADMIN_CFG}")).texts[0]
assert ADMIN_CFG in ctx.app.admins
assert "Usage" in (await say(ctx, OWNER, "/deladmin")).texts[0]
async def test_status_pings_telegram_and_reports_version_info(ctx):
ev = await say(ctx, OWNER, "/status")
text = ev.texts[0]
assert "Bot status" in text and "Uptime" in text and "Ping" in text and "Version" in text and "Telethon" in text
assert any(isinstance(r, functions.PingRequest) for r in ctx.client.requests)
assert (await say(ctx, ADMIN_CFG, "/status")).out # admins may use it too
async def test_status_shows_what_makes_transfers_fast_and_how_they_went(ctx, monkeypatch):
from bot import fasttransfer
monkeypatch.setattr(fasttransfer, "stats", fasttransfer.TransferStats(fast_down=4, fast_up=3, small_up=1))
text = (await say(ctx, OWNER, "/status")).texts[0]
assert "<b>Speed</b>" in text and "Encryption: cryptg ✅" in text and "Parallel transfer:" in text
assert "<b>Downloads:</b> 4 parallel · 0 standard" in text and "<b>Uploads:</b> 3 parallel · 1 up to 10 MB · 0 standard" in text
fasttransfer.stats.fell_back("upload", RuntimeError("boom"))
assert "failed 1×" in (await say(ctx, OWNER, "/status")).texts[0]
async def test_status_survives_a_failed_ping(ctx):
async def broken(request):
raise ConnectionError("offline")
ctx.client.__class__ = type("Broken", (FakeClient,), {"__call__": lambda self, request: broken(request)})
assert "n/a" in (await say(ctx, OWNER, "/status")).texts[0]
async def test_stats_reports_users_files_and_system_usage(ctx):
await say(ctx, ADMIN_CFG, "/help")
await ctx.db.record_job(OWNER, 3 * 1024 ** 3)
text = (await say(ctx, OWNER, "/stats")).texts[0]
for expected in ("System stats", "Users", "Files renamed", "3.00 GB", "Queue", "CPU", "RAM", "Disk"):
assert expected in text, expected
assert (await say(ctx, ADMIN_CFG, "/stats")).out
assert (await say(ctx, STRANGER, "/stats")).out == []
# ---------------------------------------------------------------------------
# /tracks
# ---------------------------------------------------------------------------
async def test_tracks_needs_a_reply_to_a_file(ctx):
for reply in (None, FakeMessage("text", mid=3)):
assert "Reply to a video or audio file" in (await say(ctx, OWNER, "/tracks", reply=reply)).texts[0]
@needs_ffmpeg
async def test_tracks_lists_numbered_audio_and_subtitles_and_cleans_up(ctx, media_dir, monkeypatch):
calls = []
async def fake_download(client, message, dest, **kw):
calls.append(kw)
shutil.copy(media_dir / "sample.mkv", dest)
monkeypatch.setattr(appmod.transfer, "download", fake_download)
reply = document_message("Show.mkv", (media_dir / "sample.mkv").stat().st_size, mid=33)
ev = await say(ctx, OWNER, "/tracks", reply=reply, mid=34)
assert ev.texts == ["🔎 Reading the tracks…"]
(status, text, kw), = ctx.client.edited
assert status.id == 1001 and "Show.mkv" in text
assert "Audio" in text and "Subtitles" in text and "Japanese" in text and "English" in text and "use these numbers" in text
assert len(calls) == 1 and calls[0]["max_bytes"] == appmod.TRACKS_PROBE_BYTES
assert not list(Path(ctx.cfg.temp_dir).glob("tracks*"))
@needs_ffmpeg
async def test_tracks_downloads_the_whole_file_when_the_start_is_not_enough(ctx, media_dir, monkeypatch):
calls = []
async def fake_download(client, message, dest, **kw):
calls.append(kw)
if "max_bytes" in kw:
Path(dest).write_bytes(b"\x00" * 4096) # e.g. an mp4 whose index is at the end
else:
shutil.copy(media_dir / "sample.mp4", dest)
monkeypatch.setattr(appmod.transfer, "download", fake_download)
reply = document_message("Show.mp4", 40 * 1024 * 1024, mid=33)
await say(ctx, OWNER, "/tracks", reply=reply, mid=34)
assert len(calls) == 2 and "max_bytes" not in calls[1]
assert "Audio" in ctx.client.edited[-1][1] and "needs the whole file" in ctx.client.edited[0][1]
@needs_ffmpeg
async def test_tracks_gives_up_gracefully(ctx, monkeypatch):
async def junk_download(client, message, dest, **kw):
Path(dest).write_bytes(b"\x00" * 4096)
monkeypatch.setattr(appmod.transfer, "download", junk_download)
for size in (2 * 1024 * 1024, 900 * 1024 * 1024): # small file / too big for a full download
ctx.client.edited.clear()
reply = document_message("Bad.mkv", size, mid=33)
await say(ctx, OWNER, "/tracks", reply=reply, mid=34)
assert "couldn't read any tracks" in ctx.client.edited[-1][1], size
assert not list(Path(ctx.cfg.temp_dir).glob("tracks*"))
# ---------------------------------------------------------------------------
# menus (callbacks)
# ---------------------------------------------------------------------------
async def test_menu_navigation_edits_the_message_in_place(ctx):
cb = await click(ctx, OWNER, "m:meta")
text, buttons = cb.edits[-1]
assert "metadata is currently" in text and "m:tool" in data_of(buttons)
assert cb.answers == [(None, False)]
cb = await click(ctx, OWNER, "m:tool")
assert "TOOL" in cb.edits[-1][0] and "m:meta" in data_of(cb.edits[-1][1])
cb = await click(ctx, OWNER, "m:home")
assert "Auto Rename Bot" in cb.edits[-1][0]
cb = await click(ctx, OWNER, "m:admin")
assert "ADMIN" in cb.edits[-1][0]
cb = await click(ctx, OWNER, "m:bogus")
assert cb.edits == [] and cb.answers == [(None, False)]
async def test_toggles_update_settings_and_refresh_the_panel(ctx):
cb = await click(ctx, OWNER, "set:mode:caption")
assert (await prefs(ctx))["mode"] == "caption" and cb.toasts == ["Mode: Caption"]
assert "Caption ✅" in [label for label, _d in flat_buttons(cb.edits[-1][1])]
cb = await click(ctx, OWNER, "set:ftype:video")
assert (await prefs(ctx))["file_type"] == "video" and cb.toasts == ["File type: Video"]
cb = await click(ctx, OWNER, "set:ftype:same")
assert cb.toasts == ["File type: Same as original"]
cb = await click(ctx, OWNER, "set:meta:on")
assert (await prefs(ctx))["metadata_on"] is True and cb.toasts == ["Metadata ON"]
await click(ctx, OWNER, "set:meta:off")
assert (await prefs(ctx))["metadata_on"] is False
before = await prefs(ctx)
for junk in ("set:mode:bogus", "set:nothing:x", "set:ftype:zip", "set:meta:maybe", "set:mode"):
cb = await click(ctx, OWNER, junk)
assert cb.edits == [] and cb.answers == [(None, False)], junk
assert await prefs(ctx) == before
async def test_clear_buttons(ctx):
await say(ctx, OWNER, "/keepaudio 2 1")
await say(ctx, OWNER, "/keepsubtitle 1")
cb = await click(ctx, OWNER, "clr:keep_audio")
s = await prefs(ctx)
assert s["keep_audio"] == [] and s["keep_subtitle"] == ["1"] and "TOOL" in cb.edits[-1][0]
await click(ctx, OWNER, "clr:keep_subtitle")
assert (await prefs(ctx))["keep_subtitle"] == []
cb = await click(ctx, OWNER, "clr:whatever")
assert cb.edits == []
async def test_close_deletes_the_message_and_falls_back_to_editing(ctx):
ctx.app.pending[OWNER] = Pending("remove", OWNER, 1, "rename", False, time.time() + 100)
cb = await click(ctx, OWNER, "x:close")
assert cb.deleted and OWNER not in ctx.app.pending
old = FakeCallback(OWNER, "x:close")
old.delete_error = RuntimeError("MESSAGE_DELETE_FORBIDDEN")
await dispatch(ctx.client, old)
assert old.edits == [("Closed.", None)] and not old.deleted
async def test_metadata_help_and_unknown_callbacks(ctx):
cb = await click(ctx, OWNER, "help:meta")
assert "How to set metadata" in cb.edits[-1][0]
for junk in ("", "zzz", "adm", "th:what", "job:x:abc", "in:nope"):
cb = await click(ctx, OWNER, junk)
assert cb.answers and not cb.alerts, junk
async def test_admin_panel_buttons(ctx):
await send_doc(ctx, OWNER)
await send_doc(ctx, OWNER, "Two.mkv")
cb = await click(ctx, OWNER, "adm:stats", chat=OWNER)
(chat, text, kw), = ctx.client.sent
assert chat == OWNER and "System stats" in text and "2 waiting" in text
await click(ctx, OWNER, "adm:status")
assert "Bot status" in ctx.client.sent[-1][1] and "<b>Speed</b>" in ctx.client.sent[-1][1]
cb = await click(ctx, OWNER, "adm:clear")
assert cb.alerts == ["Cancelling 2 job(s)."] and not ctx.app.queue.jobs
@pytest.mark.parametrize("panel", sorted(PANEL_NAMES))
async def test_every_button_of_every_panel_is_handled(ctx, panel):
await set_auto_thumb(ctx, b"\xff\xd8thumb")
await ctx.db.create_format(OWNER, "Other auto", kind="auto")
await ctx.db.create_format(OWNER, "Manual one", kind="manual")
_text, rows = await ctx.app.panel(OWNER, panel)
clicked = 0
for row in rows:
for _label, data in row:
cb = await click(ctx, OWNER, data)
assert cb.answers or cb.deleted, (panel, data)
assert not any("went wrong" in a for a in cb.alerts), (panel, data)
clicked += 1
assert clicked >= 1
@pytest.mark.parametrize("kind", sorted(panels.PROMPTS))
async def test_every_prompt_opens_and_cancels(ctx, kind):
cb = await click(ctx, OWNER, f"in:{kind}")
p = ctx.app.pending[OWNER]
assert p.kind == kind and p.back in PANEL_NAMES and not p.is_photo
text, buttons = cb.edits[-1]
assert text == panels.PROMPTS[kind] and data_of(buttons) == [f"in:cancel:{p.back}"]
cb = await click(ctx, OWNER, f"in:cancel:{p.back}")
assert OWNER not in ctx.app.pending and cb.toasts == ["Cancelled."] and cb.edits
# ---------------------------------------------------------------------------
# "tap a button, then send the value"
# ---------------------------------------------------------------------------
async def test_remove_words_prompt_flow(ctx):
await click(ctx, OWNER, "in:remove", msg_id=500)
ev = await say(ctx, OWNER, "@Chan, [Group]", mid=60)
assert ev.deleted and OWNER not in ctx.app.pending # the typed value is removed to keep the chat tidy
assert (await prefs(ctx))["remove_words"] == ["@Chan", "[Group]"]
(chat, text, kw), = ctx.client.edited
assert chat == OWNER and kw["message"] == 500 and "I'll strip" in text and "RENAME MODE" in text
assert "set:mode:filename" in data_of(kw["buttons"]) and "in:template" not in data_of(kw["buttons"])
async def test_keep_audio_prompt_flow_enables_metadata_and_returns_to_the_tool_panel(ctx):
await click(ctx, OWNER, "in:keep_audio")
await say(ctx, OWNER, "English Japanese")
s = await prefs(ctx)
assert s["keep_audio"] == ["english", "japanese"] and s["metadata_on"] is True
(chat, text, kw), = ctx.client.edited
assert "TOOL" in text and "English, Japanese" in text and "in:keep_subtitle" in data_of(kw["buttons"])
async def test_invalid_input_keeps_the_prompt_open(ctx):
await click(ctx, OWNER, "in:mt_title")
ev = await say(ctx, OWNER, "x" * 300)
assert "too long" in ev.texts[0] and "Try again" in ev.texts[0] and not ev.deleted
assert OWNER in ctx.app.pending and ctx.client.edited == []
ev = await say(ctx, OWNER, "My Title")
assert OWNER not in ctx.app.pending and (await prefs(ctx))["meta"]["title"] == "My Title"
async def test_expired_prompts_and_other_users_text_are_ignored(ctx):
await click(ctx, OWNER, "in:mt_title")
assert (await say(ctx, ADMIN_CFG, "Title")).out == [] # not their prompt
assert OWNER in ctx.app.pending
ctx.app.pending[OWNER].expires = time.time() - 1
ev = await say(ctx, OWNER, "Title")
assert ev.out == [] and not ev.deleted and OWNER not in ctx.app.pending
assert (await prefs(ctx))["meta"]["title"] == "" and (await prefs(ctx, ADMIN_CFG))["meta"]["title"] == "" # nothing was saved
async def test_plain_text_without_a_prompt_is_ignored(ctx):
ev = await say(ctx, OWNER, "hello bot")
assert ev.out == [] and not ev.deleted
async def test_prompt_on_a_photo_banner_message_is_flagged_and_long_updates_fall_back(ctx, monkeypatch):
await click(ctx, OWNER, "in:mt_title", photo=True)
assert ctx.app.pending[OWNER].is_photo and ctx.app.pending[OWNER].back == "meta"
monkeypatch.setattr(appmod, "visible_len", lambda _t: 5000) # pretend it would not fit a caption
await say(ctx, OWNER, "My Title")
(_chat, text, _kw), = ctx.client.edited
assert "Open the menu to see the update" in text and "My Title" in text
assert (await prefs(ctx))["meta"]["title"] == "My Title"
async def test_when_the_menu_message_cannot_be_edited_a_new_one_is_sent(ctx):
async def refuse(*a, **k):
raise RuntimeError("MESSAGE_ID_INVALID")
await click(ctx, OWNER, "in:remove")
ctx.client.edit_message = refuse
ev = await say(ctx, OWNER, "@Chan, [Group]")
assert (await prefs(ctx))["remove_words"] == ["@Chan", "[Group]"]
assert "I'll strip" in ev.texts[0] and ev.out[0][1] # sent as a fresh message with buttons
# ---------------------------------------------------------------------------
# robustness
# ---------------------------------------------------------------------------
async def test_handler_failures_are_reported_to_the_user(ctx, monkeypatch):
async def boom(*a, **k):
raise RuntimeError("database is down")
monkeypatch.setattr(ctx.db, "get_settings", boom)
ev = await say(ctx, OWNER, "/start")
assert ev.texts == ["⚠️ Something went wrong. Please try again."]
cb = await click(ctx, OWNER, "m:meta")
assert cb.alerts == ["⚠️ Something went wrong."]
# and the bot keeps working afterwards
monkeypatch.undo()
assert "Auto Rename Bot" in (await say(ctx, OWNER, "/start")).texts[0]
async def test_handler_failures_reach_the_log_channel_with_the_update_and_the_traceback(ctx, monkeypatch):
channel_log = errorlog.install(ctx.app._log_send, errorlog.secrets_from(ctx.cfg)) # the root logger, as in main.py
async def boom(*a, **k):
raise RuntimeError("database is down")
monkeypatch.setattr(ctx.db, "get_settings", boom)
ev = await say(ctx, OWNER, "/start")
await channel_log.aclose()
assert ev.texts == ["⚠️ Something went wrong. Please try again."]
(_chat, text, kw), = [s for s in ctx.client.sent if s[0] == LOG]
assert kw["parse_mode"] == "html"
assert text.startswith(
'<pre><code class="language-python">Error handling update:\n'
f"FakeEvent(sender_id={OWNER}, chat_id={OWNER}, message_id=1, text='/start')\n\nTraceback (most recent call last):"
)
assert text.endswith("RuntimeError: database is down</code></pre>")
async def test_an_error_inside_a_helper_still_names_the_update_that_started_it(ctx, monkeypatch):
channel_log = errorlog.install(ctx.app._log_send)
async def boom(*a, **k):
raise RuntimeError("telegraph exploded")
monkeypatch.setattr(ctx.app, "_mediainfo", boom)
reply = document_message("Movie.mkv", 5 * 1024 * 1024)
await say(ctx, OWNER, "/mediainfo", reply=reply)
await channel_log.aclose()
(_chat, text, _kw), = [s for s in ctx.client.sent if s[0] == LOG]
assert text.startswith('<pre><code class="language-python">/mediainfo failed\nFakeEvent(sender_id=')
assert "text='/mediainfo'" in text and text.endswith("RuntimeError: telegraph exploded</code></pre>")
async def test_a_file_is_handled_by_exactly_one_handler(ctx):
ctx.app.pending[OWNER] = Pending("template", OWNER, 1, "rename", False, time.time() + 100)
ev = await send_doc(ctx, OWNER)
assert len(ev.out) == 1 and "Queued" in ev.texts[0]
assert OWNER in ctx.app.pending # a file does not feed the text prompt
|