diff --git a/webui/src/App.tsx b/webui/src/App.tsx index ee035c2f..1eb19792 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -411,7 +411,7 @@ export default function App() { if (cancelled) return; const msg = (e as Error).message; if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) { - setState({ status: "auth", failed: true }); + setState({ status: "auth", failed: !!secret }); } else { setState({ status: "error", message: msg }); } @@ -433,7 +433,7 @@ export default function App() { } catch (e) { const msg = (e as Error).message; if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) { - setState({ status: "auth", failed: true }); + setState({ status: "auth", failed: !!bootstrapSecretRef.current }); } } }, tokenRefreshDelayMs(state.tokenExpiresAt)); diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx index 687660ed..0a3fa8ba 100644 --- a/webui/src/tests/app-layout.test.tsx +++ b/webui/src/tests/app-layout.test.tsx @@ -256,6 +256,34 @@ describe("App layout", () => { vi.useRealTimers(); }); + it("shows the auth form without an invalid-password error on first load", async () => { + vi.mocked(fetchBootstrap).mockRejectedValueOnce( + new Error("bootstrap failed: HTTP 401"), + ); + + render(); + + expect(await screen.findByText("Authentication required")).toBeInTheDocument(); + expect(screen.queryByText("Invalid password. Try again.")).not.toBeInTheDocument(); + expect(connectSpy).not.toHaveBeenCalled(); + }); + + it("shows an invalid-password error after a submitted password is rejected", async () => { + vi.mocked(fetchBootstrap).mockRejectedValue( + new Error("bootstrap failed: HTTP 401"), + ); + + render(); + + const password = await screen.findByPlaceholderText("Password"); + fireEvent.change(password, { target: { value: "wrong-password" } }); + fireEvent.click(screen.getByRole("button", { name: "Connect" })); + + expect(await screen.findByText("Invalid password. Try again.")).toBeInTheDocument(); + expect(fetchBootstrap).toHaveBeenLastCalledWith("", "wrong-password"); + expect(connectSpy).not.toHaveBeenCalled(); + }); + it("keeps sidebar layout out of the main thread width contract", async () => { const { container } = render();