d2ae56dc65
- 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>
70 lines
2.3 KiB
Python
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
|