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