A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "Coves/internal/api/handlers/discover"
5 "Coves/internal/db/postgres"
6 "context"
7 "encoding/json"
8 "fmt"
9 "net/http"
10 "net/http/httptest"
11 "testing"
12 "time"
13
14 discoverCore "Coves/internal/core/discover"
15
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18)
19
20// TestGetDiscover_ShowsAllCommunities tests discover feed shows posts from ALL communities
21func TestGetDiscover_ShowsAllCommunities(t *testing.T) {
22 if testing.Short() {
23 t.Skip("Skipping integration test in short mode")
24 }
25
26 db := setupTestDB(t)
27 t.Cleanup(func() { _ = db.Close() })
28
29 // Setup services
30 discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
31 discoverService := discoverCore.NewDiscoverService(discoverRepo)
32 handler := discover.NewGetDiscoverHandler(discoverService)
33
34 ctx := context.Background()
35 testID := time.Now().UnixNano()
36
37 // Create three communities
38 community1DID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("gaming-%d", testID), fmt.Sprintf("alice-%d.test", testID))
39 require.NoError(t, err)
40
41 community2DID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("tech-%d", testID), fmt.Sprintf("bob-%d.test", testID))
42 require.NoError(t, err)
43
44 community3DID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("cooking-%d", testID), fmt.Sprintf("charlie-%d.test", testID))
45 require.NoError(t, err)
46
47 // Create posts in all three communities
48 post1URI := createTestPost(t, db, community1DID, "did:plc:alice", "Gaming post", 50, time.Now().Add(-1*time.Hour))
49 post2URI := createTestPost(t, db, community2DID, "did:plc:bob", "Tech post", 30, time.Now().Add(-2*time.Hour))
50 post3URI := createTestPost(t, db, community3DID, "did:plc:charlie", "Cooking post", 100, time.Now().Add(-30*time.Minute))
51
52 // Request discover feed (no auth required!)
53 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=50", nil)
54 rec := httptest.NewRecorder()
55 handler.HandleGetDiscover(rec, req)
56
57 // Assertions
58 assert.Equal(t, http.StatusOK, rec.Code)
59
60 var response discoverCore.DiscoverResponse
61 err = json.Unmarshal(rec.Body.Bytes(), &response)
62 require.NoError(t, err)
63
64 // Verify all our posts are present (may include posts from other tests)
65 uris := make(map[string]bool)
66 for _, post := range response.Feed {
67 uris[post.Post.URI] = true
68 }
69 assert.True(t, uris[post1URI], "Should contain gaming post")
70 assert.True(t, uris[post2URI], "Should contain tech post")
71 assert.True(t, uris[post3URI], "Should contain cooking post")
72
73 // Verify newest post appears before older posts in the feed
74 var post3Index, post1Index, post2Index int = -1, -1, -1
75 for i, post := range response.Feed {
76 switch post.Post.URI {
77 case post3URI:
78 post3Index = i
79 case post1URI:
80 post1Index = i
81 case post2URI:
82 post2Index = i
83 }
84 }
85 if post3Index >= 0 && post1Index >= 0 && post2Index >= 0 {
86 assert.Less(t, post3Index, post1Index, "Newest post (30min ago) should appear before 1hr old post")
87 assert.Less(t, post1Index, post2Index, "1hr old post should appear before 2hr old post")
88 }
89}
90
91// TestGetDiscover_NoAuthRequired tests discover feed works without authentication
92func TestGetDiscover_NoAuthRequired(t *testing.T) {
93 if testing.Short() {
94 t.Skip("Skipping integration test in short mode")
95 }
96
97 db := setupTestDB(t)
98 t.Cleanup(func() { _ = db.Close() })
99
100 // Setup services
101 discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
102 discoverService := discoverCore.NewDiscoverService(discoverRepo)
103 handler := discover.NewGetDiscoverHandler(discoverService)
104
105 ctx := context.Background()
106 testID := time.Now().UnixNano()
107
108 // Create community and post
109 communityDID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("public-%d", testID), fmt.Sprintf("alice-%d.test", testID))
110 require.NoError(t, err)
111
112 postURI := createTestPost(t, db, communityDID, "did:plc:alice", "Public post", 10, time.Now())
113
114 // Request discover WITHOUT any authentication
115 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=50", nil)
116 // Note: No auth context set!
117 rec := httptest.NewRecorder()
118 handler.HandleGetDiscover(rec, req)
119
120 // Should succeed without auth
121 assert.Equal(t, http.StatusOK, rec.Code, "Discover should work without authentication")
122
123 var response discoverCore.DiscoverResponse
124 err = json.Unmarshal(rec.Body.Bytes(), &response)
125 require.NoError(t, err)
126
127 // Verify our post is present
128 found := false
129 for _, post := range response.Feed {
130 if post.Post.URI == postURI {
131 found = true
132 break
133 }
134 }
135 assert.True(t, found, "Should show post even without authentication")
136}
137
138// TestGetDiscover_HotSort tests hot sorting across all communities
139func TestGetDiscover_HotSort(t *testing.T) {
140 if testing.Short() {
141 t.Skip("Skipping integration test in short mode")
142 }
143
144 db := setupTestDB(t)
145 t.Cleanup(func() { _ = db.Close() })
146
147 // Setup services
148 discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
149 discoverService := discoverCore.NewDiscoverService(discoverRepo)
150 handler := discover.NewGetDiscoverHandler(discoverService)
151
152 ctx := context.Background()
153 testID := time.Now().UnixNano()
154
155 // Create communities
156 community1DID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("gaming-%d", testID), fmt.Sprintf("alice-%d.test", testID))
157 require.NoError(t, err)
158
159 community2DID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("tech-%d", testID), fmt.Sprintf("bob-%d.test", testID))
160 require.NoError(t, err)
161
162 // Create posts with different scores/ages
163 post1URI := createTestPost(t, db, community1DID, "did:plc:alice", "Recent trending", 50, time.Now().Add(-1*time.Hour))
164 post2URI := createTestPost(t, db, community2DID, "did:plc:bob", "Old popular", 100, time.Now().Add(-24*time.Hour))
165 post3URI := createTestPost(t, db, community1DID, "did:plc:charlie", "Brand new", 5, time.Now().Add(-10*time.Minute))
166
167 // Request hot discover
168 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=hot&limit=50", nil)
169 rec := httptest.NewRecorder()
170 handler.HandleGetDiscover(rec, req)
171
172 assert.Equal(t, http.StatusOK, rec.Code)
173
174 var response discoverCore.DiscoverResponse
175 err = json.Unmarshal(rec.Body.Bytes(), &response)
176 require.NoError(t, err)
177
178 // Verify all our posts are present
179 uris := make(map[string]bool)
180 for _, post := range response.Feed {
181 uris[post.Post.URI] = true
182 }
183 assert.True(t, uris[post1URI], "Should contain recent trending post")
184 assert.True(t, uris[post2URI], "Should contain old popular post")
185 assert.True(t, uris[post3URI], "Should contain brand new post")
186}
187
188// TestGetDiscover_Pagination tests cursor-based pagination
189func TestGetDiscover_Pagination(t *testing.T) {
190 if testing.Short() {
191 t.Skip("Skipping integration test in short mode")
192 }
193
194 db := setupTestDB(t)
195 t.Cleanup(func() { _ = db.Close() })
196
197 // Setup services
198 discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
199 discoverService := discoverCore.NewDiscoverService(discoverRepo)
200 handler := discover.NewGetDiscoverHandler(discoverService)
201
202 ctx := context.Background()
203 testID := time.Now().UnixNano()
204
205 // Create community
206 communityDID, err := createFeedTestCommunity(db, ctx, fmt.Sprintf("test-%d", testID), fmt.Sprintf("alice-%d.test", testID))
207 require.NoError(t, err)
208
209 // Create 5 posts
210 for i := 0; i < 5; i++ {
211 createTestPost(t, db, communityDID, "did:plc:alice", fmt.Sprintf("Post %d", i), 10-i, time.Now().Add(-time.Duration(i)*time.Hour))
212 }
213
214 // First page: limit 2
215 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=2", nil)
216 rec := httptest.NewRecorder()
217 handler.HandleGetDiscover(rec, req)
218
219 assert.Equal(t, http.StatusOK, rec.Code)
220
221 var page1 discoverCore.DiscoverResponse
222 err = json.Unmarshal(rec.Body.Bytes(), &page1)
223 require.NoError(t, err)
224
225 assert.Len(t, page1.Feed, 2, "First page should have 2 posts")
226 assert.NotNil(t, page1.Cursor, "Should have cursor for next page")
227
228 // Second page: use cursor
229 req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/xrpc/social.coves.feed.getDiscover?sort=new&limit=2&cursor=%s", *page1.Cursor), nil)
230 rec = httptest.NewRecorder()
231 handler.HandleGetDiscover(rec, req)
232
233 assert.Equal(t, http.StatusOK, rec.Code)
234
235 var page2 discoverCore.DiscoverResponse
236 err = json.Unmarshal(rec.Body.Bytes(), &page2)
237 require.NoError(t, err)
238
239 assert.Len(t, page2.Feed, 2, "Second page should have 2 posts")
240
241 // Verify no overlap
242 assert.NotEqual(t, page1.Feed[0].Post.URI, page2.Feed[0].Post.URI, "Pages should not overlap")
243}
244
245// TestGetDiscover_LimitValidation tests limit parameter validation
246func TestGetDiscover_LimitValidation(t *testing.T) {
247 if testing.Short() {
248 t.Skip("Skipping integration test in short mode")
249 }
250
251 db := setupTestDB(t)
252 t.Cleanup(func() { _ = db.Close() })
253
254 // Setup services
255 discoverRepo := postgres.NewDiscoverRepository(db, "test-cursor-secret")
256 discoverService := discoverCore.NewDiscoverService(discoverRepo)
257 handler := discover.NewGetDiscoverHandler(discoverService)
258
259 t.Run("Limit exceeds maximum", func(t *testing.T) {
260 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.feed.getDiscover?sort=new&limit=100", nil)
261 rec := httptest.NewRecorder()
262 handler.HandleGetDiscover(rec, req)
263
264 assert.Equal(t, http.StatusBadRequest, rec.Code)
265
266 var errorResp map[string]string
267 err := json.Unmarshal(rec.Body.Bytes(), &errorResp)
268 require.NoError(t, err)
269
270 assert.Equal(t, "InvalidRequest", errorResp["error"])
271 assert.Contains(t, errorResp["message"], "limit")
272 })
273}