Files
thejayman77 d2ae56dc65 Accounts Phase 1b: magic-link auth endpoints + sessions
- POST /api/auth/email/start — validate email, rate-limit, email a single-use
  magic link (identical reply regardless, so no account enumeration).
- POST /api/auth/email/verify — consume token, find-or-create user, open a
  session, set an httpOnly cookie (web) and return a bearer token (app).
- GET /api/auth/me, POST /api/auth/logout.
- Session resolved from cookie OR Authorization: Bearer; cookie is Secure in
  prod (https), relaxed for http so tests round-trip. CORS now allows POST.

Live SMTP send verified against the DNSExit relay (587/STARTTLS). 108 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:08:33 +00:00

70 lines
2.3 KiB
Python

import os
import pytest
from fastapi.testclient import TestClient
@pytest.fixture
def client(tmp_path, monkeypatch):
db = tmp_path / "t.sqlite3"
monkeypatch.setenv("GOODNEWS_DB", str(db))
# http base → cookies aren't Secure-only, so the TestClient round-trips them
monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver")
import importlib
import goodnews.api as api
importlib.reload(api)
from goodnews.db import connect, init_db
c = connect(str(db)); init_db(c); c.close()
return api.create_app(), api
def test_magic_link_end_to_end(client, monkeypatch):
app, api = client
sent = {}
# capture the link instead of sending real email
monkeypatch.setattr(api.email_send, "send_magic_link", lambda to, link: sent.update(to=to, link=link))
tc = TestClient(app)
# not signed in
assert tc.get("/api/auth/me").json() is None
# request a link
r = tc.post("/api/auth/email/start", json={"email": "Jay@Example.com"})
assert r.status_code == 200 and r.json() == {"ok": True}
assert sent["to"] == "jay@example.com"
token = sent["link"].split("token=")[1]
# verify → session established (cookie set), user created
r = tc.post("/api/auth/email/verify", json={"token": token})
assert r.status_code == 200
body = r.json()
assert body["user"]["email"] == "jay@example.com" and body["token"]
# cookie now authenticates /me
me = tc.get("/api/auth/me").json()
assert me and me["email"] == "jay@example.com"
# bearer token also works (app clients)
bare = TestClient(app)
me2 = bare.get("/api/auth/me", headers={"Authorization": f"Bearer {body['token']}"}).json()
assert me2["email"] == "jay@example.com"
# reusing the magic link fails (single use)
assert tc.post("/api/auth/email/verify", json={"token": token}).status_code == 400
# logout clears the session
assert tc.post("/api/auth/logout").json() == {"ok": True}
assert tc.get("/api/auth/me").json() is None
def test_start_rejects_bad_email(client):
app, _ = client
tc = TestClient(app)
assert tc.post("/api/auth/email/start", json={"email": "nope"}).status_code == 422
def test_verify_bad_token(client):
app, _ = client
tc = TestClient(app)
assert tc.post("/api/auth/email/verify", json={"token": "garbage"}).status_code == 400