A community based topic aggregation platform built on atproto
1package votes 2 3import ( 4 "Coves/internal/core/posts" 5 "context" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/mock" 11 "github.com/stretchr/testify/require" 12) 13 14// Mock repositories for testing 15type mockVoteRepository struct { 16 mock.Mock 17} 18 19func (m *mockVoteRepository) Create(ctx context.Context, vote *Vote) error { 20 args := m.Called(ctx, vote) 21 return args.Error(0) 22} 23 24func (m *mockVoteRepository) GetByURI(ctx context.Context, uri string) (*Vote, error) { 25 args := m.Called(ctx, uri) 26 if args.Get(0) == nil { 27 return nil, args.Error(1) 28 } 29 return args.Get(0).(*Vote), args.Error(1) 30} 31 32func (m *mockVoteRepository) GetByVoterAndSubject(ctx context.Context, voterDID string, subjectURI string) (*Vote, error) { 33 args := m.Called(ctx, voterDID, subjectURI) 34 if args.Get(0) == nil { 35 return nil, args.Error(1) 36 } 37 return args.Get(0).(*Vote), args.Error(1) 38} 39 40func (m *mockVoteRepository) Delete(ctx context.Context, uri string) error { 41 args := m.Called(ctx, uri) 42 return args.Error(0) 43} 44 45func (m *mockVoteRepository) ListBySubject(ctx context.Context, subjectURI string, limit, offset int) ([]*Vote, error) { 46 args := m.Called(ctx, subjectURI, limit, offset) 47 if args.Get(0) == nil { 48 return nil, args.Error(1) 49 } 50 return args.Get(0).([]*Vote), args.Error(1) 51} 52 53func (m *mockVoteRepository) ListByVoter(ctx context.Context, voterDID string, limit, offset int) ([]*Vote, error) { 54 args := m.Called(ctx, voterDID, limit, offset) 55 if args.Get(0) == nil { 56 return nil, args.Error(1) 57 } 58 return args.Get(0).([]*Vote), args.Error(1) 59} 60 61type mockPostRepository struct { 62 mock.Mock 63} 64 65func (m *mockPostRepository) GetByURI(ctx context.Context, uri string) (*posts.Post, error) { 66 args := m.Called(ctx, uri) 67 if args.Get(0) == nil { 68 return nil, args.Error(1) 69 } 70 return args.Get(0).(*posts.Post), args.Error(1) 71} 72 73func (m *mockPostRepository) Create(ctx context.Context, post *posts.Post) error { 74 args := m.Called(ctx, post) 75 return args.Error(0) 76} 77 78func (m *mockPostRepository) GetByRkey(ctx context.Context, communityDID, rkey string) (*posts.Post, error) { 79 args := m.Called(ctx, communityDID, rkey) 80 if args.Get(0) == nil { 81 return nil, args.Error(1) 82 } 83 return args.Get(0).(*posts.Post), args.Error(1) 84} 85 86func (m *mockPostRepository) ListByCommunity(ctx context.Context, communityDID string, limit, offset int) ([]*posts.Post, error) { 87 args := m.Called(ctx, communityDID, limit, offset) 88 if args.Get(0) == nil { 89 return nil, args.Error(1) 90 } 91 return args.Get(0).([]*posts.Post), args.Error(1) 92} 93 94func (m *mockPostRepository) Delete(ctx context.Context, uri string) error { 95 args := m.Called(ctx, uri) 96 return args.Error(0) 97} 98 99// TestVoteService_CreateVote_NoExistingVote tests creating a vote when no vote exists 100// NOTE: This test is skipped because we need to refactor service to inject HTTP client 101// for testing PDS writes. The full flow is covered by E2E tests. 102func TestVoteService_CreateVote_NoExistingVote(t *testing.T) { 103 t.Skip("Skipping because we need to refactor service to inject HTTP client for testing PDS writes - covered by E2E tests") 104 105 // This test would verify: 106 // - Post exists check 107 // - No existing vote 108 // - PDS write succeeds 109 // - Response contains vote URI and CID 110} 111 112// TestVoteService_ValidateInput tests input validation 113func TestVoteService_ValidateInput(t *testing.T) { 114 mockVoteRepo := new(mockVoteRepository) 115 mockPostRepo := new(mockPostRepository) 116 117 service := &voteService{ 118 repo: mockVoteRepo, 119 postRepo: mockPostRepo, 120 pdsURL: "http://mock-pds.test", 121 } 122 123 ctx := context.Background() 124 125 tests := []struct { 126 name string 127 voterDID string 128 accessToken string 129 req CreateVoteRequest 130 expectedError string 131 }{ 132 { 133 name: "missing voter DID", 134 voterDID: "", 135 accessToken: "token123", 136 req: CreateVoteRequest{Subject: "at://test", Direction: "up"}, 137 expectedError: "voterDid", 138 }, 139 { 140 name: "missing access token", 141 voterDID: "did:plc:test", 142 accessToken: "", 143 req: CreateVoteRequest{Subject: "at://test", Direction: "up"}, 144 expectedError: "userAccessToken", 145 }, 146 { 147 name: "missing subject", 148 voterDID: "did:plc:test", 149 accessToken: "token123", 150 req: CreateVoteRequest{Subject: "", Direction: "up"}, 151 expectedError: "subject", 152 }, 153 { 154 name: "invalid direction", 155 voterDID: "did:plc:test", 156 accessToken: "token123", 157 req: CreateVoteRequest{Subject: "at://test", Direction: "invalid"}, 158 expectedError: "invalid vote direction", 159 }, 160 { 161 name: "invalid subject format", 162 voterDID: "did:plc:test", 163 accessToken: "token123", 164 req: CreateVoteRequest{Subject: "http://not-at-uri", Direction: "up"}, 165 expectedError: "invalid subject URI", 166 }, 167 } 168 169 for _, tt := range tests { 170 t.Run(tt.name, func(t *testing.T) { 171 _, err := service.CreateVote(ctx, tt.voterDID, tt.accessToken, tt.req) 172 require.Error(t, err) 173 assert.Contains(t, err.Error(), tt.expectedError) 174 }) 175 } 176} 177 178// TestVoteService_GetVote tests retrieving a vote 179func TestVoteService_GetVote(t *testing.T) { 180 mockVoteRepo := new(mockVoteRepository) 181 mockPostRepo := new(mockPostRepository) 182 183 service := &voteService{ 184 repo: mockVoteRepo, 185 postRepo: mockPostRepo, 186 pdsURL: "http://mock-pds.test", 187 } 188 189 ctx := context.Background() 190 voterDID := "did:plc:voter123" 191 subjectURI := "at://did:plc:community/social.coves.post.record/abc123" 192 193 expectedVote := &Vote{ 194 ID: 1, 195 URI: "at://did:plc:voter123/social.coves.interaction.vote/xyz789", 196 VoterDID: voterDID, 197 SubjectURI: subjectURI, 198 Direction: "up", 199 CreatedAt: time.Now(), 200 } 201 202 mockVoteRepo.On("GetByVoterAndSubject", ctx, voterDID, subjectURI).Return(expectedVote, nil) 203 204 result, err := service.GetVote(ctx, voterDID, subjectURI) 205 assert.NoError(t, err) 206 assert.Equal(t, expectedVote.URI, result.URI) 207 assert.Equal(t, expectedVote.Direction, result.Direction) 208 209 mockVoteRepo.AssertExpectations(t) 210} 211 212// TestVoteService_GetVote_NotFound tests getting a non-existent vote 213func TestVoteService_GetVote_NotFound(t *testing.T) { 214 mockVoteRepo := new(mockVoteRepository) 215 mockPostRepo := new(mockPostRepository) 216 217 service := &voteService{ 218 repo: mockVoteRepo, 219 postRepo: mockPostRepo, 220 pdsURL: "http://mock-pds.test", 221 } 222 223 ctx := context.Background() 224 voterDID := "did:plc:voter123" 225 subjectURI := "at://did:plc:community/social.coves.post.record/noexist" 226 227 mockVoteRepo.On("GetByVoterAndSubject", ctx, voterDID, subjectURI).Return(nil, ErrVoteNotFound) 228 229 result, err := service.GetVote(ctx, voterDID, subjectURI) 230 assert.ErrorIs(t, err, ErrVoteNotFound) 231 assert.Nil(t, result) 232 233 mockVoteRepo.AssertExpectations(t) 234} 235 236// TestVoteService_SubjectNotFound tests voting on non-existent post 237func TestVoteService_SubjectNotFound(t *testing.T) { 238 mockVoteRepo := new(mockVoteRepository) 239 mockPostRepo := new(mockPostRepository) 240 241 service := &voteService{ 242 repo: mockVoteRepo, 243 postRepo: mockPostRepo, 244 pdsURL: "http://mock-pds.test", 245 } 246 247 ctx := context.Background() 248 voterDID := "did:plc:voter123" 249 subjectURI := "at://did:plc:community/social.coves.post.record/noexist" 250 251 // Mock post not found 252 mockPostRepo.On("GetByURI", ctx, subjectURI).Return(nil, posts.ErrNotFound) 253 254 req := CreateVoteRequest{ 255 Subject: subjectURI, 256 Direction: "up", 257 } 258 259 _, err := service.CreateVote(ctx, voterDID, "token123", req) 260 assert.ErrorIs(t, err, ErrSubjectNotFound) 261 262 mockPostRepo.AssertExpectations(t) 263} 264 265// NOTE: Testing toggle logic (same direction, different direction) requires mocking HTTP client 266// These tests are covered by integration tests in tests/integration/vote_e2e_test.go 267// To add unit tests for toggle logic, we would need to: 268// 1. Refactor voteService to accept an HTTP client interface 269// 2. Mock the PDS createRecord and deleteRecord calls 270// 3. Verify the correct sequence of operations 271 272// Example of what toggle tests would look like (requires refactoring): 273/* 274func TestVoteService_ToggleSameDirection(t *testing.T) { 275 // Setup 276 mockVoteRepo := new(mockVoteRepository) 277 mockPostRepo := new(mockPostRepository) 278 mockPDSClient := new(mockPDSClient) 279 280 service := &voteService{ 281 repo: mockVoteRepo, 282 postRepo: mockPostRepo, 283 pdsClient: mockPDSClient, // Would need to refactor to inject this 284 } 285 286 ctx := context.Background() 287 voterDID := "did:plc:voter123" 288 subjectURI := "at://did:plc:community/social.coves.post.record/abc123" 289 290 // Mock existing upvote 291 existingVote := &Vote{ 292 URI: "at://did:plc:voter123/social.coves.interaction.vote/existing", 293 VoterDID: voterDID, 294 SubjectURI: subjectURI, 295 Direction: "up", 296 } 297 mockVoteRepo.On("GetByVoterAndSubject", ctx, voterDID, subjectURI).Return(existingVote, nil) 298 299 // Mock post exists 300 mockPostRepo.On("GetByURI", ctx, subjectURI).Return(&posts.Post{ 301 URI: subjectURI, 302 CID: "bafyreigpost123", 303 }, nil) 304 305 // Mock PDS delete 306 mockPDSClient.On("DeleteRecord", voterDID, "social.coves.interaction.vote", "existing").Return(nil) 307 308 // Execute: Click upvote when already upvoted -> should delete 309 req := CreateVoteRequest{ 310 Subject: subjectURI, 311 Direction: "up", // Same direction 312 } 313 314 response, err := service.CreateVote(ctx, voterDID, "token123", req) 315 316 // Assert 317 assert.NoError(t, err) 318 assert.Equal(t, "", response.URI, "Should return empty URI when toggled off") 319 mockPDSClient.AssertCalled(t, "DeleteRecord", voterDID, "social.coves.interaction.vote", "existing") 320 mockVoteRepo.AssertExpectations(t) 321 mockPostRepo.AssertExpectations(t) 322} 323 324func TestVoteService_ToggleDifferentDirection(t *testing.T) { 325 // Similar test but existing vote is "up" and new vote is "down" 326 // Should delete old vote and create new vote 327 // Would verify: 328 // 1. DeleteRecord called for old vote 329 // 2. CreateRecord called for new vote 330 // 3. Response contains new vote URI 331} 332*/ 333 334// Documentation test to explain toggle logic (verified by E2E tests) 335func TestVoteService_ToggleLogicDocumentation(t *testing.T) { 336 t.Log("Toggle Logic (verified by E2E tests in tests/integration/vote_e2e_test.go):") 337 t.Log("1. No existing vote + upvote clicked → Create upvote") 338 t.Log("2. Upvote exists + upvote clicked → Delete upvote (toggle off)") 339 t.Log("3. Upvote exists + downvote clicked → Delete upvote + Create downvote (switch)") 340 t.Log("4. Downvote exists + downvote clicked → Delete downvote (toggle off)") 341 t.Log("5. Downvote exists + upvote clicked → Delete downvote + Create upvote (switch)") 342 t.Log("") 343 t.Log("To add unit tests for toggle logic, refactor service to accept HTTP client interface") 344}