This commit fixes 5 critical bugs found during code review: Bug #1 (CRITICAL): Missing API endpoints for election filtering - Added GET /api/elections/upcoming endpoint - Added GET /api/elections/completed endpoint - Both properly filter elections by date Bug #2 (HIGH): Auth context has_voted state inconsistency - Backend schemas now include has_voted in LoginResponse and RegisterResponse - Auth routes return actual has_voted value from database - Frontend context uses server response instead of hardcoding false - Frontend API client properly typed with has_voted field Bug #3 (HIGH): Transaction safety in vote submission - Simplified error handling in vote submission endpoints - Now only calls mark_as_voted() once at the end - Vote response includes voter_marked_voted flag to indicate success - Ensures consistency even if blockchain submission fails Bug #4 (MEDIUM): Vote status endpoint - Verified endpoint already exists at GET /api/votes/status - Tests confirm proper functionality Bug #5 (MEDIUM): Response format inconsistency - Previously fixed in commit e10a882 - Frontend now handles both array and wrapped object formats Added comprehensive test coverage: - 20+ backend API tests (tests/test_api_fixes.py) - 6+ auth context tests (frontend/__tests__/auth-context.test.tsx) - 8+ elections API tests (frontend/__tests__/elections-api.test.ts) - 10+ vote submission tests (frontend/__tests__/vote-submission.test.ts) All fixes ensure frontend and backend communicate consistently. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
195 lines
5.5 KiB
TypeScript
195 lines
5.5 KiB
TypeScript
/**
|
|
* Elections API Tests
|
|
* Tests for Bug #1: Missing /api/elections/upcoming and /completed endpoints
|
|
*/
|
|
|
|
import * as api from "@/lib/api"
|
|
|
|
// Mock fetch
|
|
global.fetch = jest.fn()
|
|
|
|
describe("Elections API - Bug #1: Missing Endpoints Fix", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
localStorage.getItem = jest.fn().mockReturnValue("test-token")
|
|
})
|
|
|
|
test("getActive elections endpoint works", async () => {
|
|
const mockElections = [
|
|
{
|
|
id: 1,
|
|
name: "Active Election",
|
|
description: "Currently active",
|
|
start_date: new Date().toISOString(),
|
|
end_date: new Date(Date.now() + 86400000).toISOString(),
|
|
is_active: true,
|
|
results_published: false,
|
|
candidates: [],
|
|
},
|
|
]
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockElections,
|
|
})
|
|
|
|
const response = await api.electionsApi.getActive()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(response.data).toEqual(mockElections)
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/elections/active"),
|
|
expect.any(Object)
|
|
)
|
|
})
|
|
|
|
test("getUpcoming elections endpoint works", async () => {
|
|
const mockUpcomingElections = [
|
|
{
|
|
id: 2,
|
|
name: "Upcoming Election",
|
|
description: "Starting soon",
|
|
start_date: new Date(Date.now() + 864000000).toISOString(),
|
|
end_date: new Date(Date.now() + 950400000).toISOString(),
|
|
is_active: true,
|
|
results_published: false,
|
|
candidates: [],
|
|
},
|
|
]
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockUpcomingElections,
|
|
})
|
|
|
|
const response = await api.electionsApi.getUpcoming()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(response.data).toEqual(mockUpcomingElections)
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/elections/upcoming"),
|
|
expect.any(Object)
|
|
)
|
|
})
|
|
|
|
test("getCompleted elections endpoint works", async () => {
|
|
const mockCompletedElections = [
|
|
{
|
|
id: 3,
|
|
name: "Completed Election",
|
|
description: "Already finished",
|
|
start_date: new Date(Date.now() - 864000000).toISOString(),
|
|
end_date: new Date(Date.now() - 777600000).toISOString(),
|
|
is_active: true,
|
|
results_published: true,
|
|
candidates: [],
|
|
},
|
|
]
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockCompletedElections,
|
|
})
|
|
|
|
const response = await api.electionsApi.getCompleted()
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(response.data).toEqual(mockCompletedElections)
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining("/api/elections/completed"),
|
|
expect.any(Object)
|
|
)
|
|
})
|
|
|
|
test("all election endpoints accept authentication token", async () => {
|
|
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => [],
|
|
})
|
|
|
|
const token = "test-auth-token"
|
|
;(localStorage.getItem as jest.Mock).mockReturnValue(token)
|
|
|
|
await api.electionsApi.getActive()
|
|
|
|
const callArgs = (global.fetch as jest.Mock).mock.calls[0][1]
|
|
expect(callArgs.headers.Authorization).toBe(`Bearer ${token}`)
|
|
})
|
|
|
|
test("election endpoints handle errors gracefully", async () => {
|
|
const errorMessage = "Server error"
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 500,
|
|
json: async () => ({ detail: errorMessage }),
|
|
})
|
|
|
|
const response = await api.electionsApi.getUpcoming()
|
|
|
|
expect(response.error).toBeDefined()
|
|
expect(response.status).toBe(500)
|
|
})
|
|
|
|
test("election endpoints return array of elections", async () => {
|
|
const mockElections = [
|
|
{ id: 1, name: "Election 1" },
|
|
{ id: 2, name: "Election 2" },
|
|
]
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockElections,
|
|
})
|
|
|
|
const response = await api.electionsApi.getActive()
|
|
|
|
expect(Array.isArray(response.data)).toBe(true)
|
|
expect(response.data).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe("Elections API - Response Format Consistency", () => {
|
|
test("all election endpoints return consistent response format", async () => {
|
|
const mockData = []
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockData,
|
|
})
|
|
|
|
const activeResp = await api.electionsApi.getActive()
|
|
const upcomingResp = await api.electionsApi.getUpcoming()
|
|
const completedResp = await api.electionsApi.getCompleted()
|
|
|
|
// All should have same structure
|
|
expect(activeResp).toHaveProperty("data")
|
|
expect(activeResp).toHaveProperty("status")
|
|
expect(upcomingResp).toHaveProperty("data")
|
|
expect(upcomingResp).toHaveProperty("status")
|
|
expect(completedResp).toHaveProperty("data")
|
|
expect(completedResp).toHaveProperty("status")
|
|
})
|
|
|
|
test("election endpoints return array directly, not wrapped in object", async () => {
|
|
const mockElections = [{ id: 1, name: "Test" }]
|
|
|
|
;(global.fetch as jest.Mock).mockResolvedValueOnce({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => mockElections,
|
|
})
|
|
|
|
const response = await api.electionsApi.getActive()
|
|
|
|
// Should be array, not { elections: [...] }
|
|
expect(Array.isArray(response.data)).toBe(true)
|
|
expect(response.data[0].name).toBe("Test")
|
|
})
|
|
})
|