762f121320
- users.is_admin (+ migration); admin = is_admin OR email in GOODNEWS_ADMIN_EMAILS (normalized). is_admin exposed on /api/auth/me. Server-authorized GET /api/admin/stats (403 for non-admins). - queries.admin_stats: visitors (today/7d/30d), returning vs one-and-done, top opened articles, popular groupings + topics (derived from article_id at query time), share breakdown, daily opens/visits trend — all aggregate, no PII. - /admin page (gated, redirects non-admins): stat cards, CSS bar lists, a daily trend; "Admin dashboard" link on /account for admins. 129 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
58 lines
2.6 KiB
Python
58 lines
2.6 KiB
Python
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def _make(tmp_path, monkeypatch, admin_email=""):
|
|
db = tmp_path / "t.sqlite3"
|
|
monkeypatch.setenv("GOODNEWS_DB", str(db))
|
|
monkeypatch.setenv("GOODNEWS_PUBLIC_BASE_URL", "http://testserver")
|
|
monkeypatch.setenv("GOODNEWS_ADMIN_EMAILS", admin_email)
|
|
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.execute("INSERT INTO article_scores (article_id,accepted,topic) VALUES (1,1,'science')")
|
|
c.execute("INSERT INTO article_tags (article_id,tag) VALUES (1,'science')")
|
|
c.commit(); c.close()
|
|
return api.create_app(), api
|
|
|
|
|
|
def _signin(app, api, email):
|
|
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": email})
|
|
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
|
|
finally:
|
|
es.send_magic_link = orig
|
|
return tc
|
|
|
|
|
|
def test_admin_gating(tmp_path, monkeypatch):
|
|
app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com")
|
|
assert TestClient(app).get("/api/admin/stats").status_code == 401 # anon
|
|
nonadmin = _signin(app, api, "rando@x.com")
|
|
assert nonadmin.get("/api/admin/stats").status_code == 403 # signed in, not admin
|
|
assert nonadmin.get("/api/auth/me").json()["is_admin"] is False
|
|
admin = _signin(app, api, "Boss@X.com") # case-insensitive match
|
|
assert admin.get("/api/auth/me").json()["is_admin"] is True
|
|
assert admin.get("/api/admin/stats").status_code == 200
|
|
|
|
|
|
def test_admin_stats_shape(tmp_path, monkeypatch):
|
|
app, api = _make(tmp_path, monkeypatch, admin_email="boss@x.com")
|
|
admin = _signin(app, api, "boss@x.com")
|
|
# log a couple of events
|
|
admin.post("/api/events", json={"kind": "visit", "visitor": "v1"})
|
|
admin.post("/api/events", json={"kind": "open", "article_id": 1, "visitor": "v1"})
|
|
stats = admin.get("/api/admin/stats").json()
|
|
assert set(stats) >= {"visitors", "returning", "once", "top_articles", "top_groupings", "top_topics", "shares", "daily"}
|
|
assert stats["top_articles"][0]["id"] == 1 and stats["top_articles"][0]["opens"] == 1
|
|
assert any(g["tag"] == "science" for g in stats["top_groupings"])
|