From 31b2173380749d324358bc780e1b133ba6f58ec4 Mon Sep 17 00:00:00 2001 From: Ethan Stoner Date: Sat, 29 Aug 2026 12:51:52 -0700 Subject: [PATCH] check multi-account against two real profiles Five layers, cheapest first: which accounts a configuration produces, the flags each one hands Edge, the run loop's ordering and exit codes, one account failing every way it can without ending the batch, and two real Edge profiles holding two independent, persistent identities. Layers 1 to 4 need nothing installed beyond selenium and run in a second. Layer 5 starts Edge twice and reaches bing.com, so it is opt in behind --browser. It keys on bing.com's own MUID rather than an injected cookie: a cookie added through webdriver is not written to the profile the way a Set-Cookie is, so it would prove nothing about a sign-in surviving. Claude-Session: https://claude.ai/code/session_019PWUtibJn81iDbxZ4hzeU3 --- tests/test_multi_account.py | 359 ++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 tests/test_multi_account.py diff --git a/tests/test_multi_account.py b/tests/test_multi_account.py new file mode 100644 index 0000000..bfbd569 --- /dev/null +++ b/tests/test_multi_account.py @@ -0,0 +1,359 @@ +"""Checks for REWARDS_ACCOUNTS. + +Five layers, cheapest first: + + 1. which accounts a configuration produces + 2. the flags each one hands Edge + 3. the run loop's ordering, skip-on-failure and exit codes + 4. one account failing every way it can, without ending the batch + 5. two real Edge profiles holding two independent, persistent identities + +Layers 1 to 4 are pure and need nothing installed. Layer 4 drives the real +run loop with a stand-in for the browser, so the code under test is the +shipped one and only selenium is replaced. Layer 5 starts Edge twice and +reaches bing.com, so it is opt in: + + python tests/test_multi_account.py # layers 1-4 + python tests/test_multi_account.py --browser # all five + +Layer 5 is the one that answers "does multi-account work". Two profiles must +end up with two different identities, and each must keep its own across a +restart, because that is what a per-account sign-in is made of. It uses +bing.com's own MUID cookie rather than an injected one: a cookie added through +webdriver is not written to the profile the way a Set-Cookie is, so it proves +nothing about a sign-in surviving. +""" + +import logging +import os +import shutil +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src")) + +FAILURES = [] + + +def check(label, got, want): + if got == want: + print(f" ok {label}") + + return True + + print(f" FAIL {label}\n got {got!r}\n want {want!r}") + FAILURES.append(label) + + return False + + +def accounts_for(value): + """configured() under a given REWARDS_ACCOUNTS, or ValueError.""" + if value is None: + os.environ.pop(accounts.ENV_VAR, None) + else: + os.environ[accounts.ENV_VAR] = value + + return accounts.configured() + + +# -------------------------------------------------------------------------- +# 1. which accounts a configuration produces +# -------------------------------------------------------------------------- + +def test_configuration(): + print("\n[1] account configuration") + + default = accounts_for(None) + check("unset gives one account", [a.name for a in default], ["default"]) + check("unset uses the existing profile directory", default[0].user_data_dir, USER_DATA_DIR) + check("unset is the default profile", default[0].is_default, True) + + check("two names, in order", [a.name for a in accounts_for("personal,spare")], ["personal", "spare"]) + check("surrounding whitespace ignored", [a.name for a in accounts_for(" personal , spare ")], ["personal", "spare"]) + check("empty entries dropped", [a.name for a in accounts_for("personal,,spare,")], ["personal", "spare"]) + check("blank value falls back to default", [a.name for a in accounts_for(" ")], ["default"]) + check("duplicates collapse, case insensitively", [a.name for a in accounts_for("personal,PERSONAL,spare")], ["personal", "spare"]) + + # Names become directory names. Anything that resolves outside the profile + # directory, or onto a directory another entry already owns, has to be + # refused rather than quietly writing somewhere else. + refused = [ + # relative traversal + "..", ".", "../escape", "..\\escape", "a/b", "a\\b", + # absolute, drive-relative and UNC + "/etc", "\\", "/", "C:", "C:\\Windows", "\\\\server\\share", + # expanded elsewhere, not here + "~", "%TEMP%", "$HOME", + # Win32 strips trailing dots, so these are not the directories they read + # as: "personal." is "personal", and "..." is data-dir itself + "...", "....", "personal.", "personal..", + # shell and filesystem metacharacters + "a b", "a:b", "a;b", "a|b", "a*b", "a?b", "a