From 3baf126a559d97e212eb8d7b2734ec980699fc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Tue, 10 Mar 2026 16:53:21 +0100 Subject: [PATCH] add legislation API client for frontend --- .../legislation/lib/legislation-api.test.ts | 72 +++++++++++++++++++ .../legislation/lib/legislation-api.ts | 45 ++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/client/features/legislation/lib/legislation-api.test.ts create mode 100644 src/client/features/legislation/lib/legislation-api.ts diff --git a/src/client/features/legislation/lib/legislation-api.test.ts b/src/client/features/legislation/lib/legislation-api.test.ts new file mode 100644 index 0000000..c92a854 --- /dev/null +++ b/src/client/features/legislation/lib/legislation-api.test.ts @@ -0,0 +1,72 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const mockFetch = vi.fn() +vi.stubGlobal("fetch", mockFetch) + +const { fetchLegislation, castVote, fetchUserVote } = await import( + "./legislation-api" +) + +describe("legislation-api", () => { + beforeEach(() => { + mockFetch.mockReset() + }) + + it("fetches legislation detail", async () => { + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + id: 1, + dipVorgangsId: 100, + title: "Test Gesetz", + abstract: "Summary", + fullText: null, + summary: null, + fetchedAt: "2026-03-10T00:00:00Z", + }), + ), + ) + + const result = await fetchLegislation(1) + expect(result.title).toBe("Test Gesetz") + }) + + it("casts a vote", async () => { + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify({ ok: true }), { status: 201 }), + ) + + await castVote(1, "device-123", "ja") + expect(mockFetch).toHaveBeenCalledOnce() + const [url, opts] = mockFetch.mock.calls[0] + expect(url).toContain("/legislation/1/vote") + expect(opts.method).toBe("POST") + }) + + it("fetches existing user vote", async () => { + mockFetch.mockResolvedValueOnce( + new Response( + JSON.stringify({ + legislationId: 1, + vote: "ja", + votedAt: "2026-03-10T00:00:00Z", + }), + ), + ) + + const result = await fetchUserVote(1, "device-123") + expect(result).not.toBeNull() + expect(result?.vote).toBe("ja") + }) + + it("returns null when no user vote exists (404)", async () => { + mockFetch.mockResolvedValueOnce( + new Response(JSON.stringify({ error: "no vote found" }), { + status: 404, + }), + ) + + const result = await fetchUserVote(1, "device-123") + expect(result).toBeNull() + }) +}) diff --git a/src/client/features/legislation/lib/legislation-api.ts b/src/client/features/legislation/lib/legislation-api.ts new file mode 100644 index 0000000..0d0795a --- /dev/null +++ b/src/client/features/legislation/lib/legislation-api.ts @@ -0,0 +1,45 @@ +import { BACKEND_URL } from "@/shared/lib/constants" +import type { + LegislationDetail, + UserVoteChoice, + UserVoteRecord, +} from "../../../../shared/legislation-types" + +export async function fetchLegislation(id: number): Promise { + const res = await fetch(`${BACKEND_URL}/legislation/${id}`) + if (!res.ok) throw new Error(`Failed to fetch legislation ${id}`) + return res.json() +} + +export async function fetchLegislationText(id: number): Promise { + const res = await fetch(`${BACKEND_URL}/legislation/${id}/text`) + if (!res.ok) return null + const data = await res.json() + return data.text ?? null +} + +export async function castVote( + legislationId: number, + deviceId: string, + vote: UserVoteChoice, +): Promise { + const res = await fetch(`${BACKEND_URL}/legislation/${legislationId}/vote`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deviceId, vote }), + }) + if (!res.ok) + throw new Error(`Failed to cast vote for legislation ${legislationId}`) +} + +export async function fetchUserVote( + legislationId: number, + deviceId: string, +): Promise { + const res = await fetch( + `${BACKEND_URL}/legislation/${legislationId}/vote/${deviceId}`, + ) + if (res.status === 404) return null + if (!res.ok) throw new Error("Failed to fetch vote") + return res.json() +}