website refactor
This commit is contained in:
@@ -5,10 +5,26 @@ 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' | 'notFoundAllowed' | 'errorRoute';
|
||||
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<string | RegExp> | undefined;
|
||||
ssrMustNotContain?: Array<string | RegExp> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* RouteContract defines the "Single Source of Truth" for how a website route
|
||||
@@ -19,16 +35,18 @@ export interface RouteContract {
|
||||
path: string;
|
||||
/** The required access level for this route */
|
||||
accessLevel: RouteAccess;
|
||||
/** What we expect when hitting this route unauthenticated */
|
||||
/** Baseline expectations (usually for the "intended" role or unauth) */
|
||||
expectedStatus: ExpectedStatus;
|
||||
/** If expectedStatus is 'redirect', where should it go? (pathname only) */
|
||||
expectedRedirectTo?: string;
|
||||
expectedRedirectTo?: string | undefined;
|
||||
/** Strings or Regex that MUST be present in the SSR HTML */
|
||||
ssrMustContain?: Array<string | RegExp>;
|
||||
ssrMustContain?: Array<string | RegExp> | undefined;
|
||||
/** Strings or Regex that MUST NOT be present in the SSR HTML (e.g. error markers) */
|
||||
ssrMustNotContain?: Array<string | RegExp>;
|
||||
ssrMustNotContain?: Array<string | RegExp> | undefined;
|
||||
/** Minimum expected length of the HTML response body */
|
||||
minTextLength?: number;
|
||||
minTextLength?: number | undefined;
|
||||
/** Per-role scenario expectations */
|
||||
scenarios: Partial<Record<ScenarioRole, ScenarioExpectation>>;
|
||||
}
|
||||
|
||||
const DEFAULT_SSR_MUST_CONTAIN = ['<!DOCTYPE html>', '<body'];
|
||||
@@ -81,8 +99,35 @@ export function getWebsiteRouteContracts(): RouteContract[] {
|
||||
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) {
|
||||
|
||||
37
tests/shared/website/RouteScenarioMatrix.ts
Normal file
37
tests/shared/website/RouteScenarioMatrix.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { getWebsiteRouteContracts, ScenarioRole } from './RouteContractSpec';
|
||||
import { WebsiteRouteManager, RouteAccess } from './WebsiteRouteManager';
|
||||
import { routeMatchers } from '../../../apps/website/lib/routing/RouteConfig';
|
||||
|
||||
/**
|
||||
* Represents a single entry in the route coverage matrix.
|
||||
* This is a machine-readable artifact used to verify testing gaps.
|
||||
*/
|
||||
export interface RouteScenarioMatrixEntry {
|
||||
/** The resolved path of the route */
|
||||
path: string;
|
||||
/** The access level required for this route */
|
||||
accessLevel: RouteAccess;
|
||||
/** The scenarios that must be tested for this route */
|
||||
requiredScenarios: ScenarioRole[];
|
||||
/** Whether this route has parameter-based edge cases (e.g. 404s for bad IDs) */
|
||||
hasParamEdgeCases: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The RouteScenarioMatrix provides a structured view of all routes and their
|
||||
* required test scenarios. It is derived from the route contracts and inventory.
|
||||
*/
|
||||
export const RouteScenarioMatrix: RouteScenarioMatrixEntry[] = (() => {
|
||||
const contracts = getWebsiteRouteContracts();
|
||||
const manager = new WebsiteRouteManager();
|
||||
const edgeCases = manager.getParamEdgeCases();
|
||||
|
||||
return contracts.map(contract => {
|
||||
return {
|
||||
path: contract.path,
|
||||
accessLevel: contract.accessLevel,
|
||||
requiredScenarios: Object.keys(contract.scenarios) as ScenarioRole[],
|
||||
hasParamEdgeCases: edgeCases.some(ec => routeMatchers.matches(contract.path, ec.pathTemplate)),
|
||||
};
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user