From c2fb935ae9faf0738a7817cb0478db1a5a49db72 Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Sun, 13 Sep 2026 14:27:24 +0200 Subject: [PATCH] Stop the exporter naming a serial it is not sending Both the CLI and the wizard print "serial 0PENTAGXPORT" in the notice shown before the password prompt. It is a literal, written when every install shared one serial, and left behind when they stopped. An install that drew its own is told to look for a device it will not find - and the row it does find then looks like somebody else's, which is the belief that gets it removed and takes the session with it. Exactly the defect fixed on the Android side's registered-device screen in #182. These two copies were missed, and they are worse than that one was: the sentence is shown to every CLI and wizard user on every sign-in, and it is the only place the exporter says what it registered as. Found the hard way. While testing whether a freshly drawn serial gets past Apple's 503, the run printed `serial 0PENTAGXPORT` with no identity file on disk at all - which for a moment read as the draw having failed, rather than the sentence being a lie. `FakeAccount` had no `serial` attribute, so nothing in the suite could tell a sentence that names the account from one that names a literal. It has one now, deliberately not `0PENTAGXPORT`, and `test_the_notice_names_the_serial_this_run_presents` asserts both that the drawn value appears and that the old constant does not. Checked by reinstating the literal and watching it go red. --- python/exporter/cli.py | 8 +++++++- python/exporter/wizard.py | 6 +++++- python/test/test_cli.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) 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.