import { WebsiteRouteManager, RouteAccess } from './WebsiteRouteManager'; import { routes } from '../../../apps/website/lib/routing/RouteConfig'; /** * Expected HTTP status or behavior for a route. * - 'ok': 200 OK * - 'redirect': 3xx redirect (usually to login) * - 'forbidden': 403 Forbidden (or redirect to dashboard for wrong role) * - 'notFoundAllowed': 404 is an acceptable/expected outcome (e.g. for edge cases) * - 'errorRoute': The dedicated error pages themselves */ export type ExpectedStatus = 'ok' | 'redirect' | 'forbidden' | 'notFoundAllowed' | 'errorRoute'; /** * Roles that can access routes, used for scenario testing. */ export type ScenarioRole = 'unauth' | 'auth' | 'admin' | 'sponsor' | 'wrong-role'; /** * Expectations for a specific scenario/role. */ export interface ScenarioExpectation { expectedStatus: ExpectedStatus; expectedRedirectTo?: string | undefined; ssrMustContain?: Array | undefined; ssrMustNotContain?: Array | undefined; } /** * RouteContract defines the "Single Source of Truth" for how a website route * should behave during SSR and E2E testing. */ export interface RouteContract { /** The fully resolved path (e.g. /leagues/123 instead of /leagues/[id]) */ path: string; /** The required access level for this route */ accessLevel: RouteAccess; /** Baseline expectations (usually for the "intended" role or unauth) */ expectedStatus: ExpectedStatus; /** If expectedStatus is 'redirect', where should it go? (pathname only) */ expectedRedirectTo?: string | undefined; /** Strings or Regex that MUST be present in the SSR HTML */ ssrMustContain?: Array | undefined; /** Strings or Regex that MUST NOT be present in the SSR HTML (e.g. error markers) */ ssrMustNotContain?: Array | undefined; /** Minimum expected length of the HTML response body */ minTextLength?: number | undefined; /** Per-role scenario expectations */ scenarios: Partial>; } const DEFAULT_SSR_MUST_CONTAIN = ['', '> = { [routes.error.notFound]: { expectedStatus: 'notFoundAllowed', }, [routes.error.serverError]: { expectedStatus: 'errorRoute', }, }; return inventory.map((def) => { const path = manager.resolvePathTemplate(def.pathTemplate, def.params); // Default augmentation based on access level let expectedStatus: ExpectedStatus = 'ok'; let expectedRedirectTo: string | undefined = undefined; if (def.access !== 'public') { expectedStatus = 'redirect'; // Most protected routes redirect to login when unauthenticated expectedRedirectTo = routes.auth.login; } // If the inventory explicitly allows 404 (e.g. for non-existent IDs in edge cases) if (def.allowNotFound) { expectedStatus = 'notFoundAllowed'; } const contract: RouteContract = { path, accessLevel: def.access, expectedStatus, expectedRedirectTo, ssrMustContain: [...DEFAULT_SSR_MUST_CONTAIN], ssrMustNotContain: [...DEFAULT_SSR_MUST_NOT_CONTAIN], minTextLength: 1000, // Reasonable minimum for a Next.js page scenarios: {}, }; // Populate scenarios based on access level contract.scenarios.unauth = { expectedStatus: contract.expectedStatus, expectedRedirectTo: contract.expectedRedirectTo, }; if (def.access === 'public') { contract.scenarios.auth = { expectedStatus: 'ok' }; contract.scenarios.admin = { expectedStatus: 'ok' }; contract.scenarios.sponsor = { expectedStatus: 'ok' }; } else if (def.access === 'auth') { contract.scenarios.auth = { expectedStatus: 'ok' }; contract.scenarios.admin = { expectedStatus: 'ok' }; contract.scenarios.sponsor = { expectedStatus: 'ok' }; } else if (def.access === 'admin') { contract.scenarios.auth = { expectedStatus: 'redirect', expectedRedirectTo: routes.protected.dashboard }; contract.scenarios.admin = { expectedStatus: 'ok' }; contract.scenarios.sponsor = { expectedStatus: 'redirect', expectedRedirectTo: routes.sponsor.dashboard }; contract.scenarios['wrong-role'] = { expectedStatus: 'redirect', expectedRedirectTo: routes.protected.dashboard }; } else if (def.access === 'sponsor') { contract.scenarios.auth = { expectedStatus: 'redirect', expectedRedirectTo: routes.protected.dashboard }; contract.scenarios.admin = { expectedStatus: 'redirect', expectedRedirectTo: routes.admin.root }; contract.scenarios.sponsor = { expectedStatus: 'ok' }; contract.scenarios['wrong-role'] = { expectedStatus: 'redirect', expectedRedirectTo: routes.protected.dashboard }; } // Apply per-route overrides (matching by template or resolved path) const override = overrides[def.pathTemplate] || overrides[path]; if (override) { Object.assign(contract, override); } return contract; }); }