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.
144 lines
4.3 KiB
TypeScript
144 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
|
|
// Mock next/navigation
|
|
const mockPush = vi.fn();
|
|
const mockRefresh = vi.fn();
|
|
vi.mock("next/navigation", () => ({
|
|
useRouter: () => ({ push: mockPush, refresh: mockRefresh }),
|
|
}));
|
|
|
|
// Mock Supabase
|
|
const mockSignInWithPassword = vi.fn();
|
|
const mockSignInWithOtp = vi.fn();
|
|
vi.mock("@/lib/supabase/client", () => ({
|
|
createClient: () => ({
|
|
auth: {
|
|
signInWithPassword: mockSignInWithPassword,
|
|
signInWithOtp: mockSignInWithOtp,
|
|
},
|
|
}),
|
|
}));
|
|
|
|
// Import after mocks
|
|
const { default: LoginPage } = await import(
|
|
"@/app/(auth)/login/page"
|
|
);
|
|
|
|
describe("LoginPage", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("renders login form with email and password fields", () => {
|
|
render(<LoginPage />);
|
|
|
|
expect(screen.getByText("KanzlAI")).toBeInTheDocument();
|
|
expect(screen.getByText("Melden Sie sich an")).toBeInTheDocument();
|
|
expect(screen.getByLabelText("E-Mail")).toBeInTheDocument();
|
|
expect(screen.getByLabelText("Passwort")).toBeInTheDocument();
|
|
expect(screen.getByText("Anmelden")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders mode toggle between Passwort and Magic Link", () => {
|
|
render(<LoginPage />);
|
|
|
|
// "Passwort" appears twice (toggle button + label), so use getAllByText
|
|
const passwortElements = screen.getAllByText("Passwort");
|
|
expect(passwortElements.length).toBeGreaterThanOrEqual(1);
|
|
expect(screen.getByText("Magic Link")).toBeInTheDocument();
|
|
});
|
|
|
|
it("switches to magic link mode and hides password field", () => {
|
|
render(<LoginPage />);
|
|
|
|
fireEvent.click(screen.getByText("Magic Link"));
|
|
|
|
expect(screen.queryByLabelText("Passwort")).not.toBeInTheDocument();
|
|
expect(screen.getByText("Link senden")).toBeInTheDocument();
|
|
});
|
|
|
|
it("submits password login to Supabase", async () => {
|
|
mockSignInWithPassword.mockResolvedValue({ error: null });
|
|
render(<LoginPage />);
|
|
|
|
fireEvent.change(screen.getByLabelText("E-Mail"), {
|
|
target: { value: "test@kanzlei.de" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText("Passwort"), {
|
|
target: { value: "geheim123" },
|
|
});
|
|
fireEvent.click(screen.getByText("Anmelden"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockSignInWithPassword).toHaveBeenCalledWith({
|
|
email: "test@kanzlei.de",
|
|
password: "geheim123",
|
|
});
|
|
});
|
|
});
|
|
|
|
it("redirects to / on successful login", async () => {
|
|
mockSignInWithPassword.mockResolvedValue({ error: null });
|
|
render(<LoginPage />);
|
|
|
|
fireEvent.change(screen.getByLabelText("E-Mail"), {
|
|
target: { value: "test@kanzlei.de" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText("Passwort"), {
|
|
target: { value: "geheim123" },
|
|
});
|
|
fireEvent.click(screen.getByText("Anmelden"));
|
|
|
|
await waitFor(() => {
|
|
expect(mockPush).toHaveBeenCalledWith("/");
|
|
expect(mockRefresh).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it("displays error on failed login", async () => {
|
|
mockSignInWithPassword.mockResolvedValue({
|
|
error: { message: "Ungültige Anmeldedaten" },
|
|
});
|
|
render(<LoginPage />);
|
|
|
|
fireEvent.change(screen.getByLabelText("E-Mail"), {
|
|
target: { value: "bad@email.de" },
|
|
});
|
|
fireEvent.change(screen.getByLabelText("Passwort"), {
|
|
target: { value: "wrong" },
|
|
});
|
|
fireEvent.click(screen.getByText("Anmelden"));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Ungültige Anmeldedaten")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("shows magic link sent confirmation", async () => {
|
|
mockSignInWithOtp.mockResolvedValue({ error: null });
|
|
render(<LoginPage />);
|
|
|
|
// Switch to magic link mode
|
|
fireEvent.click(screen.getByText("Magic Link"));
|
|
|
|
fireEvent.change(screen.getByLabelText("E-Mail"), {
|
|
target: { value: "test@kanzlei.de" },
|
|
});
|
|
fireEvent.click(screen.getByText("Link senden"));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Link gesendet")).toBeInTheDocument();
|
|
expect(screen.getByText("Zurueck zum Login")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("has link to registration page", () => {
|
|
render(<LoginPage />);
|
|
|
|
const registerLink = screen.getByText("Registrieren");
|
|
expect(registerLink).toBeInTheDocument();
|
|
expect(registerLink.closest("a")).toHaveAttribute("href", "/register");
|
|
});
|
|
});
|