summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_recovery_email.py
blob: 0e19be97529af6d37631161699669a1388acdb55 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""
The recovery key never reaches the hub (docs/MESHBAY_DESIGN.md §3.6).

It used to: a client that ticked "e-mail it to me" sent it in the register body
and the hub appended it to the verification e-mail, so the hub saw it in clear
and so did anyone who could make the hub mail one (the resend door). The person
now mails it to themselves from their own application. The hub refuses the
field rather than dropping it, so an older client that still offers the option
cannot tell somebody their key was sent when it was not.
"""

import base64
import hashlib

import pytest
from meshbay_hub import mail
from meshbay_hub.db.models import User
from sqlalchemy import select

RECOVERY = "ABCD EFGH JKLM NPQR STUV WXYZ 2345 6789 ABCD EFGH JKLM NPQR STUV"


def _auth_key(password: str, username: str) -> str:
    salt = hashlib.sha256(f"meshbay:auth:v1:{username}".encode()).digest()
    return base64.b64encode(
        hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 600_000, 32)).decode()


@pytest.fixture(autouse=True)
def _skip_email_verification(monkeypatch):
    """
    Override conftest's skip: this module needs the real verification path to
    run so the e-mail is actually built. Capture it instead of sending.
    """
    sent = []
    # `**_` swallows the `purpose` keyword `_send` requires — the gate that
    # keeps this hub off the open-relay list, exercised in
    # `test_mail_is_not_a_relay.py`. What this module is about is the body.
    monkeypatch.setattr("meshbay_hub.mail._send",
                        lambda msg, **_: sent.append(msg) or True)
    return sent


@pytest.mark.asyncio
async def test_a_register_carrying_the_recovery_key_is_refused(
        client, db_session, _skip_email_verification):
    r = await client.post("/v1/users/register", json={
        "username": "rk1_test", "email": "rk1@example.com",
        "auth_key": _auth_key("a-long-enough-passphrase", "rk1_test"),
        "recovery_key": RECOVERY,
    })
    assert r.status_code == 400, r.text
    assert "recovery key" in r.json()["detail"]
    assert _skip_email_verification == [], "nothing was mailed"
    assert (await db_session.execute(
        select(User).where(User.username == "rk1_test"))).scalar_one_or_none() is None


@pytest.mark.asyncio
async def test_a_resend_carrying_the_recovery_key_is_refused(
        client, _skip_email_verification):
    # The resend door has no captcha: it was how a stranger who knew a pending
    # username and address could have the hub mail a "recovery key" of theirs.
    body = {"username": "rk2_test", "email": "rk2@example.com",
            "auth_key": _auth_key("a-long-enough-passphrase", "rk2_test")}
    assert (await client.post("/v1/users/register", json=body)).status_code == 201
    r = await client.post("/v1/users/register", json={**body, "recovery_key": RECOVERY})
    assert r.status_code == 400, r.text
    for msg in _skip_email_verification:
        assert RECOVERY not in msg.get_content()


@pytest.mark.asyncio
async def test_the_verification_mail_carries_only_the_code(
        client, _skip_email_verification):
    r = await client.post("/v1/users/register", json={
        "username": "rk3_test", "email": "rk3@example.com",
        "auth_key": _auth_key("a-long-enough-passphrase", "rk3_test"),
    })
    assert r.status_code == 201, r.text
    (msg,) = _skip_email_verification
    body = msg.get_content()
    assert "verification code is" in body.lower()
    assert "recovery" not in body.lower()


def test_the_mail_function_takes_no_recovery_key():
    with pytest.raises(TypeError):
        mail.send_verification_code("x@example.com", "123456", recovery_key=RECOVERY)