aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_known_browser.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_known_browser.py')
-rw-r--r--packages/meshbay-hub/tests/test_known_browser.py98
1 files changed, 98 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_known_browser.py b/packages/meshbay-hub/tests/test_known_browser.py
new file mode 100644
index 0000000..260f08e
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_known_browser.py
@@ -0,0 +1,98 @@
+"""
+A stranger who knows your name locks only the browsers you never used.
+
+The sign-in lockout counts wrong passphrases per username: four an hour from
+anyone kept the owner out of every browser for as long as they cared to keep
+going. A browser that signed in to the account before presents a token and has
+a counter of its own, which nobody else can spend. The token is not a
+credential — the passphrase is still asked — and a passphrase re-checked inside
+an open session is not the sign-in counter's business at all.
+"""
+
+import pytest
+from meshbay_hub.db.models import KnownBrowser, User
+from sqlalchemy import func, select
+from test_bundle_pepper import KEY, _register
+
+WRONG = "w" * 44
+
+
+async def _sign_in(client, username, key=KEY, known=None):
+ body = {"username": username, "auth_key": key}
+ if known:
+ body["known_browser"] = known
+ return await client.post("/v1/users/login", json=body)
+
+
+async def _lock(client, username):
+ for _ in range(4):
+ await _sign_in(client, username, WRONG)
+ assert (await _sign_in(client, username)).status_code == 429
+
+
+@pytest.mark.asyncio
+async def test_a_known_browser_signs_in_through_a_strangers_lockout(client):
+ await _register(client, "known_owner")
+ token = (await _sign_in(client, "known_owner")).json()["known_browser"]
+
+ await _lock(client, "known_owner") # the stranger, without a token
+ r = await _sign_in(client, "known_owner", known=token)
+ assert r.status_code == 200, r.text
+ assert "known_browser" not in r.json(), "a known browser is not given a second token"
+
+
+@pytest.mark.asyncio
+async def test_another_accounts_token_is_no_way_round(client):
+ await _register(client, "known_alice")
+ await _register(client, "known_bobby")
+ alices = (await _sign_in(client, "known_alice")).json()["known_browser"]
+
+ await _lock(client, "known_bobby")
+ assert (await _sign_in(client, "known_bobby", known=alices)).status_code == 429
+
+
+@pytest.mark.asyncio
+async def test_a_known_browser_is_locked_by_its_own_failures(client):
+ """Whoever holds the token still guesses at the same rate."""
+ await _register(client, "known_guess")
+ token = (await _sign_in(client, "known_guess")).json()["known_browser"]
+ for _ in range(4):
+ assert (await _sign_in(client, "known_guess", WRONG, token)).status_code == 401
+ assert (await _sign_in(client, "known_guess", known=token)).status_code == 429
+ # ...and that spent nothing of a browser that has no token.
+ assert (await _sign_in(client, "known_guess")).status_code == 200
+
+
+@pytest.mark.asyncio
+async def test_a_locked_name_does_not_stop_its_owner_inside_a_session(client):
+ await _register(client, "known_inside")
+ session = (await _sign_in(client, "known_inside")).json()["access_token"]
+ await _lock(client, "known_inside")
+
+ r = await client.post("/v1/users/me/bundle-pepper", json={"auth_key": KEY},
+ headers={"Authorization": f"Bearer {session}"})
+ assert r.status_code == 200, r.text
+
+
+@pytest.mark.asyncio
+async def test_only_a_hash_is_kept_and_only_twenty(client, db_session):
+ await _register(client, "known_many")
+ tokens = [(await _sign_in(client, "known_many")).json()["known_browser"]
+ for _ in range(25)]
+ uid = (await db_session.execute(
+ select(User.id).where(User.username == "known_many"))).scalar_one()
+ rows = (await db_session.execute(
+ select(KnownBrowser).where(KnownBrowser.user_id == uid))).scalars().all()
+ assert len(rows) == 20
+ assert not any(t in {r.token_hash for r in rows} for t in tokens)
+
+
+@pytest.mark.asyncio
+async def test_erasing_the_account_forgets_its_browsers(client, db_session):
+ await _register(client, "known_gone")
+ login = (await _sign_in(client, "known_gone")).json()
+ r = await client.request("DELETE", "/v1/users/me", json={"auth_key": KEY},
+ headers={"Authorization": f"Bearer {login['access_token']}"})
+ assert r.status_code == 200, r.text
+ left = await db_session.scalar(select(func.count()).select_from(KnownBrowser))
+ assert left == 0