A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "Coves/internal/api/middleware"
5 "Coves/internal/atproto/identity"
6 "Coves/internal/core/communities"
7 "Coves/internal/core/posts"
8 "Coves/internal/core/users"
9 "Coves/internal/db/postgres"
10 "context"
11 "fmt"
12 "strings"
13 "testing"
14
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17)
18
19func TestPostCreation_Basic(t *testing.T) {
20 if testing.Short() {
21 t.Skip("Skipping integration test in short mode")
22 }
23
24 db := setupTestDB(t)
25 defer func() {
26 if err := db.Close(); err != nil {
27 t.Logf("Failed to close database: %v", err)
28 }
29 }()
30
31 // Setup: Initialize services
32 userRepo := postgres.NewUserRepository(db)
33 resolver := identity.NewResolver(db, identity.DefaultConfig())
34 userService := users.NewUserService(userRepo, resolver, "http://localhost:3001")
35
36 communityRepo := postgres.NewCommunityRepository(db)
37 // Note: Provisioner not needed for this test (we're not actually creating communities)
38 communityService := communities.NewCommunityService(
39 communityRepo,
40 "http://localhost:3001",
41 "did:web:test.coves.social",
42 "test.coves.social",
43 nil, // provisioner
44 )
45
46 postRepo := postgres.NewPostRepository(db)
47 postService := posts.NewPostService(postRepo, communityService, nil, "http://localhost:3001") // nil aggregatorService for user-only tests
48
49 ctx := context.Background()
50
51 // Cleanup: Remove any existing test data
52 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'")
53 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'")
54 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'")
55
56 // Setup: Create test user
57 testUserDID := generateTestDID("postauthor")
58 testUserHandle := "postauthor.test"
59
60 _, err := userService.CreateUser(ctx, users.CreateUserRequest{
61 DID: testUserDID,
62 Handle: testUserHandle,
63 PDSURL: "http://localhost:3001",
64 })
65 require.NoError(t, err, "Failed to create test user")
66
67 // Setup: Create test community (insert directly to DB for speed)
68 testCommunity := &communities.Community{
69 DID: generateTestDID("testcommunity"),
70 Handle: "testcommunity.community.test.coves.social", // Canonical atProto handle (no ! prefix, .community. format)
71 Name: "testcommunity",
72 DisplayName: "Test Community",
73 Description: "A community for testing posts",
74 Visibility: "public",
75 CreatedByDID: testUserDID,
76 HostedByDID: "did:web:test.coves.social",
77 PDSURL: "http://localhost:3001",
78 PDSAccessToken: "fake_token_for_test", // Won't actually call PDS in unit test
79 }
80
81 _, err = communityRepo.Create(ctx, testCommunity)
82 require.NoError(t, err, "Failed to create test community")
83
84 t.Run("Create text post successfully (with DID)", func(t *testing.T) {
85 // NOTE: This test validates the service layer logic only
86 // It will fail when trying to write to PDS because we're using a fake token
87 // For full E2E testing, you'd need a real PDS instance
88
89 content := "This is a test post"
90 title := "Test Post Title"
91
92 req := posts.CreatePostRequest{
93 Community: testCommunity.DID, // Using DID directly
94 Title: &title,
95 Content: &content,
96 AuthorDID: testUserDID,
97 }
98
99 // This will fail at token refresh step (expected for unit test)
100 // We're using a fake token that can't be parsed
101 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
102 _, err := postService.CreatePost(authCtx, req)
103
104 // For now, we expect an error because token is fake
105 // In a full E2E test with real PDS, this would succeed
106 require.Error(t, err)
107 t.Logf("Expected error (fake token): %v", err)
108 // Verify the error is from token refresh or PDS, not validation
109 assert.Contains(t, err.Error(), "failed to refresh community credentials")
110 })
111
112 t.Run("Create text post with community handle", func(t *testing.T) {
113 // Test that we can use community handle instead of DID
114 // This validates at-identifier resolution per atProto best practices
115
116 content := "Post using handle instead of DID"
117 title := "Handle Test"
118
119 req := posts.CreatePostRequest{
120 Community: testCommunity.Handle, // Using canonical atProto handle
121 Title: &title,
122 Content: &content,
123 AuthorDID: testUserDID,
124 }
125
126 // Should resolve handle to DID and proceed
127 // Will still fail at token refresh (expected with fake token)
128 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
129 _, err := postService.CreatePost(authCtx, req)
130 require.Error(t, err)
131 // Should fail at token refresh, not community resolution
132 assert.Contains(t, err.Error(), "failed to refresh community credentials")
133 })
134
135 t.Run("Create text post with ! prefix handle", func(t *testing.T) {
136 // Test that we can also use ! prefix with scoped format: !name@instance
137 // This is Coves-specific UX shorthand for name.community.instance
138
139 content := "Post using !-prefixed handle"
140 title := "Prefixed Handle Test"
141
142 // Extract name from handle: "gardening.community.coves.social" -> "gardening"
143 // Scoped format: !gardening@coves.social
144 handleParts := strings.Split(testCommunity.Handle, ".")
145 communityName := handleParts[0]
146 instanceDomain := strings.Join(handleParts[2:], ".") // Skip ".community."
147 scopedHandle := fmt.Sprintf("!%s@%s", communityName, instanceDomain)
148
149 req := posts.CreatePostRequest{
150 Community: scopedHandle, // !gardening@coves.social
151 Title: &title,
152 Content: &content,
153 AuthorDID: testUserDID,
154 }
155
156 // Should resolve handle to DID and proceed
157 // Will still fail at token refresh (expected with fake token)
158 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
159 _, err := postService.CreatePost(authCtx, req)
160 require.Error(t, err)
161 // Should fail at token refresh, not community resolution
162 assert.Contains(t, err.Error(), "failed to refresh community credentials")
163 })
164
165 t.Run("Reject post with missing community", func(t *testing.T) {
166 content := "Post without community"
167
168 req := posts.CreatePostRequest{
169 Community: "", // Missing!
170 Content: &content,
171 AuthorDID: testUserDID,
172 }
173
174 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
175 _, err := postService.CreatePost(authCtx, req)
176 require.Error(t, err)
177 assert.True(t, posts.IsValidationError(err))
178 })
179
180 t.Run("Reject post with non-existent community handle", func(t *testing.T) {
181 content := "Post with non-existent handle"
182
183 req := posts.CreatePostRequest{
184 Community: "nonexistent.community.test.coves.social", // Valid canonical handle format, but doesn't exist
185 Content: &content,
186 AuthorDID: testUserDID,
187 }
188
189 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
190 _, err := postService.CreatePost(authCtx, req)
191 require.Error(t, err)
192 // Should fail with community not found (wrapped in error)
193 assert.Contains(t, err.Error(), "community not found")
194 })
195
196 t.Run("Reject post with missing author DID", func(t *testing.T) {
197 content := "Post without author"
198
199 req := posts.CreatePostRequest{
200 Community: testCommunity.DID,
201 Content: &content,
202 AuthorDID: "", // Missing!
203 }
204
205 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
206 _, err := postService.CreatePost(authCtx, req)
207 require.Error(t, err)
208 assert.True(t, posts.IsValidationError(err))
209 assert.Contains(t, err.Error(), "authorDid")
210 })
211
212 t.Run("Reject post in non-existent community", func(t *testing.T) {
213 content := "Post in fake community"
214
215 req := posts.CreatePostRequest{
216 Community: "did:plc:nonexistent",
217 Content: &content,
218 AuthorDID: testUserDID,
219 }
220
221 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
222 _, err := postService.CreatePost(authCtx, req)
223 require.Error(t, err)
224 assert.Equal(t, posts.ErrCommunityNotFound, err)
225 })
226
227 t.Run("Reject post with too-long content", func(t *testing.T) {
228 // Create content longer than 50k characters
229 longContent := string(make([]byte, 50001))
230
231 req := posts.CreatePostRequest{
232 Community: testCommunity.DID,
233 Content: &longContent,
234 AuthorDID: testUserDID,
235 }
236
237 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
238 _, err := postService.CreatePost(authCtx, req)
239 require.Error(t, err)
240 assert.True(t, posts.IsValidationError(err))
241 assert.Contains(t, err.Error(), "too long")
242 })
243
244 t.Run("Reject post with invalid content label", func(t *testing.T) {
245 content := "Post with invalid label"
246
247 req := posts.CreatePostRequest{
248 Community: testCommunity.DID,
249 Content: &content,
250 Labels: &posts.SelfLabels{
251 Values: []posts.SelfLabel{
252 {Val: "invalid_label"}, // Not in known values!
253 },
254 },
255 AuthorDID: testUserDID,
256 }
257
258 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
259 _, err := postService.CreatePost(authCtx, req)
260 require.Error(t, err)
261 assert.True(t, posts.IsValidationError(err))
262 assert.Contains(t, err.Error(), "unknown content label")
263 })
264
265 t.Run("Accept post with valid content labels", func(t *testing.T) {
266 content := "Post with valid labels"
267
268 req := posts.CreatePostRequest{
269 Community: testCommunity.DID,
270 Content: &content,
271 Labels: &posts.SelfLabels{
272 Values: []posts.SelfLabel{
273 {Val: "nsfw"},
274 {Val: "spoiler"},
275 },
276 },
277 AuthorDID: testUserDID,
278 }
279
280 // Will fail at token refresh (expected with fake token)
281 authCtx := middleware.SetTestUserDID(ctx, testUserDID)
282 _, err := postService.CreatePost(authCtx, req)
283 require.Error(t, err)
284 // Should fail at token refresh, not validation
285 assert.Contains(t, err.Error(), "failed to refresh community credentials")
286 })
287}
288
289// TestPostRepository_Create tests the repository layer
290func TestPostRepository_Create(t *testing.T) {
291 if testing.Short() {
292 t.Skip("Skipping integration test in short mode")
293 }
294
295 db := setupTestDB(t)
296 defer func() {
297 if err := db.Close(); err != nil {
298 t.Logf("Failed to close database: %v", err)
299 }
300 }()
301
302 // Cleanup first
303 _, _ = db.Exec("DELETE FROM posts WHERE community_did LIKE 'did:plc:test%'")
304 _, _ = db.Exec("DELETE FROM communities WHERE did LIKE 'did:plc:test%'")
305 _, _ = db.Exec("DELETE FROM users WHERE did LIKE 'did:plc:test%'")
306
307 // Setup: Create test user and community
308 ctx := context.Background()
309 userRepo := postgres.NewUserRepository(db)
310 communityRepo := postgres.NewCommunityRepository(db)
311
312 testUserDID := generateTestDID("postauthor2")
313 _, err := userRepo.Create(ctx, &users.User{
314 DID: testUserDID,
315 Handle: "postauthor2.test",
316 PDSURL: "http://localhost:3001",
317 })
318 require.NoError(t, err)
319
320 testCommunityDID := generateTestDID("testcommunity2")
321 _, err = communityRepo.Create(ctx, &communities.Community{
322 DID: testCommunityDID,
323 Handle: "testcommunity2.community.test.coves.social", // Canonical format (no ! prefix)
324 Name: "testcommunity2",
325 Visibility: "public",
326 CreatedByDID: testUserDID,
327 HostedByDID: "did:web:test.coves.social",
328 PDSURL: "http://localhost:3001",
329 })
330 require.NoError(t, err)
331
332 postRepo := postgres.NewPostRepository(db)
333
334 t.Run("Insert post successfully", func(t *testing.T) {
335 content := "Test post content"
336 title := "Test Title"
337
338 post := &posts.Post{
339 URI: "at://" + testCommunityDID + "/social.coves.community.post/test123",
340 CID: "bafy2test123",
341 RKey: "test123",
342 AuthorDID: testUserDID,
343 CommunityDID: testCommunityDID,
344 Title: &title,
345 Content: &content,
346 }
347
348 err := postRepo.Create(ctx, post)
349 require.NoError(t, err)
350 assert.NotZero(t, post.ID, "Post should have ID after insert")
351 assert.NotZero(t, post.IndexedAt, "Post should have IndexedAt timestamp")
352 })
353
354 t.Run("Reject duplicate post URI", func(t *testing.T) {
355 content := "Duplicate post"
356
357 post1 := &posts.Post{
358 URI: "at://" + testCommunityDID + "/social.coves.community.post/duplicate",
359 CID: "bafy2duplicate1",
360 RKey: "duplicate",
361 AuthorDID: testUserDID,
362 CommunityDID: testCommunityDID,
363 Content: &content,
364 }
365
366 err := postRepo.Create(ctx, post1)
367 require.NoError(t, err)
368
369 // Try to insert again with same URI
370 post2 := &posts.Post{
371 URI: "at://" + testCommunityDID + "/social.coves.community.post/duplicate",
372 CID: "bafy2duplicate2",
373 RKey: "duplicate",
374 AuthorDID: testUserDID,
375 CommunityDID: testCommunityDID,
376 Content: &content,
377 }
378
379 err = postRepo.Create(ctx, post2)
380 require.Error(t, err)
381 assert.Contains(t, err.Error(), "already indexed")
382 })
383}