···
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"
16
+
"github.com/stretchr/testify/assert"
17
+
"github.com/stretchr/testify/require"
20
+
// createTestCommunityWithCredentials creates a test community with valid PDS credentials
21
+
func createTestCommunityWithCredentials(t *testing.T, repo communities.Repository, suffix string) *communities.Community {
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",
37
+
created, err := repo.Create(context.Background(), community)
38
+
require.NoError(t, err)
43
+
// TestPostHandler_ThumbValidation tests strict validation of thumb field in external embeds
44
+
func TestPostHandler_ThumbValidation(t *testing.T) {
45
+
if testing.Short() {
46
+
t.Skip("Skipping integration test in short mode")
49
+
db := setupTestDB(t)
51
+
if err := db.Close(); err != nil {
52
+
t.Logf("Failed to close database: %v", err)
57
+
communityRepo := postgres.NewCommunityRepository(db)
58
+
communityService := communities.NewCommunityService(
60
+
"http://localhost:3001",
61
+
"did:web:test.coves.social",
62
+
"test.coves.social",
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")
70
+
handler := post.NewCreateHandler(postService)
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())
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)
90
+
body, _ := json.Marshal(payload)
91
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
93
+
// Mock authenticated user context
94
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
95
+
req = req.WithContext(ctx)
97
+
rec := httptest.NewRecorder()
98
+
handler.HandleCreate(rec, req)
100
+
// Should return 400 Bad Request
101
+
assert.Equal(t, http.StatusBadRequest, rec.Code)
103
+
var errResp map[string]interface{}
104
+
err := json.Unmarshal(rec.Body.Bytes(), &errResp)
105
+
require.NoError(t, err)
107
+
assert.Contains(t, errResp["message"], "thumb must be a blob reference")
108
+
assert.Contains(t, errResp["message"], "not URL string")
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",
128
+
body, _ := json.Marshal(payload)
129
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
131
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
132
+
req = req.WithContext(ctx)
134
+
rec := httptest.NewRecorder()
135
+
handler.HandleCreate(rec, req)
137
+
assert.Equal(t, http.StatusBadRequest, rec.Code)
139
+
var errResp map[string]interface{}
140
+
err := json.Unmarshal(rec.Body.Bytes(), &errResp)
141
+
require.NoError(t, err)
143
+
assert.Contains(t, errResp["message"], "thumb must have $type: blob")
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{}{
156
+
// ❌ Missing ref field
157
+
"mimeType": "image/jpeg",
164
+
body, _ := json.Marshal(payload)
165
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
167
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
168
+
req = req.WithContext(ctx)
170
+
rec := httptest.NewRecorder()
171
+
handler.HandleCreate(rec, req)
173
+
assert.Equal(t, http.StatusBadRequest, rec.Code)
175
+
var errResp map[string]interface{}
176
+
err := json.Unmarshal(rec.Body.Bytes(), &errResp)
177
+
require.NoError(t, err)
179
+
assert.Contains(t, errResp["message"], "thumb blob missing required 'ref' field")
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{}{
192
+
"ref": map[string]interface{}{"$link": "bafyrei123"},
193
+
// ❌ Missing mimeType field
200
+
body, _ := json.Marshal(payload)
201
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
203
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
204
+
req = req.WithContext(ctx)
206
+
rec := httptest.NewRecorder()
207
+
handler.HandleCreate(rec, req)
209
+
assert.Equal(t, http.StatusBadRequest, rec.Code)
211
+
var errResp map[string]interface{}
212
+
err := json.Unmarshal(rec.Body.Bytes(), &errResp)
213
+
require.NoError(t, err)
215
+
assert.Contains(t, errResp["message"], "thumb blob missing required 'mimeType' field")
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
230
+
"ref": map[string]interface{}{"$link": "bafyreib6tbnql2ux3whnfysbzabthaj2vvck53nimhbi5g5a7jgvgr5eqm"},
231
+
"mimeType": "image/jpeg",
238
+
body, _ := json.Marshal(payload)
239
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
241
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
242
+
req = req.WithContext(ctx)
244
+
rec := httptest.NewRecorder()
245
+
handler.HandleCreate(rec, req)
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")
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
271
+
body, _ := json.Marshal(payload)
272
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.community.post.create", bytes.NewReader(body))
274
+
ctx := middleware.SetTestUserDID(req.Context(), testUser.DID)
275
+
req = req.WithContext(ctx)
277
+
rec := httptest.NewRecorder()
278
+
handler.HandleCreate(rec, req)
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")