115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
/**
|
|
* Website SSR Smoke Tests
|
|
*
|
|
* Run with: npx vitest run tests/smoke/website-ssr.test.ts --config vitest.smoke.config.ts
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll } from 'vitest';
|
|
import { getWebsiteRouteContracts, RouteContract } from '../shared/website/RouteContractSpec';
|
|
import { WebsiteServerHarness } from '../integration/harness/WebsiteServerHarness';
|
|
|
|
const WEBSITE_BASE_URL = process.env.WEBSITE_BASE_URL || 'http://localhost:3000';
|
|
|
|
describe('Website SSR Contract Suite', () => {
|
|
const contracts = getWebsiteRouteContracts();
|
|
let harness: WebsiteServerHarness | null = null;
|
|
let errorCount500 = 0;
|
|
|
|
beforeAll(async () => {
|
|
// Only start harness if WEBSITE_BASE_URL is localhost and not already reachable
|
|
if (WEBSITE_BASE_URL.includes('localhost')) {
|
|
try {
|
|
await fetch(WEBSITE_BASE_URL, { method: 'HEAD' });
|
|
console.log(`Server already running at ${WEBSITE_BASE_URL}`);
|
|
} catch (e) {
|
|
console.log(`Starting website server harness on ${WEBSITE_BASE_URL}...`);
|
|
harness = new WebsiteServerHarness({
|
|
port: parseInt(new URL(WEBSITE_BASE_URL).port) || 3000,
|
|
});
|
|
await harness.start();
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (harness) {
|
|
await harness.stop();
|
|
}
|
|
|
|
// Fail suite on bursts of 500s (e.g. > 3)
|
|
if (errorCount500 > 3) {
|
|
throw new Error(`Suite failed due to high error rate: ${errorCount500} routes returned 500`);
|
|
}
|
|
|
|
// Fail on uncaught exceptions in logs
|
|
if (harness?.hasErrorPatterns()) {
|
|
console.error('Server logs contained error patterns:\n' + harness.getLogTail(50));
|
|
throw new Error('Suite failed due to error patterns in server logs');
|
|
}
|
|
});
|
|
|
|
test.each(contracts)('Contract: $path', async (contract: RouteContract) => {
|
|
const url = `${WEBSITE_BASE_URL}${contract.path}`;
|
|
|
|
let response: Response;
|
|
try {
|
|
response = await fetch(url, { redirect: 'manual' });
|
|
} catch (e) {
|
|
const logTail = harness ? `\nServer Log Tail:\n${harness.getLogTail(20)}` : '';
|
|
throw new Error(`Failed to fetch ${url}: ${e.message}${logTail}`);
|
|
}
|
|
|
|
const status = response.status;
|
|
const text = await response.text();
|
|
const location = response.headers.get('location');
|
|
|
|
if (status === 500 && contract.expectedStatus !== 'errorRoute') {
|
|
errorCount500++;
|
|
}
|
|
|
|
const failureContext = `
|
|
Route: ${contract.path}
|
|
Status: ${status}
|
|
Location: ${location || 'N/A'}
|
|
HTML (clipped): ${text.slice(0, 500)}...
|
|
${harness ? `\nServer Log Tail:\n${harness.getLogTail(10)}` : ''}
|
|
`.trim();
|
|
|
|
// 1. Status class matches expectedStatus
|
|
if (contract.expectedStatus === 'ok') {
|
|
expect(status, failureContext).toBe(200);
|
|
} else if (contract.expectedStatus === 'redirect') {
|
|
expect(status, failureContext).toBeGreaterThanOrEqual(300);
|
|
expect(status, failureContext).toBeLessThan(400);
|
|
} else if (contract.expectedStatus === 'notFoundAllowed') {
|
|
expect([200, 404], failureContext).toContain(status);
|
|
} else if (contract.expectedStatus === 'errorRoute') {
|
|
expect([200, 404, 500], failureContext).toContain(status);
|
|
}
|
|
|
|
// 2. Redirect location semantics
|
|
if (contract.expectedStatus === 'redirect' && contract.expectedRedirectTo) {
|
|
expect(location, failureContext).toContain(contract.expectedRedirectTo);
|
|
if (contract.accessLevel !== 'public' && contract.expectedRedirectTo.includes('/auth/login')) {
|
|
expect(location, failureContext).toContain('returnTo=');
|
|
}
|
|
}
|
|
|
|
// 3. HTML sanity checks
|
|
if (status === 200 || (status === 404 && contract.expectedStatus === 'notFoundAllowed')) {
|
|
if (contract.ssrMustContain) {
|
|
for (const pattern of contract.ssrMustContain) {
|
|
expect(text, failureContext).toMatch(pattern);
|
|
}
|
|
}
|
|
if (contract.ssrMustNotContain) {
|
|
for (const pattern of contract.ssrMustNotContain) {
|
|
expect(text, failureContext).not.toMatch(pattern);
|
|
}
|
|
}
|
|
if (contract.minTextLength) {
|
|
expect(text.length, failureContext).toBeGreaterThanOrEqual(contract.minTextLength);
|
|
}
|
|
}
|
|
});
|
|
});
|