package storage import "testing" func seedStory(t *testing.T, guid string) int64 { t.Helper() s := &Story{ GUID: guid, Headline: "Headline " + guid, Lede: "Lede.", ArticleURL: "https://example.com/" + guid, Source: "Test Source", Channel: "tech", Classified: true, SeenAt: 1700000000, } if err := InsertStory(s); err != nil { t.Fatalf("insert story %s: %v", guid, err) } var id int64 if err := Get().QueryRow(`SELECT id FROM stories WHERE guid = ?`, guid).Scan(&id); err != nil { t.Fatalf("lookup id %s: %v", guid, err) } return id } func TestUserStoryState_ReadAndBookmark(t *testing.T) { setupTestDB(t) id1 := seedStory(t, "s1") id2 := seedStory(t, "s2") const sub = "ak-sub-1" // Nothing set yet. read, marks, err := UserStoryState(sub, []int64{id1, id2}) if err != nil { t.Fatalf("initial state: %v", err) } if len(read) != 0 || len(marks) != 0 { t.Fatalf("expected empty state, got read=%v bookmarked=%v", read, marks) } // Mark id1 read, bookmark id1 and id2. if err := SetRead(sub, id1, true); err != nil { t.Fatalf("set read: %v", err) } if err := SetBookmark(sub, id1, true); err != nil { t.Fatalf("set bookmark id1: %v", err) } if err := SetBookmark(sub, id2, true); err != nil { t.Fatalf("set bookmark id2: %v", err) } read, marks, _ = UserStoryState(sub, []int64{id1, id2}) if !read[id1] || read[id2] { t.Fatalf("read map wrong: %v", read) } if !marks[id1] || !marks[id2] { t.Fatalf("bookmark map wrong: %v", marks) } // id1 carries both flags in a single row. if !read[id1] || !marks[id1] { t.Fatalf("expected id1 read+bookmarked, read=%v marks=%v", read, marks) } // ListBookmarks orders newest bookmark first, tiebreaking on story id so the // result is deterministic within a single second. bm, err := ListBookmarks(sub, 10, 0) if err != nil { t.Fatalf("list bookmarks: %v", err) } if len(bm) != 2 { t.Fatalf("expected 2 bookmarks, got %d", len(bm)) } if bm[0].ID != id2 || bm[1].ID != id1 { t.Fatalf("bookmark order wrong: %d then %d", bm[0].ID, bm[1].ID) } if n, _ := CountBookmarks(sub); n != 2 { t.Fatalf("count bookmarks = %d, want 2", n) } } func TestUserStoryState_ClearRemovesRow(t *testing.T) { setupTestDB(t) id := seedStory(t, "s1") const sub = "ak-sub-1" if err := SetRead(sub, id, true); err != nil { t.Fatalf("set read: %v", err) } if err := SetRead(sub, id, false); err != nil { t.Fatalf("unset read: %v", err) } // Row should be gone since neither flag is set. var n int if err := Get().QueryRow(`SELECT COUNT(*) FROM user_story_state WHERE user_sub = ?`, sub).Scan(&n); err != nil { t.Fatalf("count rows: %v", err) } if n != 0 { t.Fatalf("expected row pruned, found %d", n) } // Setting read then bookmark then clearing only read must keep the row. _ = SetRead(sub, id, true) _ = SetBookmark(sub, id, true) _ = SetRead(sub, id, false) read, marks, _ := UserStoryState(sub, []int64{id}) if read[id] { t.Fatalf("expected not read") } if !marks[id] { t.Fatalf("expected still bookmarked after clearing read") } } func TestUserStoryState_ScopedBySub(t *testing.T) { setupTestDB(t) id := seedStory(t, "s1") _ = SetBookmark("user-a", id, true) read, marks, _ := UserStoryState("user-b", []int64{id}) if len(read) != 0 || len(marks) != 0 { t.Fatalf("user-b should see no state, got read=%v marks=%v", read, marks) } if n, _ := CountBookmarks("user-b"); n != 0 { t.Fatalf("user-b count = %d, want 0", n) } }