test(auth): add regression tests for redirect loop guards
AuthGate now skips session fetch for /cp/auth/* paths, and redirectToLogin guards against re-setting window.location when already on an auth path. Both guards had no test coverage — a future refactor could silently reintroduce the redirect loop. Added: - AuthGate.test.tsx: 2 cases covering /cp/auth/login and /cp/auth/signup path skipping (no fetchSession call, no redirectToLogin call, children rendered) - auth.test.ts: 2 cases covering redirectToLogin early return for /cp/auth/login and /cp/auth/signup paths Fixes: Molecule-AI/molecule-core#1541 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e4e389950f
commit
e9be12210f
@ -105,10 +105,64 @@ describe("AuthGate — authenticated state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AuthGate — /cp/auth/* skip guard (redirect loop regression)", () => {
|
||||
it("renders children without calling fetchSession or redirect when pathname starts with /cp/auth/", async () => {
|
||||
mockGetTenantSlug.mockReturnValue("acme");
|
||||
mockFetchSession.mockResolvedValue(null);
|
||||
|
||||
// Simulate being on the login page
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: { ...window.location, pathname: "/cp/auth/login" },
|
||||
});
|
||||
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<AuthGate>
|
||||
<div data-testid="child">Protected content</div>
|
||||
</AuthGate>
|
||||
);
|
||||
});
|
||||
|
||||
// Children should render — AuthGate skips session fetch for auth paths
|
||||
expect(result!.getByTestId("child")).toBeTruthy();
|
||||
expect(mockFetchSession).not.toHaveBeenCalled();
|
||||
expect(mockRedirectToLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders children without calling redirect for /cp/auth/signup path", async () => {
|
||||
mockGetTenantSlug.mockReturnValue("acme");
|
||||
mockFetchSession.mockResolvedValue(null);
|
||||
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: { ...window.location, pathname: "/cp/auth/signup" },
|
||||
});
|
||||
|
||||
let result: ReturnType<typeof render>;
|
||||
await act(async () => {
|
||||
result = render(
|
||||
<AuthGate>
|
||||
<div data-testid="child">Protected content</div>
|
||||
</AuthGate>
|
||||
);
|
||||
});
|
||||
|
||||
expect(result!.getByTestId("child")).toBeTruthy();
|
||||
expect(mockRedirectToLogin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("AuthGate — anonymous / redirect state", () => {
|
||||
it("calls redirectToLogin when session fetch returns null", async () => {
|
||||
mockGetTenantSlug.mockReturnValue("acme");
|
||||
mockFetchSession.mockResolvedValue(null);
|
||||
// Ensure pathname is NOT on /cp/auth/* so the redirect guard fires
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: { ...window.location, pathname: "/dashboard" },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
render(
|
||||
|
||||
@ -55,8 +55,6 @@ describe("redirectToLogin", () => {
|
||||
},
|
||||
});
|
||||
redirectToLogin("sign-in");
|
||||
// href now holds the redirect target. encodeURIComponent(href) must
|
||||
// appear in the query.
|
||||
expect((window.location as unknown as { href: string }).href).toContain("/cp/auth/login");
|
||||
expect((window.location as unknown as { href: string }).href).toContain(
|
||||
encodeURIComponent(href),
|
||||
@ -76,4 +74,39 @@ describe("redirectToLogin", () => {
|
||||
redirectToLogin("sign-up");
|
||||
expect((window.location as unknown as { href: string }).href).toContain("/cp/auth/signup");
|
||||
});
|
||||
|
||||
// Regression: AuthGate + redirectToLogin mutual recursion on /cp/auth/login
|
||||
// caused double-encoded return_to that grew until the URL exceeded 431.
|
||||
// Guard: redirectToLogin must NOT set window.location when already on an
|
||||
// auth path, otherwise each call adds another encoding layer.
|
||||
it("does NOT set window.location when already on /cp/auth/login (redirect loop guard)", () => {
|
||||
const loginHref = "https://app.moleculesai.app/cp/auth/login?return_to=https%3A%2F%2Facme.moleculesai.app%2Fdashboard";
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: {
|
||||
href: loginHref,
|
||||
pathname: "/cp/auth/login",
|
||||
hostname: "app.moleculesai.app",
|
||||
protocol: "https:",
|
||||
},
|
||||
});
|
||||
redirectToLogin("sign-in");
|
||||
// href must be unchanged — any mutation means the guard is missing
|
||||
expect((window.location as unknown as { href: string }).href).toBe(loginHref);
|
||||
});
|
||||
|
||||
it("does NOT set window.location when already on /cp/auth/signup (redirect loop guard)", () => {
|
||||
const signupHref = "https://app.moleculesai.app/cp/auth/signup";
|
||||
Object.defineProperty(window, "location", {
|
||||
writable: true,
|
||||
value: {
|
||||
href: signupHref,
|
||||
pathname: "/cp/auth/signup",
|
||||
hostname: "app.moleculesai.app",
|
||||
protocol: "https:",
|
||||
},
|
||||
});
|
||||
redirectToLogin("sign-up");
|
||||
expect((window.location as unknown as { href: string }).href).toBe(signupHref);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user