9f8dd960f4
New auth suite covers registration, login (incl. wrong-password), email verification, password reset (old sessions + old password rejected), logout revocation, and no-enumeration on reset. Core tenancy tests now authenticate via real sessions. A capturing mailer makes email flows assertable. 13 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""Auth flows: registration, login, email verification, password reset, logout."""
|
|
|
|
from tests.conftest import auth, register, token_from_link
|
|
|
|
|
|
async def test_register_issues_session_and_verification_email(client, mailer):
|
|
resp = await client.post(
|
|
"/api/v1/auth/register",
|
|
json={"email": "new@example.com", "password": "password123", "display_name": "New"},
|
|
)
|
|
assert resp.status_code == 201, resp.text
|
|
body = resp.json()
|
|
assert body["token"]
|
|
assert body["user"]["email"] == "new@example.com"
|
|
assert body["user"]["email_verified_at"] is None
|
|
# A verification email was "sent".
|
|
assert len(mailer.verifications) == 1
|
|
assert mailer.verifications[0][0] == "new@example.com"
|
|
|
|
|
|
async def test_duplicate_email_conflicts(client):
|
|
await register(client, "dupe@example.com")
|
|
resp = await client.post(
|
|
"/api/v1/auth/register", json={"email": "dupe@example.com", "password": "password123"}
|
|
)
|
|
assert resp.status_code == 409
|
|
|
|
|
|
async def test_login_wrong_password_rejected(client):
|
|
await register(client, "user@example.com", password="password123")
|
|
resp = await client.post(
|
|
"/api/v1/auth/login", json={"email": "user@example.com", "password": "wrong-password"}
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
async def test_login_succeeds_and_me_returns_user(client):
|
|
await register(client, "user2@example.com", password="password123")
|
|
resp = await client.post(
|
|
"/api/v1/auth/login", json={"email": "user2@example.com", "password": "password123"}
|
|
)
|
|
assert resp.status_code == 200
|
|
token = resp.json()["token"]
|
|
resp = await client.get("/api/v1/users/me", headers=auth(token))
|
|
assert resp.status_code == 200
|
|
assert resp.json()["email"] == "user2@example.com"
|
|
|
|
|
|
async def test_email_verification(client, mailer):
|
|
await register(client, "verify@example.com")
|
|
token = token_from_link(mailer.verifications[0][1])
|
|
resp = await client.post("/api/v1/auth/verify-email", json={"token": token})
|
|
assert resp.status_code == 204
|
|
|
|
# Logging in and checking /me shows the address is now verified.
|
|
login = await client.post(
|
|
"/api/v1/auth/login", json={"email": "verify@example.com", "password": "password123"}
|
|
)
|
|
me = await client.get("/api/v1/users/me", headers=auth(login.json()["token"]))
|
|
assert me.json()["email_verified_at"] is not None
|
|
|
|
|
|
async def test_password_reset_flow_revokes_old_sessions(client, mailer):
|
|
old_token = await register(client, "reset@example.com", password="password123")
|
|
|
|
resp = await client.post(
|
|
"/api/v1/auth/request-password-reset", json={"email": "reset@example.com"}
|
|
)
|
|
assert resp.status_code == 202
|
|
reset_token = token_from_link(mailer.resets[0][1])
|
|
|
|
resp = await client.post(
|
|
"/api/v1/auth/reset-password",
|
|
json={"token": reset_token, "new_password": "new-password456"},
|
|
)
|
|
assert resp.status_code == 204
|
|
|
|
# Old session is revoked.
|
|
assert (await client.get("/api/v1/users/me", headers=auth(old_token))).status_code == 401
|
|
# Old password no longer works; new one does.
|
|
assert (
|
|
await client.post(
|
|
"/api/v1/auth/login", json={"email": "reset@example.com", "password": "password123"}
|
|
)
|
|
).status_code == 401
|
|
assert (
|
|
await client.post(
|
|
"/api/v1/auth/login",
|
|
json={"email": "reset@example.com", "password": "new-password456"},
|
|
)
|
|
).status_code == 200
|
|
|
|
|
|
async def test_request_password_reset_unknown_email_still_accepted(client, mailer):
|
|
resp = await client.post(
|
|
"/api/v1/auth/request-password-reset", json={"email": "nobody@example.com"}
|
|
)
|
|
assert resp.status_code == 202
|
|
assert len(mailer.resets) == 0 # no email sent, but no enumeration either
|
|
|
|
|
|
async def test_logout_revokes_session(client):
|
|
token = await register(client, "logout@example.com")
|
|
assert (await client.get("/api/v1/users/me", headers=auth(token))).status_code == 200
|
|
assert (await client.post("/api/v1/auth/logout", headers=auth(token))).status_code == 204
|
|
assert (await client.get("/api/v1/users/me", headers=auth(token))).status_code == 401
|