root-signed-out-host-redirect.test.ts4.3 KBView on GitHub /**
* The app host (mail.cedarcopilot.com) is not the front door any more — cedarcopilot.com
* is. A signed-out visitor who lands on the app host's root gets handed to the marketing
* origin instead of a bare sign-in screen. See apps/mail/lib/marketing-host.ts.
*/
import { redirect } from 'react-router';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
redirect: jest.fn((to: string) => ({ to })),
}));
const mockGetSession = jest.fn();
jest.mock('@/modules/auth/utils/auth-proxy', () => ({
authProxy: { api: { getSession: (...args: unknown[]) => mockGetSession(...args) } },
}));
const mockIsMarketingHost = jest.fn(() => false);
const mockIsAppHost = jest.fn(() => false);
const mockMarketingUrl = jest.fn((path: string) => `https://cedarcopilot.com${path}`);
const mockLeaveForOrigin = jest.fn();
jest.mock('@/lib/marketing-host', () => ({
isMarketingHost: () => mockIsMarketingHost(),
isAppHost: () => mockIsAppHost(),
marketingUrl: (path: string) => mockMarketingUrl(path),
leaveForOrigin: (url: string) => mockLeaveForOrigin(url),
}));
const mockIsElectron = jest.fn(() => false);
jest.mock('@/lib/is-electron', () => ({ isElectron: () => mockIsElectron() }));
// The route module renders the landing page; only its loader is under test here.
jest.mock('@/app/(full-width)/home/HomeContent', () => ({ __esModule: true, default: () => null }));
import { clientLoader } from '@/app/page';
// jsdom has no `Request`; the loader only ever reads `.headers` off it.
const request = { headers: new Headers() };
const PENDING = Symbol('pending');
/**
* Runs the loader and reports how it ended: a returned value, a thrown redirect, or
* still pending. The sentinel is a macrotask so every microtask the loader queues (the
* awaited session lookup, and the redirect it throws after it) settles first.
*/
async function runLoader() {
const pending = new Promise((resolve) => setTimeout(() => resolve(PENDING), 0));
try {
const value = await Promise.race([clientLoader({ request } as never), pending]);
return value === PENDING ? { pending: true } : { value };
} catch (thrown) {
return { thrown };
}
}
describe('root route on the app host', () => {
beforeEach(() => {
jest.clearAllMocks();
mockIsMarketingHost.mockReturnValue(false);
mockIsAppHost.mockReturnValue(false);
mockIsElectron.mockReturnValue(false);
mockMarketingUrl.mockImplementation((path: string) => `https://cedarcopilot.com${path}`);
mockGetSession.mockResolvedValue(null);
});
it('sends a signed-out visitor to the marketing origin', async () => {
mockIsAppHost.mockReturnValue(true);
const result = await runLoader();
expect(mockLeaveForOrigin).toHaveBeenCalledWith('https://cedarcopilot.com/');
expect(redirect).not.toHaveBeenCalled();
// The loader never settles, so the route stays on the hydrate fallback rather than
// flashing the landing page while the browser leaves.
expect(result).toEqual({ pending: true });
});
it('still sends a signed-in visitor to /home', async () => {
mockIsAppHost.mockReturnValue(true);
mockGetSession.mockResolvedValue({ user: { id: 'user_1' } });
const result = await runLoader();
expect(result).toEqual({ thrown: { to: '/home' } });
expect(mockLeaveForOrigin).not.toHaveBeenCalled();
});
it('keeps the desktop app on /login — it has no marketing site to fall back to', async () => {
mockIsAppHost.mockReturnValue(true);
mockIsElectron.mockReturnValue(true);
const result = await runLoader();
expect(result).toEqual({ thrown: { to: '/login' } });
expect(mockLeaveForOrigin).not.toHaveBeenCalled();
});
it('falls back to /login when no marketing origin is configured', async () => {
mockIsAppHost.mockReturnValue(true);
mockMarketingUrl.mockReturnValue(null as unknown as string);
const result = await runLoader();
expect(result).toEqual({ thrown: { to: '/login' } });
expect(mockLeaveForOrigin).not.toHaveBeenCalled();
});
it('leaves the marketing origin showing the landing page', async () => {
mockIsMarketingHost.mockReturnValue(true);
const result = await runLoader();
expect(result).toEqual({ value: null });
expect(mockGetSession).not.toHaveBeenCalled();
expect(mockLeaveForOrigin).not.toHaveBeenCalled();
});
});