ButtonBot / tests /test_shift_extras.py
github-actions[bot]
Automated sync from GitHub
c53c2ab
Raw History Blame Contribute Delete
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())