diff --git a/canvas/src/components/__tests__/AuthGate.test.tsx b/canvas/src/components/__tests__/AuthGate.test.tsx index 656a7701..633edf83 100644 --- a/canvas/src/components/__tests__/AuthGate.test.tsx +++ b/canvas/src/components/__tests__/AuthGate.test.tsx @@ -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; + await act(async () => { + result = render( + +
Protected content
+
+ ); + }); + + // 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; + await act(async () => { + result = render( + +
Protected content
+
+ ); + }); + + 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( diff --git a/canvas/src/lib/__tests__/auth.test.ts b/canvas/src/lib/__tests__/auth.test.ts index 8188ddf2..ee74a521 100644 --- a/canvas/src/lib/__tests__/auth.test.ts +++ b/canvas/src/lib/__tests__/auth.test.ts @@ -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); + }); });