/** * Vote Submission Tests * Tests for Bug #3: Transaction safety in vote submission * Tests for Bug #4: Vote status endpoint */ import * as api from "@/lib/api" // Mock fetch global.fetch = jest.fn() describe("Vote Submission API - Bug #3 & #4: Transaction Safety and Status Endpoint", () => { beforeEach(() => { jest.clearAllMocks() localStorage.getItem = jest.fn().mockReturnValue("test-token") }) test("submitVote endpoint exists and works", async () => { const mockVoteResponse = { id: 1, ballot_hash: "hash123", timestamp: Date.now(), blockchain: { status: "submitted", transaction_id: "tx-123" }, voter_marked_voted: true, } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockVoteResponse, }) const response = await api.votesApi.submitVote(1, "Yes") expect(response.status).toBe(200) expect(response.data).toEqual(mockVoteResponse) }) test("vote response includes voter_marked_voted flag", async () => { const mockVoteResponse = { id: 1, ballot_hash: "hash123", timestamp: Date.now(), blockchain: { status: "submitted", transaction_id: "tx-123" }, voter_marked_voted: true, } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockVoteResponse, }) const response = await api.votesApi.submitVote(1, "Yes") expect(response.data).toHaveProperty("voter_marked_voted") expect(typeof response.data.voter_marked_voted).toBe("boolean") }) test("vote response includes blockchain status information", async () => { const mockVoteResponse = { id: 1, ballot_hash: "hash123", timestamp: Date.now(), blockchain: { status: "submitted", transaction_id: "tx-abc123", block_hash: "block-123", }, voter_marked_voted: true, } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockVoteResponse, }) const response = await api.votesApi.submitVote(1, "No") expect(response.data.blockchain).toBeDefined() expect(response.data.blockchain.status).toBeDefined() }) test("getStatus endpoint exists and returns has_voted", async () => { const mockStatusResponse = { has_voted: false } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockStatusResponse, }) const response = await api.votesApi.getStatus(1) expect(response.status).toBe(200) expect(response.data.has_voted).toBeDefined() expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining("/api/votes/status"), expect.any(Object) ) }) test("getStatus endpoint requires election_id parameter", async () => { ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ has_voted: false }), }) await api.votesApi.getStatus(123) const callUrl = (global.fetch as jest.Mock).mock.calls[0][0] expect(callUrl).toContain("election_id=123") }) test("getStatus correctly identifies if user already voted", async () => { const mockStatusResponse = { has_voted: true } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockStatusResponse, }) const response = await api.votesApi.getStatus(1) expect(response.data.has_voted).toBe(true) }) test("vote endpoints include authentication token", async () => { const token = "auth-token-123" ;(localStorage.getItem as jest.Mock).mockReturnValue(token) ;(global.fetch as jest.Mock).mockResolvedValue({ ok: true, status: 200, json: async () => ({}), }) await api.votesApi.submitVote(1, "Yes") const callArgs = (global.fetch as jest.Mock).mock.calls[0][1] expect(callArgs.headers.Authorization).toBe(`Bearer ${token}`) }) test("vote submission handles blockchain submission failure gracefully", async () => { const mockVoteResponse = { id: 1, ballot_hash: "hash123", timestamp: Date.now(), blockchain: { status: "database_only", transaction_id: "tx-123", warning: "Vote recorded in database but blockchain submission failed", }, voter_marked_voted: true, } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockVoteResponse, }) const response = await api.votesApi.submitVote(1, "Yes") // Even if blockchain failed, vote is still recorded expect(response.status).toBe(200) expect(response.data.id).toBeDefined() expect(response.data.blockchain.status).toBe("database_only") }) test("vote response indicates fallback blockchain status", async () => { const mockVoteResponseFallback = { id: 1, ballot_hash: "hash123", timestamp: Date.now(), blockchain: { status: "submitted_fallback", transaction_id: "tx-123", warning: "Vote recorded in local blockchain (PoA validators unreachable)", }, voter_marked_voted: true, } ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockVoteResponseFallback, }) const response = await api.votesApi.submitVote(1, "Yes") expect(response.data.blockchain.status).toBe("submitted_fallback") expect(response.data.voter_marked_voted).toBe(true) }) }) describe("Vote History API", () => { beforeEach(() => { jest.clearAllMocks() localStorage.getItem = jest.fn().mockReturnValue("test-token") }) test("getHistory endpoint returns vote history with has_voted info", async () => { const mockHistory: api.VoteHistory[] = [ { vote_id: 1, election_id: 1, election_name: "Test Election", candidate_name: "Test Candidate", vote_date: new Date().toISOString(), election_status: "closed", }, ] ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockHistory, }) const response = await api.votesApi.getHistory() expect(response.data).toEqual(mockHistory) expect(response.data[0]).toHaveProperty("vote_id") expect(response.data[0]).toHaveProperty("election_name") }) })