Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7526cb1d67 |
@@ -53,6 +53,7 @@ export const { signIn, signOut, useSession, changePassword } = authClient;
|
|||||||
| TC-WEB-5.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
|
| TC-WEB-5.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
|
||||||
| TC-WEB-5.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
|
| TC-WEB-5.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
|
||||||
| TC-WEB-5.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
|
| TC-WEB-5.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
|
||||||
|
| TC-WEB-5.1.5 | Unauthenticated `/login` renders the form (GRO-2011) | In a private/incognito window with no session cookie, navigate to UAT `/login` | React root mounts; the GroomBook sign-in card with the OIDC button is visible. Network tab shows `/api/auth/get-session` 200, `/api/setup/status` 200, and the login form is rendered (NOT a blank white viewport). |
|
||||||
|
|
||||||
### 5.2 Authentication — VITE_API_URL Set
|
### 5.2 Authentication — VITE_API_URL Set
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -327,11 +327,16 @@ export function App() {
|
|||||||
.catch(() => setAuthDisabled(false));
|
.catch(() => setAuthDisabled(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// After session is confirmed, check if setup is needed
|
// After session is confirmed, check if setup is needed.
|
||||||
|
// Always run the setup/status fetch as soon as the auth state is known — even for
|
||||||
|
// unauthenticated users, so the `needsSetup` value is in place if they sign in
|
||||||
|
// mid-session. The unauth branch in the render below is handled before
|
||||||
|
// `needsSetup` is consulted, so this is safe and avoids a stuck-`null` state.
|
||||||
|
// See GRO-2011.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authDisabled === null || sessionLoading) return;
|
if (authDisabled === null || sessionLoading) return;
|
||||||
// Skip if no authenticated session (will redirect to login or dev selector)
|
// In dev mode, only fetch when a dev user has been selected — otherwise the
|
||||||
if (!authDisabled && !session) return;
|
// user is mid-redirect to the dev login selector and we don't need setup state.
|
||||||
if (authDisabled && !getDevUser()) return;
|
if (authDisabled && !getDevUser()) return;
|
||||||
|
|
||||||
fetch("/api/setup/status")
|
fetch("/api/setup/status")
|
||||||
|
|||||||
@@ -121,6 +121,65 @@ describe("App navigation", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("GRO-2011 — setup/status fetch for unauthenticated users", () => {
|
||||||
|
it("calls /api/setup/status for unauthenticated users so needsSetup is never stuck null", async () => {
|
||||||
|
const setupStatusCalls: string[] = [];
|
||||||
|
|
||||||
|
global.fetch = vi.fn((url: string) => {
|
||||||
|
if (url === "/api/dev/config") {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ authDisabled: false }),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
if (url === "/api/auth/get-session") {
|
||||||
|
// Better Auth returns 200 with null session for unauthenticated users.
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: async () => null,
|
||||||
|
} as unknown as Response);
|
||||||
|
}
|
||||||
|
if (url === "/api/setup/status") {
|
||||||
|
setupStatusCalls.push(url);
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ needsSetup: false }),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
if (url === "/api/branding") {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
businessName: "GroomBook",
|
||||||
|
primaryColor: "#4f8a6f",
|
||||||
|
accentColor: "#8b7355",
|
||||||
|
logoBase64: null,
|
||||||
|
logoMimeType: null,
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: async () => [] } as Response);
|
||||||
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={["/login"]}>
|
||||||
|
<App />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
// The login page should be rendered for the unauthenticated user.
|
||||||
|
await screen.findByText("Sign in to continue");
|
||||||
|
|
||||||
|
// Crucially, /api/setup/status must be called even when the user is unauthenticated —
|
||||||
|
// otherwise `needsSetup` stays null and a later code path can short-circuit to a
|
||||||
|
// blank page (GRO-2011).
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(setupStatusCalls.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
expect(setupStatusCalls[0]).toBe("/api/setup/status");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Dev login selector", () => {
|
describe("Dev login selector", () => {
|
||||||
it("redirects to /login when auth is disabled and no user selected", async () => {
|
it("redirects to /login when auth is disabled and no user selected", async () => {
|
||||||
global.fetch = vi.fn((url: string) => {
|
global.fetch = vi.fn((url: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user