User feedback + expanded privacy-respecting admin stats
Feedback: - feedback table; POST /api/feedback (anonymous-ok, optional category/email, honeypot + per-day flood cap) stores + emails the admin; GET /api/admin/feedback. - Shared feedback store + FeedbackModal; a speech-bubble opens it from the desktop header, the mobile top bar (logo moves left), the footer, and /account. Feedback section in /admin. Stats (additive, same privacy model — no IP/UA/referrer/raw terms): - Event vocab: summary_viewed (fired on /a load), full_story (card → source), not_today/less_like_this/hide_topic, replace_used/replace_none, paywall_replace, paywalled_source_open. Card title/image opens /a (no double-count); history records via keepalive so it survives the nav. - Dashboard: Accounts card (counts only), reading funnel (summary→source rate), emotional-mix & friction, paywall, returning-visitor buckets. (Health metrics deferred to a future monitoring dashboard.) 131 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def _app(tmp_path, monkeypatch, admin=""):
|
||||
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)
|
||||
import importlib
|
||||
import goodnews.api as api
|
||||
importlib.reload(api)
|
||||
from goodnews.db import connect, init_db
|
||||
connect(str(db)).close()
|
||||
c = connect(str(db)); init_db(c); c.close()
|
||||
return api.create_app(), api, db
|
||||
|
||||
|
||||
def _count(db):
|
||||
from goodnews.db import connect
|
||||
c = connect(str(db)); n = c.execute("SELECT COUNT(*) FROM feedback").fetchone()[0]; c.close()
|
||||
return n
|
||||
|
||||
|
||||
def test_feedback_stored_and_validated(tmp_path, monkeypatch):
|
||||
app, api, db = _app(tmp_path, monkeypatch)
|
||||
monkeypatch.setattr(api.email_send, "send_feedback", lambda *a, **k: None)
|
||||
tc = TestClient(app)
|
||||
assert tc.post("/api/feedback", json={"message": " "}).status_code == 422 # empty
|
||||
assert tc.post("/api/feedback", json={"category": "idea", "message": "Love it", "visitor": "v"}).json() == {"ok": True}
|
||||
assert _count(db) == 1
|
||||
# honeypot tripped → accepted but NOT stored
|
||||
tc.post("/api/feedback", json={"message": "spam", "hp": "i am a bot", "visitor": "v"})
|
||||
assert _count(db) == 1
|
||||
|
||||
|
||||
def test_admin_feedback_gated(tmp_path, monkeypatch):
|
||||
app, api, db = _app(tmp_path, monkeypatch, admin="boss@x.com")
|
||||
monkeypatch.setattr(api.email_send, "send_feedback", lambda *a, **k: None)
|
||||
TestClient(app).post("/api/feedback", json={"message": "hi", "visitor": "v"})
|
||||
assert TestClient(app).get("/api/admin/feedback").status_code == 401 # anon
|
||||
|
||||
# sign in as admin
|
||||
tc = TestClient(app); sent = {}
|
||||
monkeypatch.setattr(api.email_send, "send_magic_link", lambda to, link: sent.update(link=link))
|
||||
tc.post("/api/auth/email/start", json={"email": "boss@x.com"})
|
||||
tc.post("/api/auth/email/verify", json={"token": sent["link"].split("token=")[1]})
|
||||
fb = tc.get("/api/admin/feedback").json()
|
||||
assert len(fb) == 1 and fb[0]["message"] == "hi"
|
||||
stats = tc.get("/api/admin/stats").json()
|
||||
assert {"funnel", "accounts", "retention", "emotional_mix", "paywall", "replace"} <= set(stats)
|
||||
assert stats["accounts"]["total"] >= 1
|
||||
Reference in New Issue
Block a user