Task 0.1: Create database parent directory before connecting - main.py _lifespan now calls Path(database_path).parent.mkdir(parents=True, exist_ok=True) before aiosqlite.connect() so the app starts cleanly on a fresh Docker volume with a nested database path. Task 0.2: SetupRedirectMiddleware redirects when db is None - Guard now reads: if db is None or not is_setup_complete(db) A missing database (startup still in progress) is treated as setup not complete instead of silently allowing all API routes through. Task 0.3: SetupGuard redirects to /setup on API failure - .catch() handler now sets status to 'pending' instead of 'done'. A crashed backend cannot serve protected routes; conservative fallback is to redirect to /setup. Task 0.4: SetupPage shows spinner while checking setup status - Added 'checking' boolean state; full-screen Spinner is rendered until getSetupStatus() resolves, preventing form flash before redirect. - Added console.warn in catch block; cleanup return added to useEffect. Also: remove unused type: ignore[call-arg] from config.py. Tests: 18 backend tests pass; 117 frontend tests pass.
89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import { MemoryRouter, Routes, Route } from "react-router-dom";
|
|
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
|
|
import { SetupPage } from "../SetupPage";
|
|
|
|
// Mock the setup API so tests never hit a real network.
|
|
vi.mock("../../api/setup", () => ({
|
|
getSetupStatus: vi.fn(),
|
|
submitSetup: vi.fn(),
|
|
}));
|
|
|
|
// Mock the crypto utility — we only need it to resolve without testing SHA256.
|
|
vi.mock("../../utils/crypto", () => ({
|
|
sha256Hex: vi.fn().mockResolvedValue("hashed-password"),
|
|
}));
|
|
|
|
import { getSetupStatus } from "../../api/setup";
|
|
|
|
const mockedGetSetupStatus = vi.mocked(getSetupStatus);
|
|
|
|
function renderPage() {
|
|
return render(
|
|
<FluentProvider theme={webLightTheme}>
|
|
<MemoryRouter initialEntries={["/setup"]}>
|
|
<Routes>
|
|
<Route path="/setup" element={<SetupPage />} />
|
|
<Route
|
|
path="/login"
|
|
element={<div data-testid="login-page">Login</div>}
|
|
/>
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</FluentProvider>,
|
|
);
|
|
}
|
|
|
|
describe("SetupPage", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("shows a full-screen spinner while the setup status check is in flight", () => {
|
|
// getSetupStatus never resolves — spinner should be visible immediately.
|
|
mockedGetSetupStatus.mockReturnValue(new Promise(() => {}));
|
|
renderPage();
|
|
expect(screen.getByRole("progressbar")).toBeInTheDocument();
|
|
// Form should NOT be visible yet.
|
|
expect(
|
|
screen.queryByRole("heading", { name: /bangui setup/i }),
|
|
).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the setup form once the status check resolves (not complete)", async () => {
|
|
// Task 0.4: form must not flash before the check resolves.
|
|
mockedGetSetupStatus.mockResolvedValue({ completed: false });
|
|
renderPage();
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole("heading", { name: /bangui setup/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
// Spinner should be gone.
|
|
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("redirects to /login when setup is already complete", async () => {
|
|
mockedGetSetupStatus.mockResolvedValue({ completed: true });
|
|
renderPage();
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId("login-page")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("renders the form and logs a warning when the status check fails", async () => {
|
|
// Task 0.4: catch block must log a warning and keep the form visible.
|
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
mockedGetSetupStatus.mockRejectedValue(new Error("Connection refused"));
|
|
renderPage();
|
|
await waitFor(() => {
|
|
expect(
|
|
screen.getByRole("heading", { name: /bangui setup/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
expect(warnSpy).toHaveBeenCalledOnce();
|
|
warnSpy.mockRestore();
|
|
});
|
|
});
|