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
17func TestDeleteVoteHandler_Success(t *testing.T) {
18 mockService := &mockVoteService{}
19 handler := NewDeleteVoteHandler(mockService)
20
21 // Create request body
22 reqBody := DeleteVoteInput{
23 Subject: struct {
24 URI string `json:"uri"`
25 CID string `json:"cid"`
26 }{
27 URI: "at://did:plc:author123/social.coves.post/xyz789",
28 CID: "bafypost123",
29 },
30 }
31 bodyBytes, err := json.Marshal(reqBody)
32 if err != nil {
33 t.Fatalf("Failed to marshal request: %v", err)
34 }
35
36 // Create HTTP request
37 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes))
38 req.Header.Set("Content-Type", "application/json")
39
40 // Inject OAuth session into context (simulates auth middleware)
41 did, _ := syntax.ParseDID("did:plc:test123")
42 session := &oauthlib.ClientSessionData{
43 AccountDID: did,
44 AccessToken: "test_token",
45 }
46 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
47 req = req.WithContext(ctx)
48
49 // Execute handler
50 w := httptest.NewRecorder()
51 handler.HandleDeleteVote(w, req)
52
53 // Check status code
54 if w.Code != http.StatusOK {
55 t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
56 }
57
58 // Check response is empty object per lexicon
59 var response map[string]interface{}
60 if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
61 t.Fatalf("Failed to decode response: %v", err)
62 }
63
64 if len(response) != 0 {
65 t.Errorf("Expected empty object per lexicon, got %v", response)
66 }
67}
68
69func TestDeleteVoteHandler_RequiresAuth(t *testing.T) {
70 mockService := &mockVoteService{}
71 handler := NewDeleteVoteHandler(mockService)
72
73 // Create request body
74 reqBody := DeleteVoteInput{
75 Subject: struct {
76 URI string `json:"uri"`
77 CID string `json:"cid"`
78 }{
79 URI: "at://did:plc:author123/social.coves.post/xyz789",
80 CID: "bafypost123",
81 },
82 }
83 bodyBytes, err := json.Marshal(reqBody)
84 if err != nil {
85 t.Fatalf("Failed to marshal request: %v", err)
86 }
87
88 // Create HTTP request without auth context
89 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes))
90 req.Header.Set("Content-Type", "application/json")
91 // No OAuth session in context
92
93 // Execute handler
94 w := httptest.NewRecorder()
95 handler.HandleDeleteVote(w, req)
96
97 // Check status code
98 if w.Code != http.StatusUnauthorized {
99 t.Errorf("Expected status 401, got %d. Body: %s", w.Code, w.Body.String())
100 }
101
102 // Check error response
103 var errResp XRPCError
104 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
105 t.Fatalf("Failed to decode error response: %v", err)
106 }
107 if errResp.Error != "AuthRequired" {
108 t.Errorf("Expected error AuthRequired, got %s", errResp.Error)
109 }
110}
111
112func TestDeleteVoteHandler_MissingFields(t *testing.T) {
113 mockService := &mockVoteService{}
114 handler := NewDeleteVoteHandler(mockService)
115
116 tests := []struct {
117 name string
118 subjectURI string
119 subjectCID string
120 expectedError string
121 }{
122 {
123 name: "missing subject URI",
124 subjectURI: "",
125 subjectCID: "bafypost123",
126 expectedError: "subject.uri is required",
127 },
128 {
129 name: "missing subject CID",
130 subjectURI: "at://did:plc:author123/social.coves.post/xyz789",
131 subjectCID: "",
132 expectedError: "subject.cid is required",
133 },
134 }
135
136 for _, tc := range tests {
137 t.Run(tc.name, func(t *testing.T) {
138 // Create request body
139 reqBody := DeleteVoteInput{
140 Subject: struct {
141 URI string `json:"uri"`
142 CID string `json:"cid"`
143 }{
144 URI: tc.subjectURI,
145 CID: tc.subjectCID,
146 },
147 }
148 bodyBytes, err := json.Marshal(reqBody)
149 if err != nil {
150 t.Fatalf("Failed to marshal request: %v", err)
151 }
152
153 // Create HTTP request
154 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes))
155 req.Header.Set("Content-Type", "application/json")
156
157 // Inject OAuth session
158 did, _ := syntax.ParseDID("did:plc:test123")
159 session := &oauthlib.ClientSessionData{
160 AccountDID: did,
161 AccessToken: "test_token",
162 }
163 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
164 req = req.WithContext(ctx)
165
166 // Execute handler
167 w := httptest.NewRecorder()
168 handler.HandleDeleteVote(w, req)
169
170 // Check status code
171 if w.Code != http.StatusBadRequest {
172 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String())
173 }
174
175 // Check error response
176 var errResp XRPCError
177 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
178 t.Fatalf("Failed to decode error response: %v", err)
179 }
180 if errResp.Message != tc.expectedError {
181 t.Errorf("Expected message '%s', got '%s'", tc.expectedError, errResp.Message)
182 }
183 })
184 }
185}
186
187func TestDeleteVoteHandler_InvalidJSON(t *testing.T) {
188 mockService := &mockVoteService{}
189 handler := NewDeleteVoteHandler(mockService)
190
191 // Create invalid JSON
192 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBufferString("{invalid json"))
193 req.Header.Set("Content-Type", "application/json")
194
195 // Inject OAuth session
196 did, _ := syntax.ParseDID("did:plc:test123")
197 session := &oauthlib.ClientSessionData{
198 AccountDID: did,
199 AccessToken: "test_token",
200 }
201 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
202 req = req.WithContext(ctx)
203
204 // Execute handler
205 w := httptest.NewRecorder()
206 handler.HandleDeleteVote(w, req)
207
208 // Check status code
209 if w.Code != http.StatusBadRequest {
210 t.Errorf("Expected status 400, got %d. Body: %s", w.Code, w.Body.String())
211 }
212
213 // Check error response
214 var errResp XRPCError
215 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
216 t.Fatalf("Failed to decode error response: %v", err)
217 }
218 if errResp.Error != "InvalidRequest" {
219 t.Errorf("Expected error InvalidRequest, got %s", errResp.Error)
220 }
221}
222
223func TestDeleteVoteHandler_MethodNotAllowed(t *testing.T) {
224 mockService := &mockVoteService{}
225 handler := NewDeleteVoteHandler(mockService)
226
227 // Create GET request (should only accept POST)
228 req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.vote.delete", nil)
229
230 // Execute handler
231 w := httptest.NewRecorder()
232 handler.HandleDeleteVote(w, req)
233
234 // Check status code
235 if w.Code != http.StatusMethodNotAllowed {
236 t.Errorf("Expected status 405, got %d", w.Code)
237 }
238}
239
240func TestDeleteVoteHandler_ServiceError(t *testing.T) {
241 tests := []struct {
242 serviceError error
243 name string
244 expectedError string
245 expectedStatus int
246 }{
247 {
248 name: "vote not found",
249 serviceError: votes.ErrVoteNotFound,
250 expectedStatus: http.StatusNotFound,
251 expectedError: "VoteNotFound", // Per lexicon: social.coves.feed.vote.delete#VoteNotFound
252 },
253 {
254 name: "invalid subject",
255 serviceError: votes.ErrInvalidSubject,
256 expectedStatus: http.StatusBadRequest,
257 expectedError: "InvalidSubject", // Per lexicon: social.coves.feed.vote.create#InvalidSubject
258 },
259 {
260 name: "not authorized",
261 serviceError: votes.ErrNotAuthorized,
262 expectedStatus: http.StatusForbidden,
263 expectedError: "NotAuthorized", // Per lexicon: social.coves.feed.vote.delete#NotAuthorized
264 },
265 {
266 name: "banned",
267 serviceError: votes.ErrBanned,
268 expectedStatus: http.StatusForbidden,
269 expectedError: "NotAuthorized", // Banned maps to NotAuthorized per lexicon
270 },
271 }
272
273 for _, tc := range tests {
274 t.Run(tc.name, func(t *testing.T) {
275 mockService := &mockVoteService{
276 deleteFunc: func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.DeleteVoteRequest) error {
277 return tc.serviceError
278 },
279 }
280 handler := NewDeleteVoteHandler(mockService)
281
282 // Create request body
283 reqBody := DeleteVoteInput{
284 Subject: struct {
285 URI string `json:"uri"`
286 CID string `json:"cid"`
287 }{
288 URI: "at://did:plc:author123/social.coves.post/xyz789",
289 CID: "bafypost123",
290 },
291 }
292 bodyBytes, err := json.Marshal(reqBody)
293 if err != nil {
294 t.Fatalf("Failed to marshal request: %v", err)
295 }
296
297 // Create HTTP request
298 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes))
299 req.Header.Set("Content-Type", "application/json")
300
301 // Inject OAuth session
302 did, _ := syntax.ParseDID("did:plc:test123")
303 session := &oauthlib.ClientSessionData{
304 AccountDID: did,
305 AccessToken: "test_token",
306 }
307 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
308 req = req.WithContext(ctx)
309
310 // Execute handler
311 w := httptest.NewRecorder()
312 handler.HandleDeleteVote(w, req)
313
314 // Check status code
315 if w.Code != tc.expectedStatus {
316 t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String())
317 }
318
319 // Check error response
320 var errResp XRPCError
321 if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
322 t.Fatalf("Failed to decode error response: %v", err)
323 }
324 if errResp.Error != tc.expectedError {
325 t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error)
326 }
327 })
328 }
329}
330
331func TestDeleteVoteHandler_MultipleSubjects(t *testing.T) {
332 mockService := &mockVoteService{}
333 handler := NewDeleteVoteHandler(mockService)
334
335 subjects := []struct {
336 uri string
337 cid string
338 }{
339 {"at://did:plc:author1/social.coves.post/post1", "bafypost1"},
340 {"at://did:plc:author2/social.coves.post/post2", "bafypost2"},
341 {"at://did:plc:author3/social.coves.comment/comment1", "bafycomment1"},
342 }
343
344 for _, subject := range subjects {
345 t.Run("subject_"+subject.uri, func(t *testing.T) {
346 // Create request body
347 reqBody := DeleteVoteInput{
348 Subject: struct {
349 URI string `json:"uri"`
350 CID string `json:"cid"`
351 }{
352 URI: subject.uri,
353 CID: subject.cid,
354 },
355 }
356 bodyBytes, err := json.Marshal(reqBody)
357 if err != nil {
358 t.Fatalf("Failed to marshal request: %v", err)
359 }
360
361 // Create HTTP request
362 req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.delete", bytes.NewBuffer(bodyBytes))
363 req.Header.Set("Content-Type", "application/json")
364
365 // Inject OAuth session
366 did, _ := syntax.ParseDID("did:plc:test123")
367 session := &oauthlib.ClientSessionData{
368 AccountDID: did,
369 AccessToken: "test_token",
370 }
371 ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
372 req = req.WithContext(ctx)
373
374 // Execute handler
375 w := httptest.NewRecorder()
376 handler.HandleDeleteVote(w, req)
377
378 // Check status code
379 if w.Code != http.StatusOK {
380 t.Errorf("Expected status 200 for subject '%s', got %d. Body: %s", subject.uri, w.Code, w.Body.String())
381 }
382
383 // Check response is empty object per lexicon
384 var response map[string]interface{}
385 if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
386 t.Fatalf("Failed to decode response: %v", err)
387 }
388
389 if len(response) != 0 {
390 t.Errorf("Expected empty object per lexicon, got %v", response)
391 }
392 })
393 }
394}