mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 12:15:42 +00:00
36b42c1591
Playwright storage states persist cookies and localStorage only, while the primary-API-token flow authenticates through sessionStorage. Storage states captured after token auth were silently unauthenticated, so whole spec files landed on the login screen whenever the token path happened to win. Storage states now always come from the cookie-backed password login, cached once per run and probed before reuse. Explicit bearer/X-API-Token requests now go through a cookie-less request context: the backend intentionally CSRF-rejects mutations that carry a session cookie without a CSRF token even when the Authorization header is valid, so token-scope coverage could never pass from a logged-in page. Session-semantics specs use the new ensureSessionAuthenticated instead of racing the token path. First-run resets now refuse the implicitly resolved hot-dev runtime (the pid fallback with no explicit base URL and no harness-written runtime state); an ad-hoc spec run wiped the live dev backend's auth twice through that path. Resets can also recover an instance stuck unauthenticated mid-reset by re-reading the rotated bootstrap token through docker exec, and login() retries through the backend's 10-per-minute login limiter instead of failing on burst runs.
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
export type RuntimeState = {
|
|
baseURL?: string;
|
|
primaryAPIToken?: string;
|
|
};
|
|
|
|
const trim = (value: unknown): string => String(value ?? '').trim();
|
|
const firstNonEmpty = (...values: readonly unknown[]): string =>
|
|
values.map(trim).find((value) => value !== '') || '';
|
|
const browserHostForBind = (host: unknown): string => {
|
|
const normalized = trim(host).toLowerCase().replace(/^\[/, '').replace(/\]$/, '');
|
|
if (normalized === '' || normalized === '0.0.0.0' || normalized === '::') {
|
|
return '127.0.0.1';
|
|
}
|
|
return trim(host);
|
|
};
|
|
|
|
export const repoRootFromEnv = (env: NodeJS.ProcessEnv = process.env): string =>
|
|
trim(env.PULSE_E2E_REPO_ROOT) || path.resolve(__dirname, '..', '..', '..');
|
|
|
|
const managedHotDevPidPath = (env: NodeJS.ProcessEnv = process.env): string =>
|
|
path.join(repoRootFromEnv(env), 'tmp', 'hot-dev.bg.pid');
|
|
|
|
export const runtimeStatePath = (env: NodeJS.ProcessEnv = process.env): string => {
|
|
const configuredPath = trim(env.PULSE_E2E_RUNTIME_STATE_PATH);
|
|
if (configuredPath === '') {
|
|
return path.resolve(repoRootFromEnv(env), 'tmp', 'e2e-runtime-state.json');
|
|
}
|
|
return path.isAbsolute(configuredPath)
|
|
? configuredPath
|
|
: path.resolve(repoRootFromEnv(env), configuredPath);
|
|
};
|
|
|
|
export const readRuntimeState = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): RuntimeState | null => {
|
|
try {
|
|
const raw = fs.readFileSync(runtimeStatePath(env), 'utf8');
|
|
return JSON.parse(raw) as RuntimeState;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const loadRuntimeBaseURL = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): string | null => {
|
|
const parsed = readRuntimeState(env);
|
|
return typeof parsed?.baseURL === 'string' && parsed.baseURL.trim() !== ''
|
|
? parsed.baseURL.trim()
|
|
: null;
|
|
};
|
|
|
|
export const managedDevBrowserBaseURL = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): string | null => {
|
|
try {
|
|
const pid = Number.parseInt(fs.readFileSync(managedHotDevPidPath(env), 'utf8').trim(), 10);
|
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
return null;
|
|
}
|
|
process.kill(pid, 0);
|
|
const host = browserHostForBind(env.FRONTEND_DEV_HOST || '127.0.0.1');
|
|
const port = trim(env.FRONTEND_DEV_PORT) || '5173';
|
|
return `http://${host}:${port}`;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const preferredBrowserBaseURL = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
overrides: readonly unknown[] = [],
|
|
): string =>
|
|
firstNonEmpty(
|
|
...overrides,
|
|
env.PLAYWRIGHT_BASE_URL,
|
|
env.PULSE_BASE_URL,
|
|
loadRuntimeBaseURL(env),
|
|
managedDevBrowserBaseURL(env),
|
|
'http://localhost:7655',
|
|
);
|
|
|
|
// True when the base URL would resolve through the hot-dev pid fallback with
|
|
// no explicit env override and no harness-written runtime state — i.e. the
|
|
// suite is about to target a developer's live Pulse rather than an e2e stack.
|
|
export const browserBaseURLIsImplicitDevRuntime = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): boolean =>
|
|
trim(env.PLAYWRIGHT_BASE_URL) === '' &&
|
|
trim(env.PULSE_BASE_URL) === '' &&
|
|
loadRuntimeBaseURL(env) === null &&
|
|
managedDevBrowserBaseURL(env) !== null;
|
|
|
|
export const preferredPlaywrightRouteBaseURL = (
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
overrides: readonly unknown[] = [],
|
|
): string => preferredBrowserBaseURL(env, overrides).replace(/\/+$/, '');
|