Spaces:
Paused
Paused
Download tests/test_shift_extras.py from Mega-Evolutions/ButtonBot: direct link, hf CLI and curl.
- Browser
- Download file 28.9 kB
-
https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_shift_extras.py
- Command line
-
hf download hf://spaces/Mega-Evolutions/ButtonBot/tests/test_shift_extras.py
-
curl -L -o test_shift_extras.py https://huggingface.co/spaces/Mega-Evolutions/ButtonBot/resolve/main/tests/test_shift_extras.py
28.9 kB
| """/shift -c (links to the source's posts follow the copies, in every connected channel) and -all (the destination takes | |
| over the source's name, description and photo).""" | |
| import asyncio | |
| from telethon import functions, types | |
| from app.crosslinks import relink_after_shift, restore_after_shift_undo | |
| from app.profile_copy import copy_profile, profile_lines | |
| from app.shift_engine import delete_shift_copies, run_shift, shift_profile, source_of | |
| from app.sync_engine import SyncOptions, live_fields, sync_channel | |
| from app.tgutil import button_url | |
| from .dbutil import db_url_for, fresh_db | |
| from .fakes import FakeChannelClient, make_msg, markup, rpc, url_btn | |
| from .harness import norm | |
| from .test_shift import SRC, shift_app, two_channels | |
| def run(coro): | |
| return asyncio.run(coro) | |
| T1 = "Watch https://t.me/srcch/3 here" | |
| def other_posts(): | |
| """A third connected channel with every kind of link to the source's posts.""" | |
| return { | |
| 1: make_msg(1, T1, [types.MessageEntityBold(T1.index("here"), 4)], out=True), | |
| 2: make_msg(2, "Fifth", [types.MessageEntityTextUrl(0, 5, "https://t.me/srcch/5?single")], out=True), | |
| 3: make_msg( | |
| 3, "Pick", | |
| markup=markup([url_btn("Second", "https://t.me/srcch/2"), url_btn("Site", "https://example.com/x")]), out=True, | |
| ), | |
| 4: make_msg(4, "Private https://t.me/c/1/8 form", out=True), | |
| 5: make_msg(5, "Never shifted https://t.me/srcch/99", out=True), # no copy of 99: stays | |
| 6: make_msg(6, "Already there https://t.me/dstch/3", out=True), # a link to the destination: stays | |
| 7: make_msg(7, "By a friend https://t.me/srcch/1", out=False), | |
| 8: make_msg(8, "No link", out=True), | |
| } | |
| async def extras_setup(tmp_path, *marks, first=1, last=8): | |
| """source (id 1, registered), destination (id 2) and another channel (id 3), a shift s1 with the given marks.""" | |
| src, dst = two_channels() | |
| other = src.add_channel(FakeChannelClient(other_posts(), username="otherch", cid=3, title="Other")) | |
| dst.msgs[5] = make_msg(5, "Old post: https://t.me/srcch/1", out=True) # the destination had a link already | |
| db = await fresh_db(db_url_for(tmp_path)) | |
| await db.save_channel(1, 5, "Anime Source", "srcch", 1) | |
| await db.save_channel(2, 6, "Anime Backup", "dstch", 1) | |
| await db.save_channel(3, 7, "Other", "otherch", 1) | |
| for m in other.msgs.values(): | |
| if m.out: | |
| await db.create_post( | |
| channel_id=3, message_id=m.id, status="sent", source="bot", created_by=1, link_preview=False, **live_fields(m) | |
| ) | |
| await db.create_shift("s1", src=SRC, dst_channel_id=2, first_id=first, last_id=last, user_id=1) | |
| for mark in marks: | |
| await db.set_mark("s1", mark) | |
| return src, dst, other, db, await db.get_channel(2), await db.get_shift("s1") | |
| def edited(chan): | |
| return [r.id for r in chan.requests if isinstance(r, functions.messages.EditMessageRequest)] | |
| # ===================================================================================================== -c | |
| def test_links_to_the_source_follow_the_copies_in_every_connected_channel(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink") | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert (res.copied_units, res.failed, res.aborted, res.cross_error) == (6, [], None, None) | |
| # copies: 1->11 2->12 3->13 5->14 6->15 7->16 8->17 | |
| assert res.phase == "links" and res.cross is not None and res.cross.clean | |
| # a typed link, with the bold word after it moved along (the link got one character longer) | |
| assert other.msgs[1].message == "Watch https://t.me/dstch/13 here" | |
| bold = other.msgs[1].entities[0] | |
| assert other.msgs[1].message[bold.offset : bold.offset + bold.length] == "here" | |
| # a hyperlink behind text keeps its text and its ?single | |
| link = other.msgs[2].entities[0] | |
| assert link.url == "https://t.me/dstch/14?single" and (link.offset, link.length) == (0, 5) | |
| # a button changes, the other button stays | |
| row = other.msgs[3].reply_markup.rows[0].buttons | |
| assert button_url(row[0]) == "https://t.me/dstch/12" and row[0].text == "Second" | |
| assert button_url(row[1]) == "https://example.com/x" | |
| # the private form of the link, and a post made by somebody else (the bot may edit those here) | |
| assert other.msgs[4].message == "Private https://t.me/dstch/17 form" | |
| assert other.msgs[7].message == "By a friend https://t.me/dstch/11" | |
| # nothing else was touched: no copy / a link to the destination / no link | |
| assert other.msgs[5].message == "Never shifted https://t.me/srcch/99" | |
| assert other.msgs[6].message == "Already there https://t.me/dstch/3" and other.msgs[8].message == "No link" | |
| assert edited(other) == [1, 2, 3, 4, 7] | |
| # the destination's own older post was fixed too | |
| assert dst.msgs[5].message == "Old post: https://t.me/dstch/11" | |
| # the source is only read - although the bot knows it as one of its channels and its posts link to each other | |
| assert edited(src) == [] and src.delete_requests == [] | |
| assert src.msgs[8].message == "See https://t.me/srcch/1 and https://t.me/c/1/2 and https://t.me/elsewhere/9" | |
| # My posts follows what the channel shows | |
| row1 = await db.find_by_message(3, 1) | |
| assert row1.text == other.msgs[1].message | |
| assert await db.has_mark("s1", "links") | |
| assert sorted(c.channel.id for c in res.cross.changed_channels) == [2, 3] | |
| await db.close() | |
| run(go()) | |
| async def older_posts_setup(tmp_path, *, edit_right=True): | |
| """Like extras_setup, but the third channel's posts are all older ones made by somebody else before the bot was added: | |
| the sync has read them into My posts ("adopted"), nothing is registered as made by the bot.""" | |
| src, dst = two_channels() | |
| posts = other_posts() | |
| for m in posts.values(): | |
| m.out = False | |
| other = src.add_channel(FakeChannelClient(posts, username="otherch", cid=3, title="Other")) | |
| if not edit_right: | |
| other.rights = dict(admin=True, post=True, edit=False, delete=True, invite=False, add_admins=False) | |
| dst.msgs[5] = make_msg(5, "Old post: https://t.me/srcch/1", out=True) | |
| db = await fresh_db(db_url_for(tmp_path)) | |
| await db.save_channel(1, 5, "Anime Source", "srcch", 1) | |
| await db.save_channel(2, 6, "Anime Backup", "dstch", 1) | |
| await db.save_channel(3, 7, "Other", "otherch", 1) | |
| rep = await sync_channel(src, db, await db.get_channel(3), SyncOptions(pause=0, grace=0)) # reads the older posts in | |
| assert rep.imported == 8 and {p.source for p in await db.sent_posts(3)} == {"adopted"} | |
| await db.create_shift("s1", src=SRC, dst_channel_id=2, first_id=1, last_id=8, user_id=1) | |
| await db.set_mark("s1", "relink") | |
| return src, dst, other, db, await db.get_channel(2), await db.get_shift("s1") | |
| def test_links_in_the_older_posts_the_sync_read_into_my_posts_follow_the_copies_too(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await older_posts_setup(tmp_path) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert (res.copied_units, res.failed, res.aborted, res.cross_error) == (6, [], None, None) | |
| assert res.cross is not None and res.cross.clean | |
| # the links in posts that were in the channel before the bot was added: every kind is changed | |
| assert other.msgs[1].message == "Watch https://t.me/dstch/13 here" | |
| assert other.msgs[2].entities[0].url == "https://t.me/dstch/14?single" | |
| assert button_url(other.msgs[3].reply_markup.rows[0].buttons[0]) == "https://t.me/dstch/12" | |
| assert button_url(other.msgs[3].reply_markup.rows[0].buttons[1]) == "https://example.com/x" | |
| assert other.msgs[4].message == "Private https://t.me/dstch/17 form" | |
| assert other.msgs[7].message == "By a friend https://t.me/dstch/11" | |
| assert other.msgs[5].message == "Never shifted https://t.me/srcch/99" and other.msgs[8].message == "No link" | |
| assert edited(other) == [1, 2, 3, 4, 7] | |
| # My posts follows: the saved copies are the posts of others, now with the new links | |
| for mid in (1, 2, 3, 4, 7): | |
| row = await db.find_by_message(3, mid) | |
| assert row.source == "adopted" and row.created_by == 0 | |
| assert row.text == other.msgs[mid].message | |
| assert (await db.find_by_message(3, 3)).buttons[0][0]["u"] == "https://t.me/dstch/12" | |
| assert (await db.find_by_message(3, 2)).entities[0]["u"] == "https://t.me/dstch/14?single" | |
| assert (await db.find_by_message(3, 5)).text == "Never shifted https://t.me/srcch/99" | |
| # nothing is left for the sync to "find": the saved copies and the channel agree | |
| rep = await sync_channel(src, db, await db.get_channel(3), SyncOptions(pause=0, grace=0)) | |
| assert rep.edited == [] and rep.deleted == [] and rep.adopted == [] | |
| await db.close() | |
| run(go()) | |
| def test_older_posts_of_others_need_the_edit_right_and_the_report_counts_the_ones_left(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await older_posts_setup(tmp_path, edit_right=False) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.cross is not None and res.cross.not_allowed == 5 and not res.cross.clean | |
| assert edited(other) == [] # Telegram would refuse: nothing is tried, nothing is changed | |
| assert other.msgs[1].message == "Watch https://t.me/srcch/3 here" | |
| assert (await db.find_by_message(3, 1)).text == "Watch https://t.me/srcch/3 here" | |
| assert dst.msgs[5].message == "Old post: https://t.me/dstch/11" # a post of the bot itself is still fixed | |
| await db.close() | |
| run(go()) | |
| def test_without_the_c_option_only_the_copies_themselves_are_fixed(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.cross is None and res.profile is None | |
| assert edited(other) == [] and other.msgs[1].message == T1 | |
| assert dst.msgs[5].message == "Old post: https://t.me/srcch/1" # an older post of the destination stays | |
| assert not await db.has_mark("s1", "links") | |
| await db.close() | |
| run(go()) | |
| def test_only_the_posts_that_were_shifted_have_their_links_changed(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink", first=1, last=2) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| # only the posts 1 and 2 were shifted (copies 11 and 12): two posts of the other channel and the old post of the | |
| # destination link to them | |
| assert res.copied_units == 2 and res.cross.relinked == 3 | |
| assert other.msgs[7].message == "By a friend https://t.me/dstch/11" | |
| assert button_url(other.msgs[3].reply_markup.rows[0].buttons[0]) == "https://t.me/dstch/12" | |
| assert dst.msgs[5].message == "Old post: https://t.me/dstch/11" | |
| assert edited(other) == [3, 7] | |
| # the links to posts that were not shifted stay as they are | |
| assert other.msgs[1].message == T1 | |
| assert other.msgs[2].entities[0].url == "https://t.me/srcch/5?single" | |
| assert other.msgs[4].message == "Private https://t.me/c/1/8 form" | |
| await db.close() | |
| run(go()) | |
| def test_the_links_can_be_updated_again_and_nothing_changes_twice(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink") | |
| await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| n = len(edited(other)) | |
| again = await relink_after_shift(src, db, shift, source_of(shift), dst_row, delay=0, settle_pause=0, pause=0) | |
| assert again.relinked == 0 and again.clean and len(edited(other)) == n | |
| late = other._new("Late: https://t.me/srcch/6", None, None) | |
| late.out = False | |
| again = await relink_after_shift(src, db, shift, source_of(shift), dst_row, delay=0, settle_pause=0, pause=0) | |
| assert again.relinked == 1 and other.msgs[late.id].message == "Late: https://t.me/dstch/15" | |
| await db.close() | |
| run(go()) | |
| def test_undo_points_the_links_back_at_the_source(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink") | |
| before = {i: (m.message, [e.to_dict() for e in (m.entities or [])]) for i, m in other.msgs.items()} | |
| await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| back = await restore_after_shift_undo(src, db, shift, source_of(shift), dst_row, delay=0, settle_pause=0, pause=0) | |
| assert back.relinked == 6 and back.clean # the five posts of the other channel and the old post of the destination | |
| for i, (text, ents) in before.items(): | |
| if i == 4: # the private form t.me/c/1/8 comes back as the public form of the same post (the source has a name) | |
| assert other.msgs[i].message == "Private https://t.me/srcch/8 form" | |
| continue | |
| assert other.msgs[i].message == text and [e.to_dict() for e in (other.msgs[i].entities or [])] == ents, i | |
| assert button_url(other.msgs[3].reply_markup.rows[0].buttons[0]) == "https://t.me/srcch/2" | |
| assert dst.msgs[5].message == "Old post: https://t.me/srcch/1" | |
| res = await delete_shift_copies(src, db, dst_row, shift, delay=0) | |
| assert res.remaining == 0 and res.deleted == 7 | |
| await db.close() | |
| run(go()) | |
| def test_an_undo_without_c_reads_nothing(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path) | |
| await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| mark = len(other.requests) | |
| back = await restore_after_shift_undo(src, db, shift, source_of(shift), dst_row, delay=0, settle_pause=0, pause=0) | |
| assert back.channels == [] and len(other.requests) == mark | |
| await db.close() | |
| run(go()) | |
| # ================================================================================================== -all | |
| def prepare_profiles(src, dst): | |
| src.about, dst.about = "Source about", "old about" | |
| src.set_photo(b"SRC-PHOTO") | |
| dst.set_photo(b"OLD-PHOTO") | |
| def notices(chan): | |
| return [m for m in chan.msgs.values() if isinstance(m, types.MessageService)] | |
| def test_the_destination_takes_over_name_description_and_photo(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| prepare_profiles(src, dst) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.profile is not None and res.profile.ok and res.profile_error is None | |
| assert res.profile.outcome == {"name": ("done", "Anime Source"), "description": ("done", ""), "photo": ("done", "")} | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "Source about", b"SRC-PHOTO") | |
| # the source was only read | |
| assert (src.title, src.about, src.photo_bytes()) == ("Anime Source", "Source about", b"SRC-PHOTO") | |
| assert not [r for r in src.requests if type(r).__name__.startswith("Edit")] | |
| # the "name changed" / "photo changed" notices Telegram adds are deleted again | |
| assert notices(dst) == [] | |
| # the bot's own list knows the new name | |
| assert (await db.get_channel(2)).title == "Anime Source" | |
| assert await db.has_mark("s1", "profile_done") | |
| # the posts were copied as usual, and -c was not asked for | |
| assert res.copied_units == 6 and res.cross is None | |
| await db.close() | |
| run(go()) | |
| def test_it_is_done_once_not_again_when_a_stopped_shift_continues(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| prepare_profiles(src, dst) | |
| calls = [] | |
| async def prog(res): | |
| calls.append(res.copied_units) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0, progress=prog, should_stop=lambda: len(calls) >= 2) | |
| assert res.stopped and res.profile is None and dst.title == "Anime Backup" # not before everything is copied | |
| again = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert again.profile is not None and again.profile.ok and dst.title == "Anime Source" | |
| titles = [r for r in dst.requests if isinstance(r, functions.channels.EditTitleRequest)] | |
| third = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) # nothing left to do | |
| assert third.profile is None | |
| assert [r for r in dst.requests if isinstance(r, functions.channels.EditTitleRequest)] == titles | |
| await db.close() | |
| run(go()) | |
| def test_a_part_the_source_does_not_have_is_left_as_the_destination_has_it(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| dst.title = "Anime Source" # the same name already | |
| dst.about = "my own description" | |
| dst.set_photo(b"MY-OWN-PHOTO") # the source has neither a description nor a photo | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.profile.outcome == {"name": ("same", ""), "description": ("none", ""), "photo": ("none", "")} | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "my own description", b"MY-OWN-PHOTO") | |
| lines = "\n".join(profile_lines(res.profile)) | |
| assert "the source has none - left as it is" in lines and "already the same" in lines | |
| await db.close() | |
| run(go()) | |
| def test_a_missing_admin_right_is_named_and_the_parts_can_be_tried_again(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| prepare_profiles(src, dst) | |
| dst.rights = dict(admin=True, post=True, edit=True, delete=True, invite=False, add_admins=False, change_info=False) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.copied_units == 6 and res.failed == [] # the posts are there | |
| assert not res.profile.ok and [p for p, _ in res.profile.failed] == ["name", "description", "photo"] | |
| text = "\n".join(profile_lines(res.profile)) | |
| assert "Change channel info" in text and "Not copied" in text | |
| assert dst.title == "Anime Backup" and dst.about == "old about" and dst.photo_bytes() == b"OLD-PHOTO" | |
| assert not await db.has_mark("s1", "profile_done") | |
| # the right is given: the same run again (the "copy again" button) does it | |
| dst.rights["change_info"] = True | |
| holder = type(res)() | |
| await shift_profile(src, db, shift, dst_row, 1, holder) | |
| assert holder.profile.ok and (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "Source about", b"SRC-PHOTO") | |
| assert await db.has_mark("s1", "profile_done") | |
| await db.close() | |
| run(go()) | |
| def test_one_part_failing_does_not_stop_the_others(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| prepare_profiles(src, dst) | |
| dst.fail["EditPhotoRequest"] = rpc("PhotoCropSizeSmallError") | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.profile.outcome["name"][0] == "done" and res.profile.outcome["description"][0] == "done" | |
| assert res.profile.outcome["photo"] == ("failed", "PhotoCropSizeSmallError") | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "Source about", b"OLD-PHOTO") | |
| assert not await db.has_mark("s1", "profile_done") # so the button to try again is offered | |
| assert (await db.get_channel(2)).title == "Anime Source" # what was changed is known to the bot | |
| await db.close() | |
| run(go()) | |
| def test_an_unreadable_source_fails_every_part_and_changes_nothing(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "profile") | |
| prepare_profiles(src, dst) | |
| src.fail["GetFullChannelRequest"] = rpc("ChannelPrivateError") | |
| res = await copy_profile(src, source_of(shift), dst_row) | |
| assert [p for p, _ in res.failed] == ["name", "description", "photo"] and "can't be read" in res.failed[0][1] | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Backup", "old about", b"OLD-PHOTO") | |
| await db.close() | |
| run(go()) | |
| def test_both_extras_together(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink", "profile") | |
| prepare_profiles(src, dst) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert res.profile.ok and res.cross.relinked == 6 and res.cross_error is None and res.profile_error is None | |
| assert other.msgs[1].message == "Watch https://t.me/dstch/13 here" and dst.title == "Anime Source" | |
| await db.close() | |
| run(go()) | |
| # ========================================================================================= the owner's view | |
| async def extras_app(tmp_path): | |
| app, dst = await shift_app(tmp_path) | |
| other = app.tg.add_channel(FakeChannelClient(other_posts(), username="otherch", cid=3, title="Other")) | |
| await app.db.save_channel(3, 7, "Other", "otherch", 1) | |
| dst.msgs[5] = make_msg(5, "Old post: https://t.me/srcch/1", out=True) | |
| app.tg.about, dst.about = "Source about", "old about" | |
| app.tg.set_photo(b"SRC-PHOTO") | |
| dst.set_photo(b"OLD-PHOTO") | |
| return app, dst, other | |
| def labels(app): | |
| return [t for row in app.out.last_buttons() for t in row] | |
| def test_the_plan_names_the_extras_that_were_asked_for(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| await app.text("/shift @srcch @dstch") | |
| plain = norm(app.out.last_text) | |
| assert "-c" not in plain and "-all" not in plain | |
| await app.text("/shift @srcch @dstch -c") | |
| text = norm(app.out.last_text) | |
| assert "-c: afterwards every link to a post of Anime Source" in text and "in your 2 other connected channel(s)" in text | |
| assert "Anime Source itself is not changed" in text and "-all" not in text | |
| await app.text("/shift @srcch @dstch - all") # typed with a space | |
| text = norm(app.out.last_text) | |
| assert "-all: afterwards Anime Backup gets the name, description and profile photo of Anime Source" in text | |
| assert "undo does not bring them back" in text and "-c:" not in text and "Change channel info" not in text | |
| await app.text("/shift @srcch @dstch 2 5 -all -c") | |
| text = norm(app.out.last_text) | |
| assert "Post ids 2-5" in text and "-c: afterwards" in text and "-all: afterwards" in text | |
| assert dst.title == "Anime Backup" and edited(other) == [] # a plan changes nothing | |
| await app.db.close() | |
| run(go()) | |
| def test_the_plan_warns_when_the_info_right_is_missing(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| dst.rights = dict(admin=True, post=True, edit=True, delete=True, invite=False, add_admins=False, change_info=False) | |
| await app.text("/shift @srcch @dstch -all") | |
| assert "does not have the “Change channel info” admin right in Anime Backup yet" in norm(app.out.last_text) | |
| await app.db.close() | |
| run(go()) | |
| def test_an_unknown_option_is_explained(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| await app.text("/shift @srcch @dstch -x") | |
| assert "Unknown option -x" in norm(app.out.last_text) and "-c, -all and --no-media" in norm(app.out.last_text) | |
| assert dst.title == "Anime Backup" | |
| await app.db.close() | |
| run(go()) | |
| def test_the_whole_flow_with_both_extras_then_undo(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| await app.text("/shift @srcch @dstch -c -all") | |
| await app.press(app.out.callback_data("Start copying")) | |
| done = norm(app.out.last_text) | |
| assert "7 message(s) copied" in done | |
| assert "link(s) in 6 post(s) of 2 channel(s) now point at the new copies" in done | |
| assert "Other: 5 link(s) in 5 post(s)" in done | |
| assert "name copied, description copied, profile photo copied" in done | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "Source about", b"SRC-PHOTO") | |
| assert other.msgs[1].message == "Watch https://t.me/dstch/13 here" | |
| shown = labels(app) | |
| assert any("Update links again" in t for t in shown) and any("Undo" in t for t in shown) | |
| assert not any("again" in t and "photo" in t for t in shown) # the profile was copied: no button for it | |
| # "update links again": nothing new, so nothing to change | |
| await app.press(app.out.callback_data("Update links again")) | |
| assert "no link there needed changing" in norm(app.out.last_text) | |
| # undo: the links go back to the source's posts first, then the copies are removed | |
| await app.press(app.out.callback_data("Undo")) | |
| await app.press(app.out.callback_data("Yes, remove")) | |
| text = norm(app.out.last_text) | |
| assert "7 copied message(s) removed from" in text and "6 post(s) of 2 channel(s) point back at the original posts" in text | |
| assert other.msgs[1].message == T1 and other.msgs[7].message == "By a friend https://t.me/srcch/1" | |
| assert dst.msgs[5].message == "Old post: https://t.me/srcch/1" | |
| assert dst.title == "Anime Source" # the name is not taken back (the plan said so) | |
| await app.db.close() | |
| run(go()) | |
| def test_a_failed_profile_gets_its_own_button(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| dst.rights = dict(admin=True, post=True, edit=True, delete=True, invite=False, add_admins=False, change_info=False) | |
| await app.text("/shift @srcch @dstch -all") | |
| await app.press(app.out.callback_data("Start copying")) | |
| text = norm(app.out.last_text) | |
| assert "7 message(s) copied" in text and "Not copied" in text and "Change channel info" in text | |
| assert any("Copy name, description, photo again" in t for t in labels(app)) | |
| dst.rights["change_info"] = True | |
| await app.press(app.out.callback_data("Copy name, description, photo again")) | |
| text = norm(app.out.last_text) | |
| assert "name copied, description copied, profile photo copied" in text | |
| assert (dst.title, dst.about, dst.photo_bytes()) == ("Anime Source", "Source about", b"SRC-PHOTO") | |
| assert not any("photo again" in t for t in labels(app)) # done now | |
| await app.db.close() | |
| run(go()) | |
| def test_the_extra_buttons_only_exist_for_the_extras_that_were_asked_for(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| await app.text("/shift @srcch @dstch") | |
| await app.press(app.out.callback_data("Start copying")) | |
| shown = " | ".join(labels(app)) | |
| assert "Update links again" not in shown and "photo again" not in shown and "Undo" in shown | |
| assert edited(other) == [] | |
| await app.db.close() | |
| run(go()) | |
| def test_the_extras_that_were_asked_for_are_remembered_with_the_shift(tmp_path): | |
| async def go(): | |
| app, dst, other = await extras_app(tmp_path) | |
| await app.text("/shift @srcch @dstch -c") | |
| await app.press(app.out.callback_data("Start copying")) | |
| sid = app.out.callback_data("Undo").split(":")[1] | |
| assert await app.db.marks_of(sid) == {"relink", "links"} | |
| await app.db.close() | |
| run(go()) | |
| def test_a_stopped_shift_that_is_continued_still_updates_the_links(tmp_path): | |
| async def go(): | |
| src, dst, other, db, dst_row, shift = await extras_setup(tmp_path, "relink") | |
| calls = [] | |
| async def prog(res): | |
| calls.append(res.copied_units) | |
| res = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0, progress=prog, should_stop=lambda: len(calls) >= 2) | |
| assert res.stopped and res.cross is None and edited(other) == [] # the links wait until everything is copied | |
| again = await run_shift(src, db, shift, dst_row, 99, delay=0, settle_pause=0) | |
| assert not again.stopped and again.cross is not None and again.cross.relinked == 6 | |
| assert other.msgs[1].message == "Watch https://t.me/dstch/13 here" | |
| await db.close() | |
| run(go()) | |