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}