Backend (Go): - Expanded integration_test.go: health, auth middleware (expired/invalid/wrong-secret JWT), tenant CRUD, case CRUD (create/list/get/update/delete + filters + validation), deadline CRUD (create/list/update/complete/delete), appointment CRUD, dashboard (verifies all sections), deadline calculator (valid/invalid/unknown type), proceeding types & rules, document endpoints, AI extraction (no-key path), and full critical path E2E (auth -> case -> deadline -> appointment -> dashboard -> complete) - New handler unit tests: case (10), appointment (11), dashboard (1), calculate (5), document (10), AI (4) — all testing validation, auth guards, and error paths without DB - Total: ~80 backend tests (unit + integration) Frontend (TypeScript/Vitest): - Installed vitest 2.x, @testing-library/react, @testing-library/jest-dom, jsdom 24, msw - vitest.config.ts with jsdom env, esbuild JSX automatic, path aliases - API client tests (13): URL construction, no double /api/, auth header, tenant header, POST/PUT/PATCH/DELETE methods, error handling, 204 responses - DeadlineTrafficLights tests (5): renders cards, correct counts, zero state, onFilter callback - CaseOverviewGrid tests (4): renders categories, counts, header, zero state - LoginPage tests (8): form rendering, mode toggle, password login, redirect, error display, magic link, registration link - Total: 30 frontend tests Makefile: test-frontend target now runs vitest instead of placeholder echo.
183 lines
5.6 KiB
TypeScript
183 lines
5.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// Mock Supabase client
|
|
const mockGetSession = vi.fn();
|
|
vi.mock("@/lib/supabase/client", () => ({
|
|
createClient: () => ({
|
|
auth: {
|
|
getSession: mockGetSession,
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// Must import after mock setup
|
|
const { api } = await import("@/lib/api");
|
|
|
|
describe("ApiClient", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
localStorage.clear();
|
|
mockGetSession.mockResolvedValue({
|
|
data: { session: { access_token: "test-token-123" } },
|
|
});
|
|
});
|
|
|
|
it("constructs correct URL with /api base", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ cases: [], total: 0 }), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/cases");
|
|
|
|
expect(fetchSpy).toHaveBeenCalledWith(
|
|
"/api/cases",
|
|
expect.objectContaining({ method: "GET" }),
|
|
);
|
|
});
|
|
|
|
it("does not double-prefix /api/", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/deadlines");
|
|
|
|
const url = fetchSpy.mock.calls[0][0] as string;
|
|
expect(url).toBe("/api/deadlines");
|
|
expect(url).not.toContain("/api/api/");
|
|
});
|
|
|
|
it("sets Authorization header from Supabase session", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/cases");
|
|
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
const headers = requestInit.headers as Record<string, string>;
|
|
expect(headers["Authorization"]).toBe("Bearer test-token-123");
|
|
});
|
|
|
|
it("sets X-Tenant-ID header from localStorage", async () => {
|
|
localStorage.setItem("kanzlai_tenant_id", "tenant-uuid-123");
|
|
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/cases");
|
|
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
const headers = requestInit.headers as Record<string, string>;
|
|
expect(headers["X-Tenant-ID"]).toBe("tenant-uuid-123");
|
|
});
|
|
|
|
it("omits X-Tenant-ID when not in localStorage", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/cases");
|
|
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
const headers = requestInit.headers as Record<string, string>;
|
|
expect(headers["X-Tenant-ID"]).toBeUndefined();
|
|
});
|
|
|
|
it("omits Authorization when no session", async () => {
|
|
mockGetSession.mockResolvedValue({
|
|
data: { session: null },
|
|
});
|
|
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.get("/cases");
|
|
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
const headers = requestInit.headers as Record<string, string>;
|
|
expect(headers["Authorization"]).toBeUndefined();
|
|
});
|
|
|
|
it("sends POST with JSON body", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ id: "new-id" }), { status: 201 }),
|
|
);
|
|
|
|
const body = { case_number: "TEST/001", title: "Test Case" };
|
|
await api.post("/cases", body);
|
|
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
expect(requestInit.method).toBe("POST");
|
|
expect(requestInit.body).toBe(JSON.stringify(body));
|
|
const headers = requestInit.headers as Record<string, string>;
|
|
expect(headers["Content-Type"]).toBe("application/json");
|
|
});
|
|
|
|
it("sends PUT with JSON body", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.put("/cases/123", { title: "Updated" });
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
expect(requestInit.method).toBe("PUT");
|
|
});
|
|
|
|
it("sends PATCH with JSON body", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.patch("/deadlines/123/complete", {});
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
expect(requestInit.method).toBe("PATCH");
|
|
});
|
|
|
|
it("sends DELETE", async () => {
|
|
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({}), { status: 200 }),
|
|
);
|
|
|
|
await api.delete("/cases/123");
|
|
const requestInit = fetchSpy.mock.calls[0][1] as RequestInit;
|
|
expect(requestInit.method).toBe("DELETE");
|
|
});
|
|
|
|
it("throws ApiError on non-ok response", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ error: "not found" }), { status: 404 }),
|
|
);
|
|
|
|
await expect(api.get("/cases/nonexistent")).rejects.toEqual({
|
|
error: "not found",
|
|
status: 404,
|
|
});
|
|
});
|
|
|
|
it("handles 204 No Content response", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(null, { status: 204 }),
|
|
);
|
|
|
|
const result = await api.delete("/appointments/123");
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("handles error response without JSON body", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response("Internal Server Error", {
|
|
status: 500,
|
|
statusText: "Internal Server Error",
|
|
}),
|
|
);
|
|
|
|
await expect(api.get("/broken")).rejects.toEqual({
|
|
error: "Internal Server Error",
|
|
status: 500,
|
|
});
|
|
});
|
|
});
|