Files
upbeatBytes/tests/test_digest.py
T
thejayman77 0c68c22221 Brand consistency: emails say "upbeatBytes" (From + digest body)
Per the brand-name standard (camelCase, one word). Updated the SMTP From default and
the digest email body/subject strings. Live env From values (auth.env + goodnews.env)
updated to match. (Web/OG brand strings in share.py + app.html are the remaining sweep.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:38:16 -04:00

92 lines
5.1 KiB
Python

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) == []