aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_push.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_push.py')
-rw-r--r--packages/meshbay-hub/tests/test_push.py79
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