diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_email_change_requires_passphrase.py | 55 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_mail_is_not_a_relay.py | 15 |
2 files changed, 64 insertions, 6 deletions
diff --git a/packages/meshbay-hub/tests/test_email_change_requires_passphrase.py b/packages/meshbay-hub/tests/test_email_change_requires_passphrase.py new file mode 100644 index 0000000..697e6f9 --- /dev/null +++ b/packages/meshbay-hub/tests/test_email_change_requires_passphrase.py @@ -0,0 +1,55 @@ +"""Changing the address on file requires the passphrase, not merely a token. + +A member hands its hub access token to every node it connects to (the MNP +handshake), so a node operator holds a live bearer token for that member. +`PATCH /v1/users/me {email}` used to need only that token, and the confirmation +code goes to the *new* address — so an operator could point the account's e-mail +at their own inbox, confirm it, and then use the passphrase-reset path to take +the account over. The passphrase (as the derived auth_key, which is all the hub +ever sees) is now required, exactly as for a passphrase change or an account +deletion. +""" + +import pytest + + +async def _account(client, username="mail_pass_test", auth_key="k" * 44): + await client.post("/v1/users/register", json={ + "username": username, "email": f"{username}@test.local", "auth_key": auth_key}) + r = await client.post("/v1/users/login", json={ + "username": username, "auth_key": auth_key}) + return r.json()["access_token"] + + +@pytest.mark.asyncio +async def test_email_change_without_passphrase_is_refused(client): + tok = await _account(client) + r = await client.patch("/v1/users/me", headers={"Authorization": f"Bearer {tok}"}, + json={"email": "attacker@evil.invalid"}) + assert r.status_code == 403 + + +@pytest.mark.asyncio +async def test_email_change_with_wrong_passphrase_is_refused(client): + tok = await _account(client) + r = await client.patch("/v1/users/me", headers={"Authorization": f"Bearer {tok}"}, + json={"email": "attacker@evil.invalid", "auth_key": "z" * 44}) + assert r.status_code == 403 + + +@pytest.mark.asyncio +async def test_email_change_with_correct_passphrase_proceeds(client): + tok = await _account(client, username="mail_ok_test", auth_key="k" * 44) + r = await client.patch("/v1/users/me", headers={"Authorization": f"Bearer {tok}"}, + json={"email": "new@real.invalid", "auth_key": "k" * 44}) + assert r.status_code == 200 + assert r.json().get("email_verification_required") is True + + +@pytest.mark.asyncio +async def test_profile_patch_without_email_needs_no_passphrase(client): + """Regression: a PATCH that does not change the address is unaffected.""" + tok = await _account(client, username="mail_noop_test") + r = await client.patch("/v1/users/me", headers={"Authorization": f"Bearer {tok}"}, + json={}) + assert r.status_code == 200 diff --git a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py index 157dbd5..040ff43 100644 --- a/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py +++ b/packages/meshbay-hub/tests/test_mail_is_not_a_relay.py @@ -314,6 +314,9 @@ def test_a_refusal_never_names_the_address(): # ── The doors, driven through the API ──────────────────────────────────────── +_AK = base64.b64encode(b"k" * 32).decode() # the passphrase-derived auth_key these accounts use + + async def _register(client, username: str, email: str, captcha=None): sk_ed, sk_x = Ed25519PrivateKey.generate(), X25519PrivateKey.generate() return await client.post("/v1/users/register", json={ @@ -383,11 +386,11 @@ async def test_an_account_may_point_the_hub_at_one_stranger_then_wait( before = len(wire) r = await client.patch("/v1/users/me", headers=headers, - json={"email": "a-stranger@example.test"}) + json={"auth_key": _AK, "email": "a-stranger@example.test"}) assert r.status_code == 200, r.text r = await client.patch("/v1/users/me", headers=headers, - json={"email": "another-stranger@example.test"}) + json={"auth_key": _AK, "email": "another-stranger@example.test"}) assert r.status_code == 429, r.text assert len(wire) - before == 1, "the hub mailed a second stranger on demand" @@ -408,7 +411,7 @@ async def test_the_address_already_pending_may_be_asked_for_again( "relay_d@example.test") typo = "jean@gmial.test" - r = await client.patch("/v1/users/me", headers=headers, json={"email": typo}) + r = await client.patch("/v1/users/me", headers=headers, json={"auth_key": _AK, "email": typo}) assert r.status_code == 200, r.text # The recipient's own cooldown is not what is under test here. @@ -419,7 +422,7 @@ async def test_the_address_already_pending_may_be_asked_for_again( await db_session.commit() before = len(wire) - r = await client.patch("/v1/users/me", headers=headers, json={"email": typo}) + r = await client.patch("/v1/users/me", headers=headers, json={"auth_key": _AK, "email": typo}) assert r.status_code == 200, ( "a typo locked the account out of correcting it: " + r.text) assert len(wire) - before == 1 @@ -437,7 +440,7 @@ async def test_the_delay_survives_the_verification_row_being_deleted( "relay_c@example.test") r = await client.patch("/v1/users/me", headers=headers, - json={"email": "c-first@example.test"}) + json={"auth_key": _AK, "email": "c-first@example.test"}) assert r.status_code == 200, r.text from meshbay_hub.db.models import EmailVerification @@ -446,7 +449,7 @@ async def test_the_delay_survives_the_verification_row_being_deleted( await db_session.commit() r = await client.patch("/v1/users/me", headers=headers, - json={"email": "c-second@example.test"}) + json={"auth_key": _AK, "email": "c-second@example.test"}) assert r.status_code == 429, ( "the delay was counted from a table the handler empties") |