from goodnews.db import connect, init_db from goodnews import digest, email_send def _seed(c, n=5, date="2026-06-09"): bid = c.execute("INSERT INTO daily_briefs (brief_date, title) VALUES (?, 't')", (date,)).lastrowid for i in range(1, n + 1): c.execute("INSERT INTO sources (id,name,feed_url,active,content_visible) VALUES (?,?,?,1,1)", (i, f"Src{i}", f"http://s{i}/f")) c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (?,?,?,?,?)", (i, i, f"http://a/{i}", f"Title {i}", f"h{i}")) c.execute("INSERT INTO article_scores (article_id,accepted,reason_text) VALUES (?,1,?)", (i, f"reason {i}")) c.execute("INSERT INTO article_summaries (article_id,summary) VALUES (?,?)", (i, f"summary {i}")) c.execute("INSERT INTO daily_brief_items (brief_id,article_id,rank) VALUES (?,?,?)", (bid, i, i)) c.execute("INSERT INTO users (id,email,digest_enabled) VALUES (1,'reader@x.com',1)") c.commit() return date def test_build_digest_is_calm_and_dated(): items = [{"id": 1, "title": "Good thing", "canonical_url": "http://a/1", "source": "Src", "summary": "nice", "reason_text": "wonder", "paywalled": False}] subject, text, html = digest.build_digest(items, "2026-06-09", "http://ub/unsub") assert "Tuesday's upbeatBytes" in subject and "1 calm read" in subject assert "Daily Highlights" in text and "Daily Highlights" in html assert "more good news is always" in text and "http://ub/unsub" in text # points back to the site assert "Good thing" in html and "Read on upbeatBytes" in html and "Unsubscribe" in html assert "you missed" not in (text + html).lower() # no guilt language def test_send_due_digests_sends_dedupes_and_skips(monkeypatch, tmp_path): sent = [] monkeypatch.setattr(email_send, "send_email", lambda to, subj, text, html=None, headers=None, **k: sent.append((to, headers))) c = connect(str(tmp_path / "d.db")); init_db(c) date = _seed(c, n=5) monkeypatch.setattr(digest, "local_today", lambda: date) # force=True bypasses the morning window assert digest.send_due_digests(c, force=True) == 1 assert sent[0][0] == "reader@x.com" hdrs = sent[0][1] # RFC 8058 one-click unsubscribe headers present assert "List-Unsubscribe" in hdrs and hdrs["List-Unsubscribe-Post"] == "List-Unsubscribe=One-Click" assert c.execute("SELECT item_count FROM digest_sends WHERE user_id=1").fetchone()[0] == 5 # dedupe: a second run sends nothing assert digest.send_due_digests(c, force=True) == 0 # unsub token was generated on send assert c.execute("SELECT digest_unsub_token FROM users WHERE id=1").fetchone()[0] def test_send_skips_thin_day(monkeypatch, tmp_path): monkeypatch.setattr(email_send, "send_email", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not send"))) c = connect(str(tmp_path / "d.db")); init_db(c) date = _seed(c, n=3) # below MIN_ITEMS (4) monkeypatch.setattr(digest, "local_today", lambda: date) assert digest.send_due_digests(c, force=True) == 0 def test_send_respects_opt_out(monkeypatch, tmp_path): sent = [] monkeypatch.setattr(email_send, "send_email", lambda to, *a, **k: sent.append(to)) c = connect(str(tmp_path / "d.db")); init_db(c) date = _seed(c, n=5) c.execute("UPDATE users SET digest_enabled=0 WHERE id=1"); c.commit() monkeypatch.setattr(digest, "local_today", lambda: date) assert digest.send_due_digests(c, force=True) == 0 and sent == [] def test_followed_digest_items_caps_excludes_and_section(tmp_path): c = connect(str(tmp_path / "fd.db")); init_db(c) date = _seed(c, n=5) # brief sources/articles 1..5 + user 1 # two NON-brief accepted articles from followed source 1 → cap should keep 1 for aid in (10, 11): c.execute("INSERT INTO articles (id,source_id,canonical_url,title,url_hash) VALUES (?,1,?,?,?)", (aid, f"http://a/{aid}", f"Followed {aid}", f"h{aid}")) c.execute("INSERT INTO article_scores (article_id,accepted) VALUES (?,1)", (aid,)) c.execute("INSERT INTO user_follows (user_id,kind,value) VALUES (1,'source','1')") c.commit() fol = digest.followed_digest_items(c, 1, exclude_ids=[1, 2, 3, 4, 5], limit=3) assert len(fol) == 1 and fol[0]["source_id"] == 1 # one-per-source cap, brief ids excluded assert fol[0]["id"] in (10, 11) # section appears with followed items, omitted without (brief stays the star) brief = [{"id": 1, "title": "B", "canonical_url": "http://b/1", "source": "S", "summary": "s", "reason_text": "r", "paywalled": False}] _, text, html = digest.build_digest(brief, "2026-06-09", "http://u", followed=fol) assert "From what you follow" in html and "From what you follow" in text _, _, html2 = digest.build_digest(brief, "2026-06-09", "http://u", followed=[]) assert "From what you follow" not in html2 def test_followed_digest_items_empty_when_no_follows(tmp_path): c = connect(str(tmp_path / "fd2.db")); init_db(c) _seed(c, n=5) assert digest.followed_digest_items(c, 1, exclude_ids=[], limit=3) == []