← Tasks
[CP-TEST-01] Voters API unit tests (CRUD, CURP validation, campaign isolation) (retry 1)
completedcode_genP1
Description
## CP-TEST-01: Voters API unit tests (CRUD, CURP validation, campaign isolation)
Section: Testing | Milestone: M3 | Owner: G3 Flash
### Failing Tests
- **voters_test_file** (file_exists): File `campaignpro/src/api/voters.test.ts` must exist
### Acceptance Criteria
- File `campaignpro/src/api/voters.test.ts` must exist
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- CP-API-01: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
81%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 241,
"estimated_usd": 0.087093,
"output_tokens": 5758
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 60099,
"response_text": "I'll create the voters API unit tests file with comprehensive CRUD, CURP validation, and campaign isolation tests.\n\n```typescript\n// EN: Unit tests for voters API endpoints\n// VI: Kiểm tra đơn vị cho các endpoints API cử tri \n// ES: Pruebas unitarias para endpoints API de votantes\n// SPEC: CP-TEST-01\n\nimport { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';\nimport { NextRequest } from 'next/server';\nimport { GET, POST, PUT, DELETE } from './voters';\n\n// EN: Mock Supabase client\n// VI: Mock client Supabase\n// ES: Cliente Supabase simulado\nconst mockSupabase = {\n from: vi.fn().mockReturnThis(),\n select: vi.fn().mockReturnThis(),\n insert: vi.fn().mockReturnThis(),\n update: vi.fn().mockReturnThis(),\n delete: vi.fn().mockReturnThis(),\n eq: vi.fn().mockReturnThis(),\n single: vi.fn(),\n};\n\nvi.mock('@supabase/supabase-js', () => ({\n createClient: vi.fn(() => mockSupabase),\n}));\n\n// EN: Mock auth context\n// VI: Mock ngữ cảnh xác thực\n// ES: Contexto de autenticación simulado\nvi.mock('../../../lib/auth', () => ({\n getServerSession: vi.fn(),\n}));\n\ndescribe('Voters API', () => {\n const mockCampaignId = 'campaign-123';\n const mockUserId = 'user-456';\n \n const mockVoter = {\n id: 'voter-789',\n campaign_id: mockCampaignId,\n curp: 'ABCD123456HDFGHI01',\n first_name: 'Juan',\n last_name: 'Pérez',\n email: 'juan.perez@email.com',\n phone: '+525512345678',\n address: 'Calle Principal 123',\n district: 'Distrito 1',\n section: 'Sección A',\n status: 'active',\n created_at: '2024-01-01T00:00:00Z',\n updated_at: '2024-01-01T00:00:00Z',\n };\n\n beforeEach(() => {\n vi.clearAllMocks();\n // EN: Mock successful auth by default\n // VI: Mock xác thực thành công theo mặc định \n // ES: Simular autenticación exitosa por defecto\n const { getServerSession } = require('../../../lib/auth');\n getServerSession.mockResolvedValue({\n user: { id: mockUserId },\n });\n });\n\n afterEach(() => {\n vi.resetAllMocks();\n });\n\n describe('GET /api/voters', () => {\n it('should return voters for authenticated user campaign', async () => {\n // EN: Mock successful database query\n // VI: Mock truy vấn cơ sở dữ liệu thành công\n // ES: Simular consulta exitosa a base de datos\n mockSupabase.single.mockResolvedValue({\n data: [mockVoter],\n error: null,\n });\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?campaign_id=${mockCampaignId}`\n );\n\n const response = await GET(request);\n const data = await response.json();\n\n expect(response.status).toBe(200);\n expect(data).toEqual([mockVoter]);\n expect(mockSupabase.from).toHaveBeenCalledWith('voters');\n expect(mockSupabase.eq).toHaveBeenCalledWith('campaign_id', mockCampaignId);\n });\n\n it('should return 401 for unauthenticated requests', async () => {\n // EN: Mock unauthenticated session\n // VI: Mock phiên không được xác thực\n // ES: Simular sesión no autenticada\n const { getServerSession } = require('../../../lib/auth');\n getServerSession.mockResolvedValue(null);\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?campaign_id=${mockCampaignId}`\n );\n\n const response = await GET(request);\n const data = await response.json();\n\n expect(response.status).toBe(401);\n expect(data).toEqual({ error: 'Unauthorized' });\n });\n\n it('should return 400 for missing campaign_id', async () => {\n const request = new NextRequest('http://localhost:3000/api/voters');\n\n const response = await GET(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data).toEqual({ error: 'Campaign ID is required' });\n });\n\n it('should handle database errors', async () => {\n // EN: Mock database error\n // VI: Mock lỗi cơ sở dữ liệu \n // ES: Simular error de base de datos\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { message: 'Database error' },\n });\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?campaign_id=${mockCampaignId}`\n );\n\n const response = await GET(request);\n const data = await response.json();\n\n expect(response.status).toBe(500);\n expect(data).toEqual({ error: 'Failed to fetch voters' });\n });\n });\n\n describe('POST /api/voters', () => {\n const validVoterData = {\n campaign_id: mockCampaignId,\n curp: 'ABCD123456HDFGHI01',\n first_name: 'María',\n last_name: 'González',\n email: 'maria.gonzalez@email.com',\n phone: '+525587654321',\n address: 'Avenida Central 456',\n district: 'Distrito 2',\n section: 'Sección B',\n };\n\n it('should create voter with valid data', async () => {\n // EN: Mock successful database insert\n // VI: Mock chèn cơ sở dữ liệu thành công\n // ES: Simular inserción exitosa en base de datos\n const createdVoter = {\n ...validVoterData,\n id: 'new-voter-id',\n status: 'active',\n created_at: '2024-01-01T00:00:00Z',\n updated_at: '2024-01-01T00:00:00Z',\n };\n\n mockSupabase.single.mockResolvedValue({\n data: createdVoter,\n error: null,\n });\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(validVoterData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(201);\n expect(data).toEqual(createdVoter);\n expect(mockSupabase.from).toHaveBeenCalledWith('voters');\n expect(mockSupabase.insert).toHaveBeenCalledWith(validVoterData);\n });\n\n it('should validate CURP format', async () => {\n const invalidData = {\n ...validVoterData,\n curp: 'INVALID_CURP',\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(invalidData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('Invalid CURP format');\n });\n\n it('should validate required fields', async () => {\n const incompleteData = {\n campaign_id: mockCampaignId,\n first_name: 'Test',\n // EN: Missing required fields\n // VI: Thiếu các trường bắt buộc\n // ES: Faltan campos obligatorios\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(incompleteData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('validation');\n });\n\n it('should validate email format', async () => {\n const invalidEmailData = {\n ...validVoterData,\n email: 'invalid-email',\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(invalidEmailData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('email');\n });\n\n it('should handle duplicate CURP error', async () => {\n // EN: Mock duplicate key error from database\n // VI: Mock lỗi khóa trùng lặp từ cơ sở dữ liệu\n // ES: Simular error de clave duplicada de base de datos\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { code: '23505', message: 'duplicate key value' },\n });\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(validVoterData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(409);\n expect(data).toEqual({ error: 'Voter with this CURP already exists' });\n });\n\n it('should return 401 for unauthenticated requests', async () => {\n const { getServerSession } = require('../../../lib/auth');\n getServerSession.mockResolvedValue(null);\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(validVoterData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n expect(response.status).toBe(401);\n expect(data).toEqual({ error: 'Unauthorized' });\n });\n });\n\n describe('PUT /api/voters', () => {\n const updateData = {\n id: mockVoter.id,\n campaign_id: mockCampaignId,\n first_name: 'Juan Carlos',\n email: 'juancarlos.perez@email.com',\n };\n\n it('should update voter with valid data', async () => {\n const updatedVoter = {\n ...mockVoter,\n ...updateData,\n updated_at: '2024-01-02T00:00:00Z',\n };\n\n mockSupabase.single.mockResolvedValue({\n data: updatedVoter,\n error: null,\n });\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'PUT',\n body: JSON.stringify(updateData),\n });\n\n const response = await PUT(request);\n const data = await response.json();\n\n expect(response.status).toBe(200);\n expect(data).toEqual(updatedVoter);\n expect(mockSupabase.update).toHaveBeenCalled();\n expect(mockSupabase.eq).toHaveBeenCalledWith('id', mockVoter.id);\n });\n\n it('should validate CURP format on update', async () => {\n const invalidUpdate = {\n ...updateData,\n curp: 'INVALID_FORMAT',\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'PUT',\n body: JSON.stringify(invalidUpdate),\n });\n\n const response = await PUT(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('Invalid CURP format');\n });\n\n it('should enforce campaign isolation', async () => {\n // EN: Mock voter from different campaign\n // VI: Mock cử tri từ chiến dịch khác\n // ES: Simular votante de campaña diferente\n const otherCampaignUpdate = {\n ...updateData,\n campaign_id: 'other-campaign-id',\n };\n\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { message: 'No rows found' },\n });\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'PUT',\n body: JSON.stringify(otherCampaignUpdate),\n });\n\n const response = await PUT(request);\n const data = await response.json();\n\n expect(response.status).toBe(404);\n expect(data).toEqual({ error: 'Voter not found' });\n });\n\n it('should require voter ID', async () => {\n const noIdData = {\n campaign_id: mockCampaignId,\n first_name: 'Test',\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'PUT',\n body: JSON.stringify(noIdData),\n });\n\n const response = await PUT(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('ID is required');\n });\n });\n\n describe('DELETE /api/voters', () => {\n it('should delete voter with valid ID', async () => {\n mockSupabase.single.mockResolvedValue({\n data: mockVoter,\n error: null,\n });\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?id=${mockVoter.id}&campaign_id=${mockCampaignId}`,\n { method: 'DELETE' }\n );\n\n const response = await DELETE(request);\n const data = await response.json();\n\n expect(response.status).toBe(200);\n expect(data).toEqual({ message: 'Voter deleted successfully' });\n expect(mockSupabase.delete).toHaveBeenCalled();\n expect(mockSupabase.eq).toHaveBeenCalledWith('id', mockVoter.id);\n });\n\n it('should enforce campaign isolation on delete', async () => {\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { message: 'No rows found' },\n });\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?id=${mockVoter.id}&campaign_id=other-campaign`,\n { method: 'DELETE' }\n );\n\n const response = await DELETE(request);\n const data = await response.json();\n\n expect(response.status).toBe(404);\n expect(data).toEqual({ error: 'Voter not found' });\n });\n\n it('should require voter ID and campaign ID', async () => {\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'DELETE',\n });\n\n const response = await DELETE(request);\n const data = await response.json();\n\n expect(response.status).toBe(400);\n expect(data.error).toContain('required');\n });\n\n it('should return 401 for unauthenticated requests', async () => {\n const { getServerSession } = require('../../../lib/auth');\n getServerSession.mockResolvedValue(null);\n\n const request = new NextRequest(\n `http://localhost:3000/api/voters?id=${mockVoter.id}&campaign_id=${mockCampaignId}`,\n { method: 'DELETE' }\n );\n\n const response = await DELETE(request);\n const data = await response.json();\n\n expect(response.status).toBe(401);\n expect(data).toEqual({ error: 'Unauthorized' });\n });\n });\n\n describe('CURP Validation', () => {\n const testCases = [\n {\n curp: 'ABCD123456HDFGHI01',\n valid: true,\n description: 'valid CURP format',\n },\n {\n curp: 'abcd123456hdfghi01',\n valid: false,\n description: 'lowercase letters',\n },\n {\n curp: 'ABCD123456HDFGHI0',\n valid: false,\n description: 'too short',\n },\n {\n curp: 'ABCD123456HDFGHI012',\n valid: false,\n description: 'too long',\n },\n {\n curp: '1234567890123456AB',\n valid: false,\n description: 'invalid character pattern',\n },\n {\n curp: '',\n valid: false,\n description: 'empty string',\n },\n ];\n\n testCases.forEach(({ curp, valid, description }) => {\n it(`should ${valid ? 'accept' : 'reject'} ${description}`, async () => {\n const voterData = {\n campaign_id: mockCampaignId,\n curp,\n first_name: 'Test',\n last_name: 'User',\n email: 'test@email.com',\n phone: '+525512345678',\n address: 'Test Address',\n district: 'Test District',\n section: 'Test Section',\n };\n\n if (valid) {\n mockSupabase.single.mockResolvedValue({\n data: { ...voterData, id: 'test-id' },\n error: null,\n });\n }\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'POST',\n body: JSON.stringify(voterData),\n });\n\n const response = await POST(request);\n const data = await response.json();\n\n if (valid) {\n expect(response.status).toBe(201);\n } else {\n expect(response.status).toBe(400);\n expect(data.error).toContain('CURP');\n }\n });\n });\n });\n\n describe('Campaign Isolation', () => {\n it('should only return voters from user campaign', async () => {\n const campaign1Voters = [\n { ...mockVoter, id: 'voter1', campaign_id: 'campaign1' },\n ];\n const campaign2Voters = [\n { ...mockVoter, id: 'voter2', campaign_id: 'campaign2' },\n ];\n\n // EN: Mock database to return only campaign1 voters\n // VI: Mock cơ sở dữ liệu để chỉ trả về cử tri campaign1\n // ES: Simular base de datos para devolver solo votantes de campaign1\n mockSupabase.single.mockResolvedValue({\n data: campaign1Voters,\n error: null,\n });\n\n const request = new NextRequest(\n 'http://localhost:3000/api/voters?campaign_id=campaign1'\n );\n\n const response = await GET(request);\n const data = await response.json();\n\n expect(response.status).toBe(200);\n expect(data).toEqual(campaign1Voters);\n expect(data).not.toContain(campaign2Voters[0]);\n expect(mockSupabase.eq).toHaveBeenCalledWith('campaign_id', 'campaign1');\n });\n\n it('should prevent cross-campaign voter updates', async () => {\n // EN: Mock attempting to update voter from different campaign\n // VI: Mock thử cập nhật cử tri từ chiến dịch khác\n // ES: Simular intento de actualizar votante de campaña diferente\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { message: 'No rows found' },\n });\n\n const crossCampaignUpdate = {\n id: 'voter-from-other-campaign',\n campaign_id: 'user-campaign',\n first_name: 'Updated Name',\n };\n\n const request = new NextRequest('http://localhost:3000/api/voters', {\n method: 'PUT',\n body: JSON.stringify(crossCampaignUpdate),\n });\n\n const response = await PUT(request);\n const data = await response.json();\n\n expect(response.status).toBe(404);\n expect(data).toEqual({ error: 'Voter not found' });\n });\n\n it('should prevent cross-campaign voter deletion', async () => {\n mockSupabase.single.mockResolvedValue({\n data: null,\n error: { message: 'No rows found' },\n });\n\n const request = new NextRequest(\n 'http://localhost:3000/api/voters?id=voter-from-other-campaign&campaign_id=user-campaign',\n { method: 'DELETE' }\n );\n\n const response = await DELETE(request);\n const data = await response.json();\n\n expect(response.status).toBe(404);\n expect(data).toEqual({ error: 'Voter not found' });\n });\n });\n});\n```\n\nThis comprehensive test file covers:\n\n**CRUD Operations:**\n- GET: Fetch voters with filtering and error handling\n- POST: Create voters with validation\n- PUT: Update voters with validation and isolation\n- DELETE: Remove voters with proper authorization\n\n**CURP Validation:**\n- Valid and invalid CURP format testing\n- Edge cases (empty, too short/long, invalid patterns)\n- Case sensitivity validation\n\n**Campaign Isolation:**\n- Users can only access voters from their campaigns\n- Cross-campaign operations are prevented\n- Proper filtering by campaign_id\n\n**Additional Features:**\n- Authentication testing\n- Error handling for database operations\n- Duplicate CURP detection\n- Email format validation\n- Required field validation\n- Proper HTTP status codes and error messages\n\nThe tests follow the codebase conventions with trilingual comments, SPEC references, and proper mocking of Supabase operations."
}Task ID: e3f8ee3b-5306-4dd1-9932-c5d6646a1839