A community based topic aggregation platform built on atproto
1package vote
2
3import (
4 "Coves/internal/api/middleware"
5 "Coves/internal/core/votes"
6 "bytes"
7 "context"
8 "encoding/json"
9 "net/http"
10 "net/http/httptest"
11 "testing"
12
13 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
14 "github.com/bluesky-social/indigo/atproto/syntax"
15)
16
17// mockVoteService implements votes.Service for testing
18type mockVoteService struct {
19 createFunc func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error)
20 deleteFunc func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error
21}
22
23func (m *mockVoteService) CreateVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
24 if m.createFunc != nil {
25 return m.createFunc(ctx, session, req)
26 }
27 return &votes.CreateVoteResponse{
28 URI: "at://did:plc:test123/social.coves.vote/abc123",
29 CID: "bafyvote123",
30 }, nil
31}
32
33func (m *mockVoteService) DeleteVote(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error {
34 if m.deleteFunc != nil {
35 return m.deleteFunc(ctx, session, req)
36 }
37 return nil
38}
39
40func TestCreateVoteHandler_Success(t *testing.T) {
41 mockService := &mockVoteService{}
42 handler := NewCreateVoteHandler(mockService)
43
44 // Create request body
45 reqBody := CreateVoteInput{
46 Subject: struct {
47 URI string `json:"uri"`
48 CID string `json:"cid"`
49 }{
50 URI: "at://did:plc:author123/social.coves.post/xyz789",
51 CID: "bafypost123",
52 },
53 Direction: "up",
54 }
55 bodyBytes, err := json.Marshal(reqBody)
56 if err != nil {
57 t.Fatalf("Failed to marshal request: %v", err)
58 }
59
60 // Create HTTP request
61 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
62 req.Header.Set("Content-Type", "application/json")
63
64 // Inject OAuth session into context (simulates auth middleware)
65 did, _ := syntax.ParseDID("did:plc:test123")
66 session := &oauthlib.ClientSessionData{
67 AccountDID: did,
68 AccessToken: "test_token",
69 }
70 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
71 req = req.WithContext(ctx)
72
73 // Execute handler
74 w := httptest.NewRecorder()
75 handler.HandleCreateVote(w, req)
76
77 // Check status code
78 if w.Code != http.StatusOK {
79 t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
80 }
81
82 // Check response
83 var response CreateVoteOutput
84 if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
85 t.Fatalf("Failed to decode response: %v", err)
86 }
87
88 if response.URI != "at://did:plc:test123/social.coves.vote/abc123" {
89 t.Errorf("Expected URI at://did:plc:test123/social.coves.vote/abc123, got %s", response.URI)
90 }
91 if response.CID != "bafyvote123" {
92 t.Errorf("Expected CID bafyvote123, got %s", response.CID)
93 }
94}
95
96func TestCreateVoteHandler_RequiresAuth(t *testing.T) {
97 mockService := &mockVoteService{}
98 handler := NewCreateVoteHandler(mockService)
99
100 // Create request body
101 reqBody := CreateVoteInput{
102 Subject: struct {
103 URI string `json:"uri"`
104 CID string `json:"cid"`
105 }{
106 URI: "at://did:plc:author123/social.coves.post/xyz789",
107 CID: "bafypost123",
108 },
109 Direction: "up",
110 }
111 bodyBytes, err := json.Marshal(reqBody)
112 if err != nil {
113 t.Fatalf("Failed to marshal request: %v", err)
114 }
115
116 // Create HTTP request without auth context
117 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
118 req.Header.Set("Content-Type", "application/json")
119 // No OAuth session in context
120
121 // Execute handler
122 w := httptest.NewRecorder()
123 handler.HandleCreateVote(w, req)
124
125 // Check status code
126 if w.Code != http.StatusUnauthorized {
127 t.Errorf("Expected status 401, got %d. Body: %s", w.Code, w.Body.String())
128 }
129
130 // Check error response
131 var errResp XRPCError
132 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
133 t.Fatalf("Failed to decode error response: %v", err)
134 }
135 if errResp.Error != "AuthRequired" {
136 t.Errorf("Expected error AuthRequired, got %s", errResp.Error)
137 }
138}
139
140func TestCreateVoteHandler_InvalidDirection(t *testing.T) {
141 mockService := &mockVoteService{}
142 handler := NewCreateVoteHandler(mockService)
143
144 tests := []struct {
145 name string
146 direction string
147 }{
148 {"empty direction", ""},
149 {"invalid direction", "sideways"},
150 {"wrong case", "UP"},
151 {"typo", "upp"},
152 }
153
154 for _, tc := range tests {
155 t.Run(tc.name, func(t *testing.T) {
156 // Create request body
157 reqBody := CreateVoteInput{
158 Subject: struct {
159 URI string `json:"uri"`
160 CID string `json:"cid"`
161 }{
162 URI: "at://did:plc:author123/social.coves.post/xyz789",
163 CID: "bafypost123",
164 },
165 Direction: tc.direction,
166 }
167 bodyBytes, err := json.Marshal(reqBody)
168 if err != nil {
169 t.Fatalf("Failed to marshal request: %v", err)
170 }
171
172 // Create HTTP request
173 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
174 req.Header.Set("Content-Type", "application/json")
175
176 // Inject OAuth session
177 did, _ := syntax.ParseDID("did:plc:test123")
178 session := &oauthlib.ClientSessionData{
179 AccountDID: did,
180 AccessToken: "test_token",
181 }
182 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
183 req = req.WithContext(ctx)
184
185 // Execute handler
186 w := httptest.NewRecorder()
187 handler.HandleCreateVote(w, req)
188
189 // Check status code
190 if w.Code != http.StatusBadRequest {
191 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String())
192 }
193
194 // Check error response
195 var errResp XRPCError
196 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
197 t.Fatalf("Failed to decode error response: %v", err)
198 }
199 if errResp.Error != "InvalidRequest" {
200 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error)
201 }
202 })
203 }
204}
205
206func TestCreateVoteHandler_MissingFields(t *testing.T) {
207 mockService := &mockVoteService{}
208 handler := NewCreateVoteHandler(mockService)
209
210 tests := []struct {
211 name string
212 subjectURI string
213 subjectCID string
214 direction string
215 expectedError string
216 }{
217 {
218 name: "missing subject URI",
219 subjectURI: "",
220 subjectCID: "bafypost123",
221 direction: "up",
222 expectedError: "subject.uri is required",
223 },
224 {
225 name: "missing subject CID",
226 subjectURI: "at://did:plc:author123/social.coves.post/xyz789",
227 subjectCID: "",
228 direction: "up",
229 expectedError: "subject.cid is required",
230 },
231 {
232 name: "missing direction",
233 subjectURI: "at://did:plc:author123/social.coves.post/xyz789",
234 subjectCID: "bafypost123",
235 direction: "",
236 expectedError: "direction is required",
237 },
238 }
239
240 for _, tc := range tests {
241 t.Run(tc.name, func(t *testing.T) {
242 // Create request body
243 reqBody := CreateVoteInput{
244 Subject: struct {
245 URI string `json:"uri"`
246 CID string `json:"cid"`
247 }{
248 URI: tc.subjectURI,
249 CID: tc.subjectCID,
250 },
251 Direction: tc.direction,
252 }
253 bodyBytes, err := json.Marshal(reqBody)
254 if err != nil {
255 t.Fatalf("Failed to marshal request: %v", err)
256 }
257
258 // Create HTTP request
259 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
260 req.Header.Set("Content-Type", "application/json")
261
262 // Inject OAuth session
263 did, _ := syntax.ParseDID("did:plc:test123")
264 session := &oauthlib.ClientSessionData{
265 AccountDID: did,
266 AccessToken: "test_token",
267 }
268 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
269 req = req.WithContext(ctx)
270
271 // Execute handler
272 w := httptest.NewRecorder()
273 handler.HandleCreateVote(w, req)
274
275 // Check status code
276 if w.Code != http.StatusBadRequest {
277 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String())
278 }
279
280 // Check error response
281 var errResp XRPCError
282 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
283 t.Fatalf("Failed to decode error response: %v", err)
284 }
285 if errResp.Message != tc.expectedError {
286 t.Errorf("Expected message '%s', got '%s'", tc.expectedError, errResp.Message)
287 }
288 })
289 }
290}
291
292func TestCreateVoteHandler_InvalidJSON(t *testing.T) {
293 mockService := &mockVoteService{}
294 handler := NewCreateVoteHandler(mockService)
295
296 // Create invalid JSON
297 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBufferString("{invalid json"))
298 req.Header.Set("Content-Type", "application/json")
299
300 // Inject OAuth session
301 did, _ := syntax.ParseDID("did:plc:test123")
302 session := &oauthlib.ClientSessionData{
303 AccountDID: did,
304 AccessToken: "test_token",
305 }
306 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
307 req = req.WithContext(ctx)
308
309 // Execute handler
310 w := httptest.NewRecorder()
311 handler.HandleCreateVote(w, req)
312
313 // Check status code
314 if w.Code != http.StatusBadRequest {
315 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String())
316 }
317
318 // Check error response
319 var errResp XRPCError
320 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
321 t.Fatalf("Failed to decode error response: %v", err)
322 }
323 if errResp.Error != "InvalidRequest" {
324 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error)
325 }
326}
327
328func TestCreateVoteHandler_MethodNotAllowed(t *testing.T) {
329 mockService := &mockVoteService{}
330 handler := NewCreateVoteHandler(mockService)
331
332 // Create GET request (should only accept POST)
333 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.vote.create", nil)
334
335 // Execute handler
336 w := httptest.NewRecorder()
337 handler.HandleCreateVote(w, req)
338
339 // Check status code
340 if w.Code != http.StatusMethodNotAllowed {
341 t.Errorf("Expected status 405, got %d", w.Code)
342 }
343}
344
345func TestCreateVoteHandler_ServiceError(t *testing.T) {
346 tests := []struct {
347 serviceError error
348 name string
349 expectedError string
350 expectedStatus int
351 }{
352 {
353 name: "invalid direction",
354 serviceError: votes.ErrInvalidDirection,
355 expectedStatus: http.StatusBadRequest,
356 expectedError: "InvalidRequest",
357 },
358 {
359 name: "invalid subject",
360 serviceError: votes.ErrInvalidSubject,
361 expectedStatus: http.StatusBadRequest,
362 expectedError: "InvalidSubject", // Per lexicon: social.coves.feed.vote.create#InvalidSubject
363 },
364 {
365 name: "not authorized",
366 serviceError: votes.ErrNotAuthorized,
367 expectedStatus: http.StatusForbidden,
368 expectedError: "NotAuthorized", // Per lexicon: social.coves.feed.vote.create#NotAuthorized
369 },
370 {
371 name: "banned",
372 serviceError: votes.ErrBanned,
373 expectedStatus: http.StatusForbidden,
374 expectedError: "NotAuthorized", // Banned maps to NotAuthorized per lexicon
375 },
376 }
377
378 for _, tc := range tests {
379 t.Run(tc.name, func(t *testing.T) {
380 mockService := &mockVoteService{
381 createFunc: func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
382 return nil, tc.serviceError
383 },
384 }
385 handler := NewCreateVoteHandler(mockService)
386
387 // Create request body
388 reqBody := CreateVoteInput{
389 Subject: struct {
390 URI string `json:"uri"`
391 CID string `json:"cid"`
392 }{
393 URI: "at://did:plc:author123/social.coves.post/xyz789",
394 CID: "bafypost123",
395 },
396 Direction: "up",
397 }
398 bodyBytes, err := json.Marshal(reqBody)
399 if err != nil {
400 t.Fatalf("Failed to marshal request: %v", err)
401 }
402
403 // Create HTTP request
404 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
405 req.Header.Set("Content-Type", "application/json")
406
407 // Inject OAuth session
408 did, _ := syntax.ParseDID("did:plc:test123")
409 session := &oauthlib.ClientSessionData{
410 AccountDID: did,
411 AccessToken: "test_token",
412 }
413 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
414 req = req.WithContext(ctx)
415
416 // Execute handler
417 w := httptest.NewRecorder()
418 handler.HandleCreateVote(w, req)
419
420 // Check status code
421 if w.Code != tc.expectedStatus {
422 t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String())
423 }
424
425 // Check error response
426 var errResp XRPCError
427 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
428 t.Fatalf("Failed to decode error response: %v", err)
429 }
430 if errResp.Error != tc.expectedError {
431 t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error)
432 }
433 })
434 }
435}
436
437func TestCreateVoteHandler_ValidDirections(t *testing.T) {
438 mockService := &mockVoteService{}
439 handler := NewCreateVoteHandler(mockService)
440
441 directions := []string{"up", "down"}
442
443 for _, direction := range directions {
444 t.Run("direction_"+direction, func(t *testing.T) {
445 // Create request body
446 reqBody := CreateVoteInput{
447 Subject: struct {
448 URI string `json:"uri"`
449 CID string `json:"cid"`
450 }{
451 URI: "at://did:plc:author123/social.coves.post/xyz789",
452 CID: "bafypost123",
453 },
454 Direction: direction,
455 }
456 bodyBytes, err := json.Marshal(reqBody)
457 if err != nil {
458 t.Fatalf("Failed to marshal request: %v", err)
459 }
460
461 // Create HTTP request
462 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
463 req.Header.Set("Content-Type", "application/json")
464
465 // Inject OAuth session
466 did, _ := syntax.ParseDID("did:plc:test123")
467 session := &oauthlib.ClientSessionData{
468 AccountDID: did,
469 AccessToken: "test_token",
470 }
471 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
472 req = req.WithContext(ctx)
473
474 // Execute handler
475 w := httptest.NewRecorder()
476 handler.HandleCreateVote(w, req)
477
478 // Check status code
479 if w.Code != http.StatusOK {
480 t.Errorf("Expected status 200 for direction '%s', got %d. Body: %s", direction, w.Code, w.Body.String())
481 }
482 })
483 }
484}