main pico / pkg / apps / prose / db_test.go
Antonio Mika  ·  2026-10-10
  1package prose
  2
  3import (
  4	"log/slog"
  5	"testing"
  6	"time"
  7
  8	"github.com/picosh/pico/pkg/db"
  9	"github.com/picosh/pico/pkg/db/postgres"
 10	"github.com/picosh/pico/pkg/db/postgres/pgtest"
 11)
 12
 13var testDB *PsqlDB
 14
 15func TestMain(m *testing.M) {
 16	pgtest.Main(m)
 17}
 18
 19func cleanupTestData(t *testing.T) {
 20	t.Helper()
 21	if testDB == nil {
 22		testDB = NewDB(postgres.NewDB(pgtest.URL(t), slog.New(slog.DiscardHandler)))
 23	}
 24	pgtest.Clean(t, testDB.Db)
 25	testDB.ClearPopularPostsCache()
 26}
 27
 28func mustInsertPost(t *testing.T, post *db.Post) *db.Post {
 29	t.Helper()
 30	return pgtest.InsertPost(t, testDB, post)
 31}
 32
 33func TestFindPopularPosts(t *testing.T) {
 34	cleanupTestData(t)
 35
 36	user, _ := testDB.RegisterUser("feedowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI feedowner", "comment", "")
 37
 38	now := time.Now()
 39	post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedpost.md", Slug: "feedpost", Title: "Feed Post", Space: "prose", PublishAt: &now})
 40
 41	// Before having feature flag, should not appear
 42	posts, err := testDB.FindPopularPosts()
 43	if err != nil {
 44		t.Fatalf("FindPopularPosts failed: %v", err)
 45	}
 46	if len(posts) != 0 {
 47		t.Errorf("expected 0 posts without feature flag, got %d", len(posts))
 48	}
 49
 50	// Add prose feature flag
 51	_, err = testDB.InsertFeature(user.ID, "prose", time.Now().Add(time.Hour*24))
 52	if err != nil {
 53		t.Fatalf("InsertFeature failed: %v", err)
 54	}
 55	testDB.ClearPopularPostsCache()
 56
 57	posts, err = testDB.FindPopularPosts()
 58	if err != nil {
 59		t.Fatalf("FindPopularPosts failed: %v", err)
 60	}
 61	if len(posts) != 1 {
 62		t.Fatalf("expected 1 post in popular posts, got %d", len(posts))
 63	}
 64	if posts[0].Score != 0 {
 65		t.Errorf("expected score 0, got %d", posts[0].Score)
 66	}
 67
 68	// Add visits
 69	_ = testDB.InsertVisit(&db.AnalyticsVisits{UserID: user.ID, PostID: post.ID, IpAddress: "192.168.1.1", Status: 200})
 70	_ = testDB.InsertVisit(&db.AnalyticsVisits{UserID: user.ID, PostID: post.ID, IpAddress: "192.168.1.2", Status: 200})
 71	_ = testDB.InsertVisit(&db.AnalyticsVisits{UserID: user.ID, PostID: post.ID, IpAddress: "192.168.1.1", Status: 200}) // duplicate IP
 72
 73	// Before clearing cache, should return cached result (score 0)
 74	cachedPosts, err := testDB.FindPopularPosts()
 75	if err != nil {
 76		t.Fatalf("FindPopularPosts failed: %v", err)
 77	}
 78	if cachedPosts[0].Score != 0 {
 79		t.Errorf("expected cached score 0, got %d", cachedPosts[0].Score)
 80	}
 81
 82	// After clearing cache, should re-query and return updated score 2
 83	testDB.ClearPopularPostsCache()
 84	posts, err = testDB.FindPopularPosts()
 85	if err != nil {
 86		t.Fatalf("FindPopularPosts failed: %v", err)
 87	}
 88	if len(posts) != 1 {
 89		t.Fatalf("expected 1 post, got %d", len(posts))
 90	}
 91	if posts[0].Score != 2 {
 92		t.Errorf("expected score 2 (unique visitors), got %d", posts[0].Score)
 93	}
 94
 95	// Verify max 3 posts per user
 96	testDB.ClearPopularPostsCache()
 97	post2Time := now.Add(-1 * time.Hour)
 98	_ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedpost2.md", Slug: "feedpost2", Title: "Feed Post 2", Space: "prose", PublishAt: &post2Time})
 99	post3Time := now.Add(-2 * time.Hour)
100	_ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedpost3.md", Slug: "feedpost3", Title: "Feed Post 3", Space: "prose", PublishAt: &post3Time})
101	post4Time := now.Add(-3 * time.Hour)
102	_ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedpost4.md", Slug: "feedpost4", Title: "Feed Post 4", Space: "prose", PublishAt: &post4Time})
103
104	posts, err = testDB.FindPopularPosts()
105	if err != nil {
106		t.Fatalf("FindPopularPosts failed: %v", err)
107	}
108	if len(posts) != 3 {
109		t.Errorf("expected max 3 posts for single user, got %d", len(posts))
110	}
111
112	// Post older than 30 days should be excluded
113	testDB.ClearPopularPostsCache()
114	oldTime := now.Add(-31 * 24 * time.Hour)
115	_ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "oldpost.md", Slug: "oldpost", Title: "Old Post", Space: "prose", PublishAt: &oldTime})
116
117	posts, err = testDB.FindPopularPosts()
118	if err != nil {
119		t.Fatalf("FindPopularPosts failed: %v", err)
120	}
121	if len(posts) != 3 {
122		t.Errorf("expected still 3 posts (old post excluded), got %d", len(posts))
123	}
124}
125
126// ============ Tags Tests ============
127
128func TestFindUserPostsByTag(t *testing.T) {
129	cleanupTestData(t)
130
131	user, _ := testDB.RegisterUser("usertagowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI usertagowner", "comment", "")
132	now := time.Now()
133	post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "usertag.md", Slug: "usertag", Title: "User Tag", Space: "prose", PublishAt: &now})
134	_ = testDB.ReplaceTagsByPost([]string{"mytag"}, post.ID)
135
136	pager := &db.Pager{Num: 10, Page: 0}
137	result, err := testDB.FindUserPostsByTag(pager, "mytag", user.ID, "prose")
138	if err != nil {
139		t.Fatalf("FindUserPostsByTag failed: %v", err)
140	}
141	if len(result.Data) != 1 {
142		t.Errorf("expected 1 post with tag, got %d", len(result.Data))
143	}
144}
145
146func TestFindPostsByTag(t *testing.T) {
147	cleanupTestData(t)
148
149	user, _ := testDB.RegisterUser("tagsearchowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tagsearchowner", "comment", "")
150	now := time.Now()
151	post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "tagsearch.md", Slug: "tagsearch", Title: "Tag Search", Space: "prose", PublishAt: &now})
152	_ = testDB.ReplaceTagsByPost([]string{"searchtag"}, post.ID)
153
154	pager := &db.Pager{Num: 10, Page: 0}
155	result, err := testDB.FindPostsByTag(pager, "searchtag", "prose")
156	if err != nil {
157		t.Fatalf("FindPostsByTag failed: %v", err)
158	}
159	if len(result.Data) != 1 {
160		t.Errorf("expected 1 post with tag, got %d", len(result.Data))
161	}
162}
163
164func TestFindPopularTags(t *testing.T) {
165	cleanupTestData(t)
166
167	user, _ := testDB.RegisterUser("populartagowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI populartagowner", "comment", "")
168	post1 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop1.md", Slug: "pop1", Title: "Pop 1", Space: "prose"})
169	post2 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop2.md", Slug: "pop2", Title: "Pop 2", Space: "prose"})
170	_ = testDB.ReplaceTagsByPost([]string{"popular"}, post1.ID)
171	_ = testDB.ReplaceTagsByPost([]string{"popular"}, post2.ID)
172
173	tags, err := testDB.FindPopularTags("prose")
174	if err != nil {
175		t.Fatalf("FindPopularTags failed: %v", err)
176	}
177	if len(tags) < 1 {
178		t.Errorf("expected at least 1 popular tag, got %d", len(tags))
179	}
180}
181
182// ============ Aliases Tests ============