summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_recovery_email.py
blob: c6dab9777c2efbb21065336b70008d197180d470 (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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""
The recovery key in the registration e-mail (docs/auth-confirm.md §4.4).

When the client sends `recovery_key`, the hub appends it to the verification
e-mail and stores it nowhere. When it does not, the e-mail carries only the
code. `recovery_key` is a pass-through — no column, no log line beyond a
boolean.
"""

import base64
import hashlib

import pytest
from meshbay_hub import mail
from meshbay_hub.db.models import EmailVerification, 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` now requires — the gate
    # that keeps this hub off the open-relay list. What this module is
    # about is the body of the message, so the gate is stubbed away with
    # the socket; `test_mail_is_not_a_relay.py` is where it is exercised.
    monkeypatch.setattr("meshbay_hub.mail._send",
                        lambda msg, **_: sent.append(msg) or True)
    return sent


@pytest.mark.asyncio
async def test_register_appends_the_recovery_key_to_the_email(
        client, _skip_email_verification):
    r = await client.post("/v1/users/register", json={
        "username": "rk1", "email": "rk1@example.com",
        "auth_key": _auth_key("a-long-enough-passphrase", "rk1"),
        "recovery_key": RECOVERY,
    })
    assert r.status_code in (200, 201), r.text
    assert len(_skip_email_verification) == 1
    body = _skip_email_verification[0].get_content()
    assert RECOVERY in body
    assert "recovery key" in body.lower()


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


@pytest.mark.asyncio
async def test_the_recovery_key_is_not_persisted(
        client, db_session, _skip_email_verification):
    await client.post("/v1/users/register", json={
        "username": "rk3", "email": "rk3@example.com",
        "auth_key": _auth_key("a-long-enough-passphrase", "rk3"),
        "recovery_key": RECOVERY,
    })
    rows = (await db_session.execute(select(EmailVerification))).scalars().all()
    assert rows
    for row in rows:
        assert RECOVERY not in (row.code or "")
        assert RECOVERY not in (row.email_encrypted or "")
    user = (await db_session.execute(
        select(User).where(User.username == "rk3"))).scalar_one()
    assert RECOVERY not in repr(vars(user))


def test_mail_body_with_and_without_the_key(monkeypatch):
    captured = []
    monkeypatch.setattr("meshbay_hub.mail._send",
                        lambda msg, **_: captured.append(msg) or True)

    mail.send_verification_code("x@example.com", "123456",
                                recovery_key="MY-RECOVERY-KEY")
    body = captured[-1].get_content()
    assert "123456" in body
    assert "MY-RECOVERY-KEY" in body
    assert "recovery key" in body.lower()

    mail.send_verification_code("x@example.com", "123456")
    body = captured[-1].get_content()
    assert "123456" in body
    assert "recovery key" not in body.lower()