bfd612eb9b
- paywall.py: conservative domain-level paywall detection (New Scientist, Nature, and common hard/soft paywalls). Never fetches pages — an honest hint. - API: Article gains a 'paywalled' flag; the brief now leads with a gentle AND readable story (paywalled/charged stories stay in the five, just not first). - New GET /api/replacement returns the next-best readable, unshown article (honors mood+prefs via the merged prefs param; gentle=true for hero swaps). - UI: paywalled cards show 'May need a subscription'; a Replace / 'Find one I can read' action (always visible, while tuning actions stay tucked) swaps the card for a readable alternative, with a gentle notice when none remain. - Tests: paywall detection + replacement behavior (77 total). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
38 lines
1.8 KiB
Python
38 lines
1.8 KiB
Python
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from goodnews.db import connect, init_db
|
|
|
|
@pytest.fixture
|
|
def client(tmp_path, monkeypatch):
|
|
db = tmp_path / "t.sqlite3"
|
|
monkeypatch.setenv("GOODNEWS_DB", str(db))
|
|
conn = connect(db); init_db(conn)
|
|
conn.execute("INSERT INTO sources (id,name,feed_url,trust_score) VALUES (1,'S','http://s/f',6)")
|
|
def add(aid, url, cort=1):
|
|
conn.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (?,1,?,?,?)",
|
|
(aid, url, f"t{aid}", f"h{aid}"))
|
|
conn.execute("INSERT INTO article_scores (article_id,constructive_score,agency_score,human_benefit_score,"
|
|
"cortisol_score,ragebait_score,pr_risk_score,accepted,topic,flavor) "
|
|
"VALUES (?,7,2,2,?,0,2,1,'science','discovery')", (aid, cort))
|
|
add(1, "https://phys.org/free-a") # free, best
|
|
add(2, "https://www.newscientist.com/pay") # paywalled
|
|
add(3, "https://www.goodnewsnetwork.org/free-b") # free
|
|
conn.commit(); conn.close()
|
|
from goodnews.api import create_app
|
|
return TestClient(create_app())
|
|
|
|
def test_replacement_skips_paywalled_and_excluded(client):
|
|
# exclude the top free one -> should return the OTHER free one, never the paywalled
|
|
r = client.get("/api/replacement", params={"exclude": "1"})
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body is not None and body["id"] == 3 and body["paywalled"] is False
|
|
|
|
def test_replacement_none_when_only_paywalled_left(client):
|
|
r = client.get("/api/replacement", params={"exclude": "1,3"})
|
|
assert r.json() is None # only the paywalled one remains, and avoid_paywall defaults true
|
|
|
|
def test_replacement_can_include_paywalled_when_allowed(client):
|
|
r = client.get("/api/replacement", params={"exclude": "1,3", "avoid_paywall": "false"})
|
|
assert r.json()["id"] == 2
|