···
4
+
"Coves/internal/api/middleware"
5
+
"Coves/internal/core/votes"
13
+
oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
14
+
"github.com/bluesky-social/indigo/atproto/syntax"
17
+
// mockVoteService implements votes.Service for testing
18
+
type 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
23
+
func (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)
27
+
return &votes.CreateVoteResponse{
28
+
URI: "at://did:plc:test123/social.coves.vote/abc123",
33
+
func (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)
40
+
func TestCreateVoteHandler_Success(t *testing.T) {
41
+
mockService := &mockVoteService{}
42
+
handler := NewCreateVoteHandler(mockService)
44
+
// Create request body
45
+
reqBody := CreateVoteInput{
47
+
URI string `json:"uri"`
48
+
CID string `json:"cid"`
50
+
URI: "at://did:plc:author123/social.coves.post/xyz789",
55
+
bodyBytes, err := json.Marshal(reqBody)
57
+
t.Fatalf("Failed to marshal request: %v", err)
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")
64
+
// Inject OAuth session into context (simulates auth middleware)
65
+
did, _ := syntax.ParseDID("did:plc:test123")
66
+
session := &oauthlib.ClientSessionData{
68
+
AccessToken: "test_token",
70
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
71
+
req = req.WithContext(ctx)
74
+
w := httptest.NewRecorder()
75
+
handler.HandleCreateVote(w, req)
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())
83
+
var response CreateVoteOutput
84
+
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
85
+
t.Fatalf("Failed to decode response: %v", err)
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)
91
+
if response.CID != "bafyvote123" {
92
+
t.Errorf("Expected CID bafyvote123, got %s", response.CID)
96
+
func TestCreateVoteHandler_RequiresAuth(t *testing.T) {
97
+
mockService := &mockVoteService{}
98
+
handler := NewCreateVoteHandler(mockService)
100
+
// Create request body
101
+
reqBody := CreateVoteInput{
103
+
URI string `json:"uri"`
104
+
CID string `json:"cid"`
106
+
URI: "at://did:plc:author123/social.coves.post/xyz789",
107
+
CID: "bafypost123",
111
+
bodyBytes, err := json.Marshal(reqBody)
113
+
t.Fatalf("Failed to marshal request: %v", err)
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
122
+
w := httptest.NewRecorder()
123
+
handler.HandleCreateVote(w, req)
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())
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)
135
+
if errResp.Error != "AuthRequired" {
136
+
t.Errorf("Expected error AuthRequired, got %s", errResp.Error)
140
+
func TestCreateVoteHandler_InvalidDirection(t *testing.T) {
141
+
mockService := &mockVoteService{}
142
+
handler := NewCreateVoteHandler(mockService)
144
+
tests := []struct {
148
+
{"empty direction", ""},
149
+
{"invalid direction", "sideways"},
150
+
{"wrong case", "UP"},
154
+
for _, tc := range tests {
155
+
t.Run(tc.name, func(t *testing.T) {
156
+
// Create request body
157
+
reqBody := CreateVoteInput{
159
+
URI string `json:"uri"`
160
+
CID string `json:"cid"`
162
+
URI: "at://did:plc:author123/social.coves.post/xyz789",
163
+
CID: "bafypost123",
165
+
Direction: tc.direction,
167
+
bodyBytes, err := json.Marshal(reqBody)
169
+
t.Fatalf("Failed to marshal request: %v", err)
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")
176
+
// Inject OAuth session
177
+
did, _ := syntax.ParseDID("did:plc:test123")
178
+
session := &oauthlib.ClientSessionData{
180
+
AccessToken: "test_token",
182
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
183
+
req = req.WithContext(ctx)
186
+
w := httptest.NewRecorder()
187
+
handler.HandleCreateVote(w, req)
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())
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)
199
+
if errResp.Error != "InvalidRequest" {
200
+
t.Errorf("Expected error InvalidRequest, got %s", errResp.Error)
206
+
func TestCreateVoteHandler_MissingFields(t *testing.T) {
207
+
mockService := &mockVoteService{}
208
+
handler := NewCreateVoteHandler(mockService)
210
+
tests := []struct {
215
+
expectedError string
218
+
name: "missing subject URI",
220
+
subjectCID: "bafypost123",
222
+
expectedError: "subject.uri is required",
225
+
name: "missing subject CID",
226
+
subjectURI: "at://did:plc:author123/social.coves.post/xyz789",
229
+
expectedError: "subject.cid is required",
232
+
name: "missing direction",
233
+
subjectURI: "at://did:plc:author123/social.coves.post/xyz789",
234
+
subjectCID: "bafypost123",
236
+
expectedError: "direction is required",
240
+
for _, tc := range tests {
241
+
t.Run(tc.name, func(t *testing.T) {
242
+
// Create request body
243
+
reqBody := CreateVoteInput{
245
+
URI string `json:"uri"`
246
+
CID string `json:"cid"`
248
+
URI: tc.subjectURI,
249
+
CID: tc.subjectCID,
251
+
Direction: tc.direction,
253
+
bodyBytes, err := json.Marshal(reqBody)
255
+
t.Fatalf("Failed to marshal request: %v", err)
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")
262
+
// Inject OAuth session
263
+
did, _ := syntax.ParseDID("did:plc:test123")
264
+
session := &oauthlib.ClientSessionData{
266
+
AccessToken: "test_token",
268
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
269
+
req = req.WithContext(ctx)
272
+
w := httptest.NewRecorder()
273
+
handler.HandleCreateVote(w, req)
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())
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)
285
+
if errResp.Message != tc.expectedError {
286
+
t.Errorf("Expected message '%s', got '%s'", tc.expectedError, errResp.Message)
292
+
func TestCreateVoteHandler_InvalidJSON(t *testing.T) {
293
+
mockService := &mockVoteService{}
294
+
handler := NewCreateVoteHandler(mockService)
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")
300
+
// Inject OAuth session
301
+
did, _ := syntax.ParseDID("did:plc:test123")
302
+
session := &oauthlib.ClientSessionData{
304
+
AccessToken: "test_token",
306
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
307
+
req = req.WithContext(ctx)
310
+
w := httptest.NewRecorder()
311
+
handler.HandleCreateVote(w, req)
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())
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)
323
+
if errResp.Error != "InvalidRequest" {
324
+
t.Errorf("Expected error InvalidRequest, got %s", errResp.Error)
328
+
func TestCreateVoteHandler_MethodNotAllowed(t *testing.T) {
329
+
mockService := &mockVoteService{}
330
+
handler := NewCreateVoteHandler(mockService)
332
+
// Create GET request (should only accept POST)
333
+
req := httptest.NewRequest(http.MethodGet, "/xrpc/social.coves.vote.create", nil)
336
+
w := httptest.NewRecorder()
337
+
handler.HandleCreateVote(w, req)
339
+
// Check status code
340
+
if w.Code != http.StatusMethodNotAllowed {
341
+
t.Errorf("Expected status 405, got %d", w.Code)
345
+
func TestCreateVoteHandler_ServiceError(t *testing.T) {
346
+
tests := []struct {
350
+
expectedError string
353
+
name: "subject not found",
354
+
serviceError: votes.ErrSubjectNotFound,
355
+
expectedStatus: http.StatusNotFound,
356
+
expectedError: "SubjectNotFound", // Per lexicon: social.coves.feed.vote.create#SubjectNotFound
359
+
name: "invalid direction",
360
+
serviceError: votes.ErrInvalidDirection,
361
+
expectedStatus: http.StatusBadRequest,
362
+
expectedError: "InvalidRequest",
365
+
name: "invalid subject",
366
+
serviceError: votes.ErrInvalidSubject,
367
+
expectedStatus: http.StatusBadRequest,
368
+
expectedError: "InvalidSubject", // Per lexicon: social.coves.feed.vote.create#InvalidSubject
371
+
name: "not authorized",
372
+
serviceError: votes.ErrNotAuthorized,
373
+
expectedStatus: http.StatusForbidden,
374
+
expectedError: "NotAuthorized", // Per lexicon: social.coves.feed.vote.create#NotAuthorized
378
+
serviceError: votes.ErrBanned,
379
+
expectedStatus: http.StatusForbidden,
380
+
expectedError: "NotAuthorized", // Banned maps to NotAuthorized per lexicon
384
+
for _, tc := range tests {
385
+
t.Run(tc.name, func(t *testing.T) {
386
+
mockService := &mockVoteService{
387
+
createFunc: func(ctx context.Context, session *oauthlib.ClientSessionData, req votes.CreateVoteRequest) (*votes.CreateVoteResponse, error) {
388
+
return nil, tc.serviceError
391
+
handler := NewCreateVoteHandler(mockService)
393
+
// Create request body
394
+
reqBody := CreateVoteInput{
396
+
URI string `json:"uri"`
397
+
CID string `json:"cid"`
399
+
URI: "at://did:plc:author123/social.coves.post/xyz789",
400
+
CID: "bafypost123",
404
+
bodyBytes, err := json.Marshal(reqBody)
406
+
t.Fatalf("Failed to marshal request: %v", err)
409
+
// Create HTTP request
410
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
411
+
req.Header.Set("Content-Type", "application/json")
413
+
// Inject OAuth session
414
+
did, _ := syntax.ParseDID("did:plc:test123")
415
+
session := &oauthlib.ClientSessionData{
417
+
AccessToken: "test_token",
419
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
420
+
req = req.WithContext(ctx)
423
+
w := httptest.NewRecorder()
424
+
handler.HandleCreateVote(w, req)
426
+
// Check status code
427
+
if w.Code != tc.expectedStatus {
428
+
t.Errorf("Expected status %d, got %d. Body: %s", tc.expectedStatus, w.Code, w.Body.String())
431
+
// Check error response
432
+
var errResp XRPCError
433
+
if err := json.NewDecoder(w.Body).Decode(&errResp); err != nil {
434
+
t.Fatalf("Failed to decode error response: %v", err)
436
+
if errResp.Error != tc.expectedError {
437
+
t.Errorf("Expected error %s, got %s", tc.expectedError, errResp.Error)
443
+
func TestCreateVoteHandler_ValidDirections(t *testing.T) {
444
+
mockService := &mockVoteService{}
445
+
handler := NewCreateVoteHandler(mockService)
447
+
directions := []string{"up", "down"}
449
+
for _, direction := range directions {
450
+
t.Run("direction_"+direction, func(t *testing.T) {
451
+
// Create request body
452
+
reqBody := CreateVoteInput{
454
+
URI string `json:"uri"`
455
+
CID string `json:"cid"`
457
+
URI: "at://did:plc:author123/social.coves.post/xyz789",
458
+
CID: "bafypost123",
460
+
Direction: direction,
462
+
bodyBytes, err := json.Marshal(reqBody)
464
+
t.Fatalf("Failed to marshal request: %v", err)
467
+
// Create HTTP request
468
+
req := httptest.NewRequest(http.MethodPost, "/xrpc/social.coves.vote.create", bytes.NewBuffer(bodyBytes))
469
+
req.Header.Set("Content-Type", "application/json")
471
+
// Inject OAuth session
472
+
did, _ := syntax.ParseDID("did:plc:test123")
473
+
session := &oauthlib.ClientSessionData{
475
+
AccessToken: "test_token",
477
+
ctx := context.WithValue(req.Context(), middleware.OAuthSessionKey, session)
478
+
req = req.WithContext(ctx)
481
+
w := httptest.NewRecorder()
482
+
handler.HandleCreateVote(w, req)
484
+
// Check status code
485
+
if w.Code != http.StatusOK {
486
+
t.Errorf("Expected status 200 for direction '%s', got %d. Body: %s", direction, w.Code, w.Body.String())