A community based topic aggregation platform built on atproto
1package actor
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "Coves/internal/core/blueskypost"
11 "Coves/internal/core/posts"
12 "Coves/internal/core/users"
13 "Coves/internal/core/votes"
14
15 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
16)
17
18// mockPostService implements posts.Service for testing
19type mockPostService struct {
20 getAuthorPostsFunc func(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error)
21}
22
23func (m *mockPostService) GetAuthorPosts(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error) {
24 if m.getAuthorPostsFunc != nil {
25 return m.getAuthorPostsFunc(ctx, req)
26 }
27 return &posts.GetAuthorPostsResponse{
28 Feed: []*posts.FeedViewPost{},
29 Cursor: nil,
30 }, nil
31}
32
33func (m *mockPostService) CreatePost(ctx context.Context, req posts.CreatePostRequest) (*posts.CreatePostResponse, error) {
34 return nil, nil
35}
36
37func (m *mockPostService) DeletePost(ctx context.Context, session *oauthlib.ClientSessionData, req posts.DeletePostRequest) error {
38 return nil
39}
40
41// mockUserService implements users.UserService for testing
42type mockUserService struct {
43 resolveHandleToDIDFunc func(ctx context.Context, handle string) (string, error)
44}
45
46func (m *mockUserService) CreateUser(ctx context.Context, req users.CreateUserRequest) (*users.User, error) {
47 return nil, nil
48}
49
50func (m *mockUserService) GetUserByDID(ctx context.Context, did string) (*users.User, error) {
51 return nil, nil
52}
53
54func (m *mockUserService) GetUserByHandle(ctx context.Context, handle string) (*users.User, error) {
55 return nil, nil
56}
57
58func (m *mockUserService) UpdateHandle(ctx context.Context, did, newHandle string) (*users.User, error) {
59 return nil, nil
60}
61
62func (m *mockUserService) ResolveHandleToDID(ctx context.Context, handle string) (string, error) {
63 if m.resolveHandleToDIDFunc != nil {
64 return m.resolveHandleToDIDFunc(ctx, handle)
65 }
66 return "did:plc:testuser", nil
67}
68
69func (m *mockUserService) RegisterAccount(ctx context.Context, req users.RegisterAccountRequest) (*users.RegisterAccountResponse, error) {
70 return nil, nil
71}
72
73func (m *mockUserService) IndexUser(ctx context.Context, did, handle, pdsURL string) error {
74 return nil
75}
76
77func (m *mockUserService) GetProfile(ctx context.Context, did string) (*users.ProfileViewDetailed, error) {
78 return nil, nil
79}
80
81func (m *mockUserService) DeleteAccount(ctx context.Context, did string) error {
82 return nil
83}
84
85func (m *mockUserService) UpdateProfile(ctx context.Context, did string, input users.UpdateProfileInput) (*users.User, error) {
86 return nil, nil
87}
88
89// mockVoteService implements votes.Service for testing
90type mockVoteService struct{}
91
92func (m *mockVoteService) CreateVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
93 return nil, nil
94}
95
96func (m *mockVoteService) DeleteVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error {
97 return nil
98}
99
100func (m *mockVoteService) EnsureCachePopulated(ctx context.Context, session *oauthlib.ClientSessionData) error {
101 return nil
102}
103
104func (m *mockVoteService) GetViewerVote(userDID, subjectURI string) *votes.CachedVote {
105 return nil
106}
107
108func (m *mockVoteService) GetViewerVotesForSubjects(userDID string, subjectURIs []string) map[string]*votes.CachedVote {
109 return nil
110}
111
112// mockBlueskyService implements blueskypost.Service for testing
113type mockBlueskyService struct{}
114
115func (m *mockBlueskyService) ResolvePost(ctx context.Context, atURI string) (*blueskypost.BlueskyPostResult, error) {
116 return nil, nil
117}
118
119func (m *mockBlueskyService) ParseBlueskyURL(ctx context.Context, url string) (string, error) {
120 return "", nil
121}
122
123func (m *mockBlueskyService) IsBlueskyURL(url string) bool {
124 return false
125}
126
127func TestGetPostsHandler_Success(t *testing.T) {
128 mockPosts := &mockPostService{
129 getAuthorPostsFunc: func(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error) {
130 return &posts.GetAuthorPostsResponse{
131 Feed: []*posts.FeedViewPost{
132 {
133 Post: &posts.PostView{
134 URI: "at://did:plc:testuser/social.coves.community.post/abc123",
135 CID: "bafytest123",
136 },
137 },
138 },
139 }, nil
140 },
141 }
142 mockUsers := &mockUserService{}
143 mockVotes := &mockVoteService{}
144 mockBluesky := &mockBlueskyService{}
145
146 handler := NewGetPostsHandler(mockPosts, mockUsers, mockVotes, mockBluesky)
147
148 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=did:plc:testuser", nil)
149 rec := httptest.NewRecorder()
150
151 handler.HandleGetPosts(rec, req)
152
153 if rec.Code != http.StatusOK {
154 t.Errorf("Expected status 200, got %d", rec.Code)
155 }
156
157 var response posts.GetAuthorPostsResponse
158 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
159 t.Fatalf("Failed to decode response: %v", err)
160 }
161
162 if len(response.Feed) != 1 {
163 t.Errorf("Expected 1 post in feed, got %d", len(response.Feed))
164 }
165}
166
167func TestGetPostsHandler_MissingActorParameter(t *testing.T) {
168 handler := NewGetPostsHandler(&mockPostService{}, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
169
170 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts", nil)
171 rec := httptest.NewRecorder()
172
173 handler.HandleGetPosts(rec, req)
174
175 if rec.Code != http.StatusBadRequest {
176 t.Errorf("Expected status 400, got %d", rec.Code)
177 }
178
179 var response ErrorResponse
180 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
181 t.Fatalf("Failed to decode response: %v", err)
182 }
183
184 if response.Error != "InvalidRequest" {
185 t.Errorf("Expected error 'InvalidRequest', got '%s'", response.Error)
186 }
187}
188
189func TestGetPostsHandler_InvalidLimitParameter(t *testing.T) {
190 handler := NewGetPostsHandler(&mockPostService{}, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
191
192 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=did:plc:test&limit=abc", nil)
193 rec := httptest.NewRecorder()
194
195 handler.HandleGetPosts(rec, req)
196
197 if rec.Code != http.StatusBadRequest {
198 t.Errorf("Expected status 400, got %d", rec.Code)
199 }
200
201 var response ErrorResponse
202 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
203 t.Fatalf("Failed to decode response: %v", err)
204 }
205
206 if response.Error != "InvalidRequest" {
207 t.Errorf("Expected error 'InvalidRequest', got '%s'", response.Error)
208 }
209}
210
211func TestGetPostsHandler_ActorNotFound(t *testing.T) {
212 mockUsers := &mockUserService{
213 resolveHandleToDIDFunc: func(ctx context.Context, handle string) (string, error) {
214 return "", posts.ErrActorNotFound
215 },
216 }
217
218 handler := NewGetPostsHandler(&mockPostService{}, mockUsers, &mockVoteService{}, &mockBlueskyService{})
219
220 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=nonexistent.user", nil)
221 rec := httptest.NewRecorder()
222
223 handler.HandleGetPosts(rec, req)
224
225 if rec.Code != http.StatusNotFound {
226 t.Errorf("Expected status 404, got %d", rec.Code)
227 }
228}
229
230func TestGetPostsHandler_ActorLengthExceedsMax(t *testing.T) {
231 handler := NewGetPostsHandler(&mockPostService{}, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
232
233 // Create an actor parameter that exceeds 2048 characters using valid URL characters
234 longActorBytes := make([]byte, 2100)
235 for i := range longActorBytes {
236 longActorBytes[i] = 'a'
237 }
238 longActor := "did:plc:" + string(longActorBytes)
239 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor="+longActor, nil)
240 rec := httptest.NewRecorder()
241
242 handler.HandleGetPosts(rec, req)
243
244 if rec.Code != http.StatusBadRequest {
245 t.Errorf("Expected status 400, got %d", rec.Code)
246 }
247}
248
249func TestGetPostsHandler_InvalidCursor(t *testing.T) {
250 mockPosts := &mockPostService{
251 getAuthorPostsFunc: func(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error) {
252 return nil, posts.ErrInvalidCursor
253 },
254 }
255
256 handler := NewGetPostsHandler(mockPosts, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
257
258 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=did:plc:test&cursor=invalid", nil)
259 rec := httptest.NewRecorder()
260
261 handler.HandleGetPosts(rec, req)
262
263 if rec.Code != http.StatusBadRequest {
264 t.Errorf("Expected status 400, got %d", rec.Code)
265 }
266
267 var response ErrorResponse
268 if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
269 t.Fatalf("Failed to decode response: %v", err)
270 }
271
272 if response.Error != "InvalidCursor" {
273 t.Errorf("Expected error 'InvalidCursor', got '%s'", response.Error)
274 }
275}
276
277func TestGetPostsHandler_MethodNotAllowed(t *testing.T) {
278 handler := NewGetPostsHandler(&mockPostService{}, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
279
280 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.actor.getPosts", nil)
281 rec := httptest.NewRecorder()
282
283 handler.HandleGetPosts(rec, req)
284
285 if rec.Code != http.StatusMethodNotAllowed {
286 t.Errorf("Expected status 405, got %d", rec.Code)
287 }
288}
289
290func TestGetPostsHandler_HandleResolution(t *testing.T) {
291 resolvedDID := ""
292 mockPosts := &mockPostService{
293 getAuthorPostsFunc: func(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error) {
294 resolvedDID = req.ActorDID
295 return &posts.GetAuthorPostsResponse{Feed: []*posts.FeedViewPost{}}, nil
296 },
297 }
298 mockUsers := &mockUserService{
299 resolveHandleToDIDFunc: func(ctx context.Context, handle string) (string, error) {
300 if handle == "test.user" {
301 return "did:plc:resolveduser123", nil
302 }
303 return "", posts.ErrActorNotFound
304 },
305 }
306
307 handler := NewGetPostsHandler(mockPosts, mockUsers, &mockVoteService{}, &mockBlueskyService{})
308
309 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=test.user", nil)
310 rec := httptest.NewRecorder()
311
312 handler.HandleGetPosts(rec, req)
313
314 if rec.Code != http.StatusOK {
315 t.Errorf("Expected status 200, got %d", rec.Code)
316 }
317
318 if resolvedDID != "did:plc:resolveduser123" {
319 t.Errorf("Expected resolved DID 'did:plc:resolveduser123', got '%s'", resolvedDID)
320 }
321}
322
323func TestGetPostsHandler_DirectDIDPassthrough(t *testing.T) {
324 receivedDID := ""
325 mockPosts := &mockPostService{
326 getAuthorPostsFunc: func(ctx context.Context, req posts.GetAuthorPostsRequest) (*posts.GetAuthorPostsResponse, error) {
327 receivedDID = req.ActorDID
328 return &posts.GetAuthorPostsResponse{Feed: []*posts.FeedViewPost{}}, nil
329 },
330 }
331
332 handler := NewGetPostsHandler(mockPosts, &mockUserService{}, &mockVoteService{}, &mockBlueskyService{})
333
334 // When actor is already a DID, it should pass through without resolution
335 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.actor.getPosts?actor=did:plc:directuser", nil)
336 rec := httptest.NewRecorder()
337
338 handler.HandleGetPosts(rec, req)
339
340 if rec.Code != http.StatusOK {
341 t.Errorf("Expected status 200, got %d", rec.Code)
342 }
343
344 if receivedDID != "did:plc:directuser" {
345 t.Errorf("Expected DID 'did:plc:directuser', got '%s'", receivedDID)
346 }
347}