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>
261 lines
6.5 KiB
TypeScript
261 lines
6.5 KiB
TypeScript
/**
|
|
* Auth Context Tests
|
|
* Tests for the authentication context and has_voted state fix
|
|
*/
|
|
|
|
import React from "react"
|
|
import { render, screen, waitFor } from "@testing-library/react"
|
|
import { AuthProvider, useAuth } from "@/lib/auth-context"
|
|
import * as api from "@/lib/api"
|
|
|
|
// Mock the API module
|
|
jest.mock("@/lib/api", () => ({
|
|
authApi: {
|
|
login: jest.fn(),
|
|
register: jest.fn(),
|
|
getProfile: jest.fn(),
|
|
logout: jest.fn(),
|
|
},
|
|
getAuthToken: jest.fn(),
|
|
setAuthToken: jest.fn(),
|
|
clearAuthToken: jest.fn(),
|
|
}))
|
|
|
|
// Mock window.localStorage
|
|
const localStorageMock = {
|
|
getItem: jest.fn(),
|
|
setItem: jest.fn(),
|
|
removeItem: jest.fn(),
|
|
clear: jest.fn(),
|
|
}
|
|
global.localStorage = localStorageMock as any
|
|
|
|
describe("Auth Context - Bug #2: has_voted State Fix", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
localStorageMock.getItem.mockReturnValue(null)
|
|
})
|
|
|
|
test("login response includes has_voted field", async () => {
|
|
const mockLoginResponse = {
|
|
data: {
|
|
access_token: "test-token",
|
|
id: 1,
|
|
email: "test@example.com",
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
has_voted: false,
|
|
expires_in: 1800,
|
|
},
|
|
status: 200,
|
|
}
|
|
|
|
;(api.authApi.login as jest.Mock).mockResolvedValue(mockLoginResponse)
|
|
;(api.setAuthToken as jest.Mock).mockImplementation(() => {})
|
|
|
|
let authContextValue: any
|
|
|
|
const TestComponent = () => {
|
|
authContextValue = useAuth()
|
|
return <div>{authContextValue.isLoading ? "Loading..." : "Ready"}</div>
|
|
}
|
|
|
|
const { rerender } = render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
)
|
|
|
|
// Simulate login
|
|
await waitFor(async () => {
|
|
await authContextValue.login("test@example.com", "password123")
|
|
})
|
|
|
|
expect(authContextValue.user).toBeDefined()
|
|
expect(authContextValue.user?.has_voted).toBeDefined()
|
|
expect(typeof authContextValue.user?.has_voted).toBe("boolean")
|
|
})
|
|
|
|
test("register response includes has_voted field", async () => {
|
|
const mockRegisterResponse = {
|
|
data: {
|
|
access_token: "test-token",
|
|
id: 2,
|
|
email: "newuser@example.com",
|
|
first_name: "New",
|
|
last_name: "User",
|
|
has_voted: false,
|
|
expires_in: 1800,
|
|
},
|
|
status: 200,
|
|
}
|
|
|
|
;(api.authApi.register as jest.Mock).mockResolvedValue(mockRegisterResponse)
|
|
;(api.setAuthToken as jest.Mock).mockImplementation(() => {})
|
|
|
|
let authContextValue: any
|
|
|
|
const TestComponent = () => {
|
|
authContextValue = useAuth()
|
|
return <div>{authContextValue.isLoading ? "Loading..." : "Ready"}</div>
|
|
}
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
)
|
|
|
|
// Simulate registration
|
|
await waitFor(async () => {
|
|
await authContextValue.register(
|
|
"newuser@example.com",
|
|
"password123",
|
|
"New",
|
|
"User",
|
|
"ID123456"
|
|
)
|
|
})
|
|
|
|
expect(authContextValue.user?.has_voted).toBe(false)
|
|
})
|
|
|
|
test("has_voted is correctly set from server response, not hardcoded", async () => {
|
|
const mockLoginResponseVoted = {
|
|
data: {
|
|
access_token: "test-token",
|
|
id: 3,
|
|
email: "voted@example.com",
|
|
first_name: "Voted",
|
|
last_name: "User",
|
|
has_voted: true, // User has already voted
|
|
expires_in: 1800,
|
|
},
|
|
status: 200,
|
|
}
|
|
|
|
;(api.authApi.login as jest.Mock).mockResolvedValue(mockLoginResponseVoted)
|
|
;(api.setAuthToken as jest.Mock).mockImplementation(() => {})
|
|
|
|
let authContextValue: any
|
|
|
|
const TestComponent = () => {
|
|
authContextValue = useAuth()
|
|
return <div>{authContextValue.isLoading ? "Loading..." : "Ready"}</div>
|
|
}
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
)
|
|
|
|
// Simulate login with user who has voted
|
|
await waitFor(async () => {
|
|
await authContextValue.login("voted@example.com", "password123")
|
|
})
|
|
|
|
// Verify has_voted is true (from server) not false (hardcoded)
|
|
expect(authContextValue.user?.has_voted).toBe(true)
|
|
})
|
|
|
|
test("has_voted defaults to false if not in response", async () => {
|
|
const mockLoginResponseNoField = {
|
|
data: {
|
|
access_token: "test-token",
|
|
id: 4,
|
|
email: "nofield@example.com",
|
|
first_name: "No",
|
|
last_name: "Field",
|
|
// has_voted missing from response
|
|
expires_in: 1800,
|
|
},
|
|
status: 200,
|
|
}
|
|
|
|
;(api.authApi.login as jest.Mock).mockResolvedValue(mockLoginResponseNoField)
|
|
;(api.setAuthToken as jest.Mock).mockImplementation(() => {})
|
|
|
|
let authContextValue: any
|
|
|
|
const TestComponent = () => {
|
|
authContextValue = useAuth()
|
|
return <div>{authContextValue.isLoading ? "Loading..." : "Ready"}</div>
|
|
}
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
)
|
|
|
|
await waitFor(async () => {
|
|
await authContextValue.login("nofield@example.com", "password123")
|
|
})
|
|
|
|
// Should default to false if not present
|
|
expect(authContextValue.user?.has_voted).toBe(false)
|
|
})
|
|
|
|
test("profile refresh updates has_voted state", async () => {
|
|
const mockProfileResponse = {
|
|
data: {
|
|
id: 5,
|
|
email: "profile@example.com",
|
|
first_name: "Profile",
|
|
last_name: "User",
|
|
has_voted: true,
|
|
created_at: new Date().toISOString(),
|
|
},
|
|
status: 200,
|
|
}
|
|
|
|
;(api.authApi.getProfile as jest.Mock).mockResolvedValue(mockProfileResponse)
|
|
;(api.getAuthToken as jest.Mock).mockReturnValue("test-token")
|
|
|
|
let authContextValue: any
|
|
|
|
const TestComponent = () => {
|
|
authContextValue = useAuth()
|
|
return (
|
|
<div>
|
|
{authContextValue.user?.has_voted !== undefined
|
|
? `has_voted: ${authContextValue.user.has_voted}`
|
|
: "no user"}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(
|
|
<AuthProvider>
|
|
<TestComponent />
|
|
</AuthProvider>
|
|
)
|
|
|
|
// Simulate profile refresh
|
|
await waitFor(async () => {
|
|
await authContextValue.refreshProfile()
|
|
})
|
|
|
|
expect(authContextValue.user?.has_voted).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("Auth Context - API Token Type Fix", () => {
|
|
test("AuthToken interface includes has_voted field", () => {
|
|
// This test ensures the TypeScript interface is correct
|
|
const token: api.AuthToken = {
|
|
access_token: "token",
|
|
expires_in: 1800,
|
|
id: 1,
|
|
email: "test@example.com",
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
has_voted: false,
|
|
}
|
|
|
|
expect(token.has_voted).toBeDefined()
|
|
expect(typeof token.has_voted).toBe("boolean")
|
|
})
|
|
})
|