Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions apps/web/src/lib/auth/auth-middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";

const isLocalModeMock = mock(() => true);
const hasAssistantsMock = mock(() => false);
mock.module("@/lib/local-mode", () => ({
isLocalMode: isLocalModeMock,
hasAssistants: hasAssistantsMock,
getLocalGatewayUrl: () => undefined,
}));

import { authMiddleware } from "./auth-middleware";
import { useAuthStore, type AuthUser } from "@/stores/auth-store";
import { routes } from "@/utils/routes";

const initialAuthState = useAuthStore.getState();
const fakeUser = { id: "user-123" } as AuthUser;

async function runMiddleware(pathname: string): Promise<Response> {
const args = {
request: new Request(`http://localhost${pathname}`),
context: { set: () => {}, get: () => null },
} as unknown as Parameters<typeof authMiddleware>[0];
const next = (async () => new Response()) as Parameters<
typeof authMiddleware
>[1];
// The middleware signals an unauthenticated/onboarding redirect by *throwing*
// a Response, so surface that as the resolved value for assertions.
try {
await authMiddleware(args, next);
} catch (thrown) {
if (thrown instanceof Response) return thrown;
throw thrown;
}
throw new Error("expected a redirect to be thrown");
}

const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 0));

beforeEach(() => {
isLocalModeMock.mockImplementation(() => true);
hasAssistantsMock.mockImplementation(() => false);
useAuthStore.setState(initialAuthState, true);
});

afterEach(() => {
useAuthStore.setState(initialAuthState, true);
});

describe("authMiddleware — local-mode onboarding fork", () => {
test("waits for the platform-session probe before choosing hosting vs welcome", async () => {
useAuthStore.setState({
isLoggedIn: true,
isLoading: false,
user: fakeUser,
hasPlatformSession: false,
platformSessionResolved: false,
});

let settled: Response | null = null;
const pending = runMiddleware(routes.home).then((res) => {
settled = res;
});

// Probe still in flight: the middleware must not have decided yet, so a
// returning platform user isn't prematurely sent to the welcome flow.
await tick();
expect(settled).toBeNull();

// Probe settles with a live platform session.
useAuthStore.setState({
hasPlatformSession: true,
platformSessionResolved: true,
});
await pending;

expect(settled).not.toBeNull();
expect(settled!.status).toBe(302);
expect(settled!.headers.get("Location")).toBe(routes.onboarding.hosting);
});

test("routes to welcome once resolved with no platform session", async () => {
useAuthStore.setState({
isLoggedIn: true,
isLoading: false,
user: fakeUser,
hasPlatformSession: false,
platformSessionResolved: true,
});

const res = await runMiddleware(routes.home);
expect(res.status).toBe(302);
expect(res.headers.get("Location")).toBe(routes.onboarding.welcome);
});

test("routes to hosting when a resolved platform session exists", async () => {
useAuthStore.setState({
isLoggedIn: true,
isLoading: false,
user: fakeUser,
hasPlatformSession: true,
platformSessionResolved: true,
});

const res = await runMiddleware(routes.home);
expect(res.status).toBe(302);
expect(res.headers.get("Location")).toBe(routes.onboarding.hosting);
});
});
21 changes: 21 additions & 0 deletions apps/web/src/lib/auth/auth-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export const authMiddleware: MiddlewareFunction = async ({ request, context }, n
if (isLocalMode() && !hasAssistants()) {
const url = new URL(request.url);
if (!url.pathname.includes("/onboarding/") && !url.pathname.includes("/account")) {
// The hosting-vs-welcome fork keys off `hasPlatformSession`, which is set
// by a fire-and-forget probe that may still be in flight here (the local
// gateway auth paths return before the platform session is known). Reading
// it early reports an ambiguous `false` and sends a returning platform
// user to the new-user welcome flow. Wait for the probe to settle first.
await waitForPlatformSessionResolved();
const { hasPlatformSession } = useAuthStore.getState();
throw redirect(hasPlatformSession ? routes.onboarding.hosting : routes.onboarding.welcome);
}
Expand All @@ -64,3 +70,18 @@ function waitForAuthReady(): Promise<void> {
}
});
}

function waitForPlatformSessionResolved(): Promise<void> {
return new Promise((resolve) => {
const unsubscribe = useAuthStore.subscribe((state) => {
if (state.platformSessionResolved) {
unsubscribe();
resolve();
}
});
if (useAuthStore.getState().platformSessionResolved) {
unsubscribe();
resolve();
}
});
}