FOUNDRY
C8 Platform
← 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