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}