diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_push.py | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_push.py b/packages/meshbay-hub/tests/test_push.py index c3bca4f..3450aa2 100644 --- a/packages/meshbay-hub/tests/test_push.py +++ b/packages/meshbay-hub/tests/test_push.py @@ -453,3 +453,82 @@ async def test_an_endpoint_without_its_keys_is_refused(client): json={"endpoint": "https://push.example.net/up/k"}, headers={"Authorization": f"Bearer {mine}"}) assert r.status_code == 422 + + +# ── A phone's poll secret ends with the account's sessions ─────────────────── +# +# The secret is a credential of its own, outside any session. A lost phone is +# exactly what "sign out everywhere" and a passphrase change are pressed for; +# a revoked account is not told anything more. + + +async def _registered(client, username): + token = await _user(client, username) + reg = (await client.post("/v1/push/subscriptions", json={}, + headers={"Authorization": f"Bearer {token}"})).json() + await _subscribe(client, token) + assert (await _poll(client, reg)).status_code == 200 + return token, reg + + +async def _rows(db_session, username): + uid = (await db_session.execute( + select(User.id).where(User.username == username))).scalar_one() + db_session.expire_all() + return (await db_session.execute( + select(PushSubscription).where(PushSubscription.user_id == uid))).scalars().all() + + +@pytest.mark.asyncio +async def test_signing_out_everywhere_ends_every_phone(client, db_session, unthrottled): + token, reg = await _registered(client, "lost_phone") + r = await client.post("/v1/users/me/sessions/revoke", + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 200, r.text + assert (await _poll(client, reg)).status_code == 404 + assert await _rows(db_session, "lost_phone") == [], "pushed rows go too" + + +@pytest.mark.asyncio +async def test_a_passphrase_change_ends_every_phone(client, db_session, unthrottled): + token, reg = await _registered(client, "new_passphrase") + r = await client.post("/v1/users/password", json={ + "old_auth_key": _auth_key("a-long-enough-passphrase", "new_passphrase"), + "new_auth_key": _auth_key("another-long-passphrase", "new_passphrase"), + }, headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 200, r.text + assert (await _poll(client, reg)).status_code == 404 + assert await _rows(db_session, "new_passphrase") == [] + + +@pytest.mark.asyncio +async def test_a_revoked_account_is_told_nothing(client, db_session, unthrottled): + from meshbay_hub.api.deps import set_admin_usernames + + _, reg = await _registered(client, "revoked_phone") + admin = await _user(client, "push_admin") + uid = (await db_session.execute( + select(User.id).where(User.username == "revoked_phone"))).scalar_one() + set_admin_usernames(["push_admin"]) + r = await client.post("/v1/admin/revoke", json={ + "target": "user", "target_id": uid, "reason": "test"}, + headers={"Authorization": f"Bearer {admin}"}) + assert r.status_code == 200, r.text + assert (await _poll(client, reg)).status_code == 404 + assert await _rows(db_session, "revoked_phone") == [] + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("status", "code"), [("suspended", 200), ("revoked", 404), + ("deleted", 404)]) +async def test_polling_follows_the_account_status(client, db_session, unthrottled, + status, code): + # The gate itself, apart from the paths that delete the row: a status set + # some other way still decides. Suspended still reads — the line saying so + # is one of the notifications. + _, reg = await _registered(client, f"status_{status}") + user = (await db_session.execute( + select(User).where(User.username == f"status_{status}"))).scalar_one() + user.status = status + await db_session.commit() + assert (await _poll(client, reg)).status_code == code |