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();
  });
});