A community based topic aggregation platform built on atproto
at main 347 lines 11 kB view raw
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}