bb008cfaa5
- Prefs sync: GET/PUT /api/prefs store Calm Filters/Boundaries on the account. On sign-in the client adopts the account's prefs if present, else seeds them from the device; every change PUTs to the account so tuning follows you across devices. (Login side-effects run under untrack so browsing doesn't re-trigger.) - Account panel: GET /api/account (email, connected sign-in methods, saved count, active sessions); Export my data (GET /api/account/export → JSON download); Sign out everywhere (revoke all sessions); Delete account (cascades to all account data) with an inline confirm. Reachable from You → Account. Deferred to a follow-up: link/unlink a provider (OAuth link-mode) and per-session revoke. 118 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
import json
|
|
|
|
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))
|
|
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.execute("INSERT INTO sources (id,name,feed_url,trust_score) VALUES (1,'S','http://s/f',5)")
|
|
c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (1,1,'http://s/1','t1','h1')")
|
|
c.commit(); c.close()
|
|
return api.create_app(), api
|
|
|
|
|
|
def _signed_in(app, api):
|
|
tc = TestClient(app)
|
|
sent = {}
|
|
import goodnews.email_send as es
|
|
orig = es.send_magic_link
|
|
es.send_magic_link = lambda to, link: sent.update(link=link)
|
|
try:
|
|
tc.post("/api/auth/email/start", json={"email": "a@b.com"})
|
|
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
|
|
finally:
|
|
es.send_magic_link = orig
|
|
return tc
|
|
|
|
|
|
def test_prefs_roundtrip(client):
|
|
app, api = client
|
|
tc = _signed_in(app, api)
|
|
assert tc.get("/api/prefs").json() == {"prefs": None} # nothing yet → seed from device
|
|
tc.put("/api/prefs", json={"prefs": {"mute_topics": ["health"], "avoid_terms": ["war"]}})
|
|
assert tc.get("/api/prefs").json()["prefs"]["mute_topics"] == ["health"]
|
|
|
|
|
|
def test_account_info_and_export(client):
|
|
app, api = client
|
|
tc = _signed_in(app, api)
|
|
tc.post("/api/saved/1")
|
|
info = tc.get("/api/account").json()
|
|
assert info["user"]["email"] == "a@b.com"
|
|
assert info["providers"] == ["email"] and info["sessions"] >= 1 and info["saved_count"] == 1
|
|
exp = tc.get("/api/account/export")
|
|
assert exp.headers["content-disposition"].endswith("upbeatbytes-data.json")
|
|
data = json.loads(exp.content)
|
|
assert data["account"]["email"] == "a@b.com" and {a["id"] for a in data["saved"]} == {1}
|
|
|
|
|
|
def test_logout_all_and_delete(client):
|
|
app, api = client
|
|
tc = _signed_in(app, api)
|
|
tc.post("/api/saved/1")
|
|
assert tc.post("/api/account/logout-all").json() == {"ok": True}
|
|
assert tc.get("/api/auth/me").json() is None # session revoked
|
|
|
|
tc2 = _signed_in(app, api) # same email → same account, has the save
|
|
assert tc2.get("/api/saved/ids").json() == [1]
|
|
assert tc2.delete("/api/account").json() == {"ok": True}
|
|
assert tc2.get("/api/auth/me").json() is None
|
|
# a fresh sign-in is a brand-new account (old data gone)
|
|
tc3 = _signed_in(app, api)
|
|
assert tc3.get("/api/saved/ids").json() == []
|