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