A community based topic aggregation platform built on atproto
1package integration 2 3import ( 4 "Coves/internal/api/handlers/post" 5 "Coves/internal/api/middleware" 6 "Coves/internal/core/communities" 7 "Coves/internal/core/posts" 8 "Coves/internal/db/postgres" 9 "bytes" 10 "context" 11 "encoding/json" 12 "net/http" 13 "net/http/httptest" 14 "testing" 15 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18) 19 20// createTestCommunityWithCredentials creates a test community with valid PDS credentials 21func createTestCommunityWithCredentials(t *testing.T, repo communities.Repository, suffix string) *communities.Community { 22 t.Helper() 23 24 community := &communities.Community{ 25 DID: "did:plc:testcommunity" + suffix, 26 Name: "test-community-" + suffix, 27 Handle: "test-community-" + suffix + ".communities.coves.local", 28 Description: "Test community for thumb validation", 29 Visibility: "public", 30 PDSEmail: "test@communities.coves.local", 31 PDSPassword: "test-password", 32 PDSAccessToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOnRlc3Rjb21tdW5pdHkxMjMiLCJleHAiOjk5OTk5OTk5OTl9.test", 33 PDSRefreshToken: "refresh_token_test123", 34 PDSURL: "http://localhost:3001", 35 } 36 37 created, err := repo.Create(context.Background(), community) 38 require.NoError(t, err) 39 40 return created 41} 42 43// TestPostHandler_ThumbValidation tests strict validation of thumb field in external embeds 44func TestPostHandler_ThumbValidation(t *testing.T) { 45 if testing.Short() { 46 t.Skip("Skipping integration test in short mode") 47 } 48 49 db := setupTestDB(t) 50 defer func() { 51 if err := db.Close(); err != nil { 52 t.Logf("Failed to close database: %v", err) 53 } 54 }() 55 56 // Setup services 57 communityRepo := postgres.NewCommunityRepository(db) 58 communityService := communities.NewCommunityService( 59 communityRepo, 60 "http://localhost:3001", 61 "did:web:test.coves.social", 62 "test.coves.social", 63 nil, 64 ) 65 66 postRepo := postgres.NewPostRepository(db) 67 // No blobService or unfurlService for these validation tests 68 postService := posts.NewPostService(postRepo, communityService, nil, nil, nil, "http://localhost:3001") 69 70 handler := post.NewCreateHandler(postService) 71 72 // Create test user and community with PDS credentials (use unique IDs) 73 testUser := createTestUser(t, db, "thumbtest.bsky.social", "did:plc:thumbtest"+t.Name()) 74 testCommunity := createTestCommunityWithCredentials(t, communityRepo, t.Name()) 75 76 t.Run("Reject thumb as URL string", func(t *testing.T) { 77 payload := map[string]interface{}{ 78 "community": testCommunity.DID, 79 "title": "Test Post", 80 "content": "Test content", 81 "embed": map[string]interface{}{ 82 "$type": "social.coves.embed.external", 83 "external": map[string]interface{}{ 84 "uri": "https://streamable.com/test", 85 "thumb": "https://example.com/thumb.jpg", // ❌ URL string (invalid) 86 }, 87 }, 88 } 89 90 body, _ := json.Marshal(payload) 91 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 92 93 // Mock authenticated user context 94 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 95 req = req.WithContext(ctx) 96 97 rec := httptest.NewRecorder() 98 handler.HandleCreate(rec, req) 99 100 // Should return 400 Bad Request 101 assert.Equal(t, http.StatusBadRequest, rec.Code) 102 103 var errResp map[string]interface{} 104 err := json.Unmarshal(rec.Body.Bytes(), &errResp) 105 require.NoError(t, err) 106 107 assert.Contains(t, errResp["message"], "thumb must be a blob reference") 108 assert.Contains(t, errResp["message"], "not URL string") 109 }) 110 111 t.Run("Reject thumb missing $type", func(t *testing.T) { 112 payload := map[string]interface{}{ 113 "community": testCommunity.DID, 114 "title": "Test Post", 115 "embed": map[string]interface{}{ 116 "$type": "social.coves.embed.external", 117 "external": map[string]interface{}{ 118 "uri": "https://streamable.com/test", 119 "thumb": map[string]interface{}{ // ❌ Missing $type 120 "ref": map[string]interface{}{"$link": "bafyrei123"}, 121 "mimeType": "image/jpeg", 122 "size": 12345, 123 }, 124 }, 125 }, 126 } 127 128 body, _ := json.Marshal(payload) 129 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 130 131 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 132 req = req.WithContext(ctx) 133 134 rec := httptest.NewRecorder() 135 handler.HandleCreate(rec, req) 136 137 assert.Equal(t, http.StatusBadRequest, rec.Code) 138 139 var errResp map[string]interface{} 140 err := json.Unmarshal(rec.Body.Bytes(), &errResp) 141 require.NoError(t, err) 142 143 assert.Contains(t, errResp["message"], "thumb must have $type: blob") 144 }) 145 146 t.Run("Reject thumb missing ref field", func(t *testing.T) { 147 payload := map[string]interface{}{ 148 "community": testCommunity.DID, 149 "title": "Test Post", 150 "embed": map[string]interface{}{ 151 "$type": "social.coves.embed.external", 152 "external": map[string]interface{}{ 153 "uri": "https://streamable.com/test", 154 "thumb": map[string]interface{}{ 155 "$type": "blob", 156 // ❌ Missing ref field 157 "mimeType": "image/jpeg", 158 "size": 12345, 159 }, 160 }, 161 }, 162 } 163 164 body, _ := json.Marshal(payload) 165 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 166 167 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 168 req = req.WithContext(ctx) 169 170 rec := httptest.NewRecorder() 171 handler.HandleCreate(rec, req) 172 173 assert.Equal(t, http.StatusBadRequest, rec.Code) 174 175 var errResp map[string]interface{} 176 err := json.Unmarshal(rec.Body.Bytes(), &errResp) 177 require.NoError(t, err) 178 179 assert.Contains(t, errResp["message"], "thumb blob missing required 'ref' field") 180 }) 181 182 t.Run("Reject thumb missing mimeType field", func(t *testing.T) { 183 payload := map[string]interface{}{ 184 "community": testCommunity.DID, 185 "title": "Test Post", 186 "embed": map[string]interface{}{ 187 "$type": "social.coves.embed.external", 188 "external": map[string]interface{}{ 189 "uri": "https://streamable.com/test", 190 "thumb": map[string]interface{}{ 191 "$type": "blob", 192 "ref": map[string]interface{}{"$link": "bafyrei123"}, 193 // ❌ Missing mimeType field 194 "size": 12345, 195 }, 196 }, 197 }, 198 } 199 200 body, _ := json.Marshal(payload) 201 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 202 203 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 204 req = req.WithContext(ctx) 205 206 rec := httptest.NewRecorder() 207 handler.HandleCreate(rec, req) 208 209 assert.Equal(t, http.StatusBadRequest, rec.Code) 210 211 var errResp map[string]interface{} 212 err := json.Unmarshal(rec.Body.Bytes(), &errResp) 213 require.NoError(t, err) 214 215 assert.Contains(t, errResp["message"], "thumb blob missing required 'mimeType' field") 216 }) 217 218 t.Run("Accept valid blob reference", func(t *testing.T) { 219 // Note: This test will fail at PDS write because the blob doesn't actually exist 220 // But it validates that our thumb validation accepts properly formatted blobs 221 payload := map[string]interface{}{ 222 "community": testCommunity.DID, 223 "title": "Test Post", 224 "embed": map[string]interface{}{ 225 "$type": "social.coves.embed.external", 226 "external": map[string]interface{}{ 227 "uri": "https://streamable.com/test", 228 "thumb": map[string]interface{}{ // ✅ Valid blob 229 "$type": "blob", 230 "ref": map[string]interface{}{"$link": "bafyreib6tbnql2ux3whnfysbzabthaj2vvck53nimhbi5g5a7jgvgr5eqm"}, 231 "mimeType": "image/jpeg", 232 "size": 52813, 233 }, 234 }, 235 }, 236 } 237 238 body, _ := json.Marshal(payload) 239 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 240 241 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 242 req = req.WithContext(ctx) 243 244 rec := httptest.NewRecorder() 245 handler.HandleCreate(rec, req) 246 247 // Should not fail with thumb validation error 248 // (May fail later at PDS write, but that's expected for test data) 249 if rec.Code == http.StatusBadRequest { 250 var errResp map[string]interface{} 251 _ = json.Unmarshal(rec.Body.Bytes(), &errResp) 252 // If it's a bad request, it should NOT be about thumb validation 253 assert.NotContains(t, errResp["message"], "thumb must be") 254 assert.NotContains(t, errResp["message"], "thumb blob missing") 255 } 256 }) 257 258 t.Run("Accept missing thumb (unfurl will handle)", func(t *testing.T) { 259 payload := map[string]interface{}{ 260 "community": testCommunity.DID, 261 "title": "Test Post", 262 "embed": map[string]interface{}{ 263 "$type": "social.coves.embed.external", 264 "external": map[string]interface{}{ 265 "uri": "https://streamable.com/test", 266 // ✅ No thumb field - unfurl service will handle 267 }, 268 }, 269 } 270 271 body, _ := json.Marshal(payload) 272 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body)) 273 274 ctx := middleware.SetTestUserDID(req.Context(), testUser.DID) 275 req = req.WithContext(ctx) 276 277 rec := httptest.NewRecorder() 278 handler.HandleCreate(rec, req) 279 280 // Should not fail with thumb validation error 281 if rec.Code == http.StatusBadRequest { 282 var errResp map[string]interface{} 283 _ = json.Unmarshal(rec.Body.Bytes(), &errResp) 284 // Should not be a thumb validation error 285 assert.NotContains(t, errResp["message"], "thumb must be") 286 } 287 }) 288}