diff --git a/python/exporter/cli.py b/python/exporter/cli.py index 245a8ef6..aef66bde 100644 --- a/python/exporter/cli.py +++ b/python/exporter/cli.py @@ -480,9 +480,15 @@ async def sign_in(arguments: argparse.Namespace): # Named as it will actually appear. The model and OS come from FindMy.py, which presents as a # MacBook Pro, so somebody who was only told the serial goes looking for a device they own. + # + # **Read off the account rather than written here.** Serials are drawn per install, so a + # literal in this sentence names a device the user does not have and will not find - which is + # worse than saying nothing, because they go looking and conclude the entry is somebody + # else's. The same mistake as the screen this replaced in the Android app. print("\nSigning in registers this exporter as a device on your Apple account. It appears in", file=sys.stderr) - print("your device list as a MacBook Pro on macOS 13.4.1, serial 0PENTAGXPORT - that is this", + print("your device list as a MacBook Pro on macOS 13.4.1, serial " + f"{account.serial} - that is this", file=sys.stderr) print("program, not a Mac you own. Remove it any time at account.apple.com > Devices.\n", file=sys.stderr) diff --git a/python/exporter/wizard.py b/python/exporter/wizard.py index f02e1117..f8164ff9 100644 --- a/python/exporter/wizard.py +++ b/python/exporter/wizard.py @@ -466,10 +466,14 @@ async def _read_icloud(self, asker: Asker): # Said before the password is asked for, and naming the entry as it will appear: the # model and OS come from FindMy.py, which presents as a MacBook Pro, so somebody told # only the serial goes looking for a Mac they own. + # + # **Read off the account rather than written here.** Serials are drawn per install, so + # a literal names a device this user does not have, and they go looking for it and + # conclude the entry belongs to somebody else. asker.ask(lambda: messagebox.showinfo( "Signing in registers a device", "Signing in adds an entry to your Apple account's device list: a MacBook Pro on" - " macOS 13.4.1, serial 0PENTAGXPORT.\n\n" + f" macOS 13.4.1, serial {account.serial}.\n\n" "That is this program, not a Mac you own. You can remove it at any time at" " account.apple.com under Devices, from any browser.", )) diff --git a/python/test/test_cli.py b/python/test/test_cli.py index 7bc33b2c..0577d26a 100644 --- a/python/test/test_cli.py +++ b/python/test/test_cli.py @@ -285,8 +285,19 @@ async def _nothing_ticked(*_args, **_kwargs): class FakeAccount: """Enough of an `AsyncAppleAccount` for the sign-in flow: it can be closed, and it says so.""" + SERIAL = "0PENTAGXK7QX" + """ + A drawn serial, deliberately not `0PENTAGXPORT`. + + The notice printed before the password prompt used to write the constant into the sentence by + hand, and this fake had no `serial` at all - so nothing could tell the difference between the + sentence naming the account and the sentence naming a literal. It named a literal for as long + as serials were constant, and kept naming it afterwards. + """ + def __init__(self) -> None: self.closed = False + self.serial = FakeAccount.SERIAL async def close(self) -> None: self.closed = True @@ -348,6 +359,23 @@ class TestRememberingTheDevice: not recognise. See `exporter.device`. """ + def test_the_notice_names_the_serial_this_run_presents(self, apple, capsys): + """ + The sentence shown before the password prompt tells the user what to look for. + + **It used to be a literal**, written when every install shared one serial and left there + when they stopped. A user on a drawn serial was told to look for `0PENTAGXPORT`, would not + find it in their device list, and would reasonably conclude the entry they did find + belonged to somebody else - which is the belief that gets it removed, taking the session + with it. The same defect was fixed on the Android side's registered-device screen. + """ + asyncio.run(cli.sign_in(signing_in())) + + notice = capsys.readouterr().err + + assert FakeAccount.SERIAL in notice + assert "0PENTAGXPORT" not in notice + def test_an_ordinary_sign_in_is_remembered(self, apple): # The path almost every run takes, and the one that was storing nothing: the call sat in # the terms handler, so only an account with unaccepted terms kept its identity.