repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

7.4 KB · 245 lines · Go Raw History
  1package main
  2
  3import (
  4	"strings"
  5	"testing"
  6	"time"
  7)
  8
  9// row builds a dated at a fixed offset back from a fixed now, so the tests can
 10// talk about order without carrying timestamps around.
 11func row(source string, minsAgo int) dated {
 12	base := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)
 13	id := source + string(rune('a'+minsAgo))
 14	return dated{
 15		Headline: Headline{Title: id, URL: "https://example.test/" + id, Source: source},
 16		at:       base.Add(-time.Duration(minsAgo) * time.Minute),
 17	}
 18}
 19
 20func sources(out []Headline) string {
 21	var b strings.Builder
 22	for i, h := range out {
 23		if i > 0 {
 24			b.WriteString(" ")
 25		}
 26		b.WriteString(h.Source)
 27	}
 28	return b.String()
 29}
 30
 31// The panel reads down newest to oldest, which is the whole shape Isaac asked
 32// for, and the cap must not reorder anything on the way there.
 33func TestPickReadsNewestFirst(t *testing.T) {
 34	all := []dated{row("NPR", 5), row("BBC", 20), row("NPR", 40), row("BBC", 90)}
 35
 36	out := pick(all)
 37	if got := sources(out); got != "NPR BBC NPR BBC" {
 38		t.Errorf("want the input order back, got %v", got)
 39	}
 40}
 41
 42func TestPickFillsThePanel(t *testing.T) {
 43	var all []dated
 44	for i := 0; i < 40; i++ {
 45		all = append(all, row("BBC", i))
 46		all = append(all, row("NPR", i))
 47	}
 48
 49	out := pick(all)
 50	if len(out) != wireShown {
 51		t.Fatalf("want %d rows, got %d", wireShown, len(out))
 52	}
 53}
 54
 55// BBC supplies two of the three feeds and posts more often, so without the cap
 56// a busy afternoon is a column of one outlet.
 57func TestPickCapsOneOutlet(t *testing.T) {
 58	var all []dated
 59	for i := 0; i < 20; i++ {
 60		all = append(all, row("BBC", i))
 61	}
 62	for i := 0; i < 20; i++ {
 63		all = append(all, row("NPR", i+30))
 64	}
 65
 66	out := pick(all)
 67	bbc := 0
 68	for _, h := range out {
 69		if h.Source == "BBC" {
 70			bbc++
 71		}
 72	}
 73	if bbc > wirePerSource {
 74		t.Errorf("BBC took %d rows, cap is %d: %v", bbc, wirePerSource, sources(out))
 75	}
 76}
 77
 78// The cap is a preference, not a reason to serve a short panel.
 79func TestPickFillsPastTheCapWhenItHasTo(t *testing.T) {
 80	var all []dated
 81	for i := 0; i < 14; i++ {
 82		all = append(all, row("NPR", i))
 83	}
 84
 85	out := pick(all)
 86	if len(out) != wireShown {
 87		t.Fatalf("want %d rows from a single outlet, got %d", wireShown, len(out))
 88	}
 89	// The fill has to keep the panel in time order rather than append what the
 90	// cap passed over to the bottom.
 91	// row names each headline in age order, so the panel is in time order when
 92	// the titles come back ascending.
 93	for i := 1; i < len(out); i++ {
 94		if out[i-1].Title > out[i].Title {
 95			t.Fatalf("rows are out of order: %v", sources(out))
 96		}
 97	}
 98}
 99
100func TestPickTakesWhatItHasWhenTheresLittle(t *testing.T) {
101	all := []dated{row("NPR", 1), row("BBC", 2)}
102
103	out := pick(all)
104	if len(out) != 2 {
105		t.Fatalf("want 2 rows, got %d", len(out))
106	}
107}
108
109func TestPromotionalKeepsRealNews(t *testing.T) {
110	keep := []string{
111		"Congress averts a government shutdown ahead of the midterms",
112		"Federal Reserve issues FOMC statement",
113		"Germany says Russia behind Leipzig airport drone attack",
114	}
115	for _, title := range keep {
116		if promotional(title) || sidebar(title) {
117			t.Errorf("filtered real news: %q", title)
118		}
119	}
120
121	drop := []string{
122		"Sign up for our daily briefing",
123		"Sponsored: the best cash back cards",
124		"Prime Day deals you can still get",
125	}
126	for _, title := range drop {
127		if !promotional(title) {
128			t.Errorf("kept an ad: %q", title)
129		}
130	}
131}
132
133// BBC mixes video and rolling live pages into its news feeds under a headline
134// that reads like a story.
135func TestSidebarDropsTheNonStories(t *testing.T) {
136	drop := []string{
137		"Watch: Defence calls for one juror to be dismissed",
138		"In pictures: the storm that flooded Toronto",
139		"Live updates: the Fed decision",
140	}
141	for _, title := range drop {
142		if !sidebar(title) {
143			t.Errorf("kept a non story: %q", title)
144		}
145	}
146}
147
148// NPR's feeds carry the same story under the same headline, and two outlets
149// carry it under headlines that differ by punctuation.
150func TestTitleKeyMatchesAcrossFeeds(t *testing.T) {
151	a := "Congress averts a government shutdown ahead of the midterms"
152	b := "Congress averts a government shutdown ahead of the midterms."
153	if titleKey(a) != titleKey(b) {
154		t.Errorf("same story read as two: %q vs %q", titleKey(a), titleKey(b))
155	}
156	if titleKey(a) == titleKey("Germany says Russia behind Leipzig drone attack") {
157		t.Error("two stories read as one")
158	}
159}
160
161func TestParseRSSTimeReadsBothFeedFormats(t *testing.T) {
162	for _, v := range []string{"Thu, 03 Sep 2026 22:31:23 GMT", "Thu, 03 Sep 2026 18:19:02 -0400"} {
163		got, err := parseRSSTime(v)
164		if err != nil {
165			t.Fatalf("pubDate %q did not parse: %v", v, err)
166		}
167		if got.Year() != 2026 || got.Month() != time.September || got.Day() != 3 {
168			t.Errorf("%q parsed to %v", v, got)
169		}
170	}
171}
172
173func TestClipDropsBBCVideoAndLive(t *testing.T) {
174	if !clip("https://www.bbc.co.uk/news/videos/ce30lqln299o?at_medium=RSS") {
175		t.Error("kept a video clip")
176	}
177	if !clip("https://www.bbc.co.uk/news/live/cx2g5nrpz4vt?at_medium=RSS") {
178		t.Error("kept a live page")
179	}
180	if clip("https://www.bbc.co.uk/news/articles/cj06q4ynpmjo?at_medium=RSS") {
181		t.Error("dropped a story")
182	}
183}
184
185// Two outlets word the same story differently, which is what the exact title
186// key cannot catch, and a duplicate row is the most obvious thing on a panel
187// of ten.
188func TestDedupeCatchesTheSameStoryTwice(t *testing.T) {
189	npr := "Leon Black defies subpoena to testify in Epstein inquiry and sues House panel"
190	bbc := "US billionaire Leon Black defies summons and sues Epstein panel"
191	if !sameStory(significant(npr), significant(bbc)) {
192		t.Errorf("read one story as two: %q and %q", npr, bbc)
193	}
194
195	// Same day, same president, different stories.
196	a := "Trump asks Supreme Court to lift block on USPS plan to restrict mail voting"
197	b := "Trump $1 coin makes him first living president on US currency in a century"
198	if sameStory(significant(a), significant(b)) {
199		t.Errorf("read two stories as one: %q and %q", a, b)
200	}
201}
202
203func TestDedupeKeepsTheNewerCopy(t *testing.T) {
204	newer := row("BBC", 5)
205	newer.Title = "US billionaire Leon Black defies summons and sues Epstein panel"
206	older := row("NPR", 60)
207	older.Title = "Leon Black defies subpoena to testify in Epstein inquiry and sues House panel"
208
209	out := dedupe([]dated{newer, older})
210	if len(out) != 1 {
211		t.Fatalf("want 1 row, got %d", len(out))
212	}
213	if out[0].Source != "BBC" {
214		t.Errorf("want the newer copy, got %q", out[0].Source)
215	}
216}
217
218// An outlet runs an explainer beside the story it explains, and the panel wants
219// the story.
220func TestSidebarDropsTheExplainer(t *testing.T) {
221	if !sidebar("Could Lindsay Clancy trial end in a mistrial? Here are the jury's options") {
222		t.Error("kept an explainer")
223	}
224	if sidebar("Trump asks Supreme Court to lift block on USPS plan") {
225		t.Error("dropped a story")
226	}
227}
228
229// One outlet writes charged where the other writes charges, and the stem is
230// what makes those the same word.
231func TestDedupeCatchesTheSameStoryWordedApart(t *testing.T) {
232	bbc := "ICE agent charged with lying about shooting Venezuelan man during crackdown"
233	npr := "ICE officer faces federal charges for lying over shooting of Venezuelan immigrant"
234	if !sameStory(significant(bbc), significant(npr)) {
235		t.Errorf("read one story as two: %q and %q", bbc, npr)
236	}
237
238	// Three angles on one death are three stories and the panel keeps them all.
239	a := "Tributes to Gloria Steinem are flooding in, from Hollywood to Capitol Hill"
240	b := "Feminist activist and journalist Gloria Steinem dies, aged 92"
241	if sameStory(significant(a), significant(b)) {
242		t.Errorf("read two stories as one: %q and %q", a, b)
243	}
244}