Files
pulse/tests/integration/scripts/fixture-auth.test.mjs
T
rcourtman 12d050a5be Wait for resolved workspace routes before browser journeys
A mounted cookie shell can still be waiting for its default workspace
redirect. Admitting that entry races the next deep link in WebKit. Wait
for the destination and retain controlled missing-admission and resource
snapshot coverage, including rejection of the old helper predicate.

Use the shared content-owner geometry audit for infrastructure onboarding
and assert SPA route changes without waiting for the document load event.
2026-10-25 01:41:31 +00:00

480 lines
22 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import { stripTypeScriptTypes } from 'node:module';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const source = fs.readFileSync(
process.env.PULSE_FIXTURE_AUTH_SOURCE || path.join(root, 'tests/helpers.ts'),
'utf8',
);
const fragment = (start, end) => {
const from = source.indexOf(start);
const to = source.indexOf(end, from);
assert.ok(from >= 0 && to > from, `missing helper boundary: ${start}`);
return source.slice(from, to).replace(/^export /gm, '');
};
// Execute the real public helpers and their cookie-reuse implementation, not
// a second implementation. Strip types with the governed Node 24 runtime;
// stub only the browser/backend boundaries. No listener or credentials needed.
const helpers = stripTypeScriptTypes([
source.match(/const AUTHENTICATED_URL =[\s\S]*?;\n/)[0],
fragment('async function waitForAuthenticatedAppShell(', 'export async function waitForPulseReady('),
fragment('export async function ensureAuthenticated(', 'async function storageStateHasLiveSession('),
fragment('export async function apiRequest(', 'export async function isMultiTenantEnabled('),
'({ ensureAuthenticated, ensureSessionAuthenticated, apiRequest });',
].join('\n'));
const appRuntimeSource = fs.readFileSync(
path.resolve(root, '../../frontend-modern/src/useAppRuntimeState.ts'), 'utf8',
);
const preAuthPolicy = appRuntimeSource.match(/const isPreAuthLoginBootstrapPath =[\s\S]*?;\n/);
assert.ok(preAuthPolicy, 'missing app pre-auth entrypoint policy');
const isPreAuthLoginBootstrapPath = runInNewContext(stripTypeScriptTypes(
`${preAuthPolicy[0]}\nisPreAuthLoginBootstrapPath;`,
));
const cookie = (name, value) => ({
name, value, domain: '127.0.0.1', path: '/', expires: -1,
httpOnly: name === 'pulse_session', secure: false, sameSite: 'Lax',
});
const response = status => ({ status: () => status, ok: () => status === 200 });
function fixture(t, options = {}) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-fixture-auth-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const statePath = path.join(directory, 'session.json');
const sessions = new Set(['saved-live']);
let loginCalls = 0;
let probeCalls = 0;
let disposed = 0;
let primaryRevoked = false;
let registeredWithToken = false;
const bootstrapChecks = [];
const requests = [];
const primary = options.primary || '';
const validSession = cookies => cookies.some(
item => item.name === 'pulse_session' && sessions.has(item.value) &&
item.domain === '127.0.0.1' && (item.expires === -1 || item.expires > Date.now() / 1000),
);
const fetch = (cookies, url, args = {}) => {
const method = args.method || 'GET';
const headers = args.headers || {};
requests.push({ method, headers });
const tokenAuth = !primaryRevoked && primary && headers['X-API-Token'] === primary;
if (!validSession(cookies) && !tokenAuth) return response(401);
if (validSession(cookies) && method !== 'GET' && headers['X-CSRF-Token'] !== 'csrf') {
return response(403);
}
if (url.endsWith('/report') && method === 'POST') registeredWithToken = !!tokenAuth;
if (url.endsWith('/fixture-host') && method === 'DELETE' && registeredWithToken) {
primaryRevoked = true;
}
return response(200);
};
const createPage = () => {
let cookies = [];
let url = 'about:blank';
let hasLoginHint = false;
let bootstrapState = 'empty';
let pendingBootstrap;
let resolveWaitStarted;
const waitStarted = new Promise(resolve => { resolveWaitStarted = resolve; });
const navigations = [];
const context = {
cookies: async () => cookies,
addCookies: async values => { cookies = [...cookies, ...values]; },
};
return {
context: () => context,
request: { fetch: async (target, args) => fetch(cookies, target, args) },
goto: async target => {
navigations.push(target);
url = new URL(target, 'http://127.0.0.1:7655').href;
bootstrapState = options.delayedBootstrap ? 'loading' : 'ready';
if (bootstrapState === 'ready' && !options.holdWorkspaceRedirect &&
!options.browserRejectSession && !options.bootstrapFailure && validSession(cookies) &&
new URL(url).pathname === '/infrastructure') {
url = 'http://127.0.0.1:7655/proxmox/overview';
}
},
url: () => url,
navigations,
waitStarted,
rememberLoginHint: () => { hasLoginHint = true; },
completeBootstrap: () => {
bootstrapState = 'ready';
if (!options.holdWorkspaceRedirect) {
url = 'http://127.0.0.1:7655/proxmox/overview';
pendingBootstrap?.();
}
},
completeWorkspaceRedirect: () => {
url = 'http://127.0.0.1:7655/proxmox/overview';
pendingBootstrap?.();
},
waitForFunction: async (predicate, arg, settings) => {
assert.equal(arg, undefined);
assert.equal(settings.timeout, 20_000);
const admitted = () => validSession(cookies) && !options.browserRejectSession &&
(!isPreAuthLoginBootstrapPath(new URL(url).pathname) || hasLoginHint);
// Run the shipped predicate against a DOM-shaped boundary. Rejection
// leaves the requested URL unchanged, just as the real app does.
const evaluate = () => runInNewContext(`(${predicate.toString()})()`, {
window: { location: new URL(url) },
document: { querySelector: selector => {
if (bootstrapState !== 'ready' || options.bootstrapFailure) return null;
const present = selector === '.app-scroll-shell' ? admitted() : !admitted();
return present ? { getClientRects: () => [1] } : null;
} },
getComputedStyle: () => ({ visibility: 'visible' }),
});
const initial = evaluate();
bootstrapChecks.push(initial);
resolveWaitStarted();
if (!initial && (bootstrapState === 'loading' || options.holdWorkspaceRedirect)) {
await new Promise(resolve => { pendingBootstrap = resolve; });
}
const value = evaluate();
if (!value) throw new Error('browser bootstrap timed out');
return { jsonValue: async () => value, dispose: async () => {} };
},
};
};
const api = runInNewContext(helpers, {
fs, path, helpersDir: path.join(root, 'tests'),
process: { pid: process.pid, env: { PULSE_E2E_COOKIE_STATE_PATH: statePath } },
preferredBrowserBaseURL: () => 'http://127.0.0.1:7655',
preferredPlaywrightRouteBaseURL: () => 'http://127.0.0.1:7655',
waitForPulseReady: async () => {},
maybeCompleteSetupWizard: async () => {},
waitForAppShell: async () => {},
authenticateWithPrimaryAPIToken: async page => {
if (!options.tokenAuth) return false;
page.url = () => 'http://127.0.0.1:7655/proxmox';
return true;
},
login: async page => {
loginCalls++;
if (options.loginFailure) throw new Error('Login failed: fixture rejection');
const value = `login-${loginCalls}`;
sessions.add(value);
await page.context().addCookies([cookie('pulse_session', value), cookie('pulse_csrf', 'csrf')]);
page.rememberLoginHint();
await page.goto('/proxmox');
},
expect: page => ({ toHaveURL: async pattern => assert.match(page.url(), pattern) }),
playwrightRequest: { newContext: async ({ storageState, baseURL }) => {
assert.equal(baseURL, 'http://127.0.0.1:7655');
assert.equal(storageState.origins.length, 0);
return {
get: async endpoint => {
assert.equal(endpoint, '/api/state');
probeCalls++;
return response(options.probeStatus ?? (validSession(storageState.cookies) ? 200 : 401));
},
dispose: async () => { disposed++; },
};
} },
configuredPrimaryAPIToken: () => primary,
getTokenAuthRequestContext: async () => ({ fetch: async (url, args) => fetch([], url, args) }),
});
return {
...api, createPage, statePath, sessions, requests, bootstrapChecks,
counts: () => ({ loginCalls, probeCalls, disposed, primaryRevoked }),
save: (cookies, origins = []) => fs.writeFileSync(statePath, JSON.stringify({ cookies, origins })),
};
}
test('twelve generic page fixtures reuse one password session, without backoff or retries', async t => {
const f = fixture(t);
for (let index = 0; index < 12; index++) await f.ensureAuthenticated(f.createPage());
assert.equal(f.counts().loginCalls, 1);
assert.equal(f.counts().probeCalls, 11);
assert.equal(f.counts().disposed, 11);
assert.equal(fs.statSync(f.statePath).mode & 0o777, 0o600);
assert.deepEqual(JSON.parse(fs.readFileSync(f.statePath)).origins, []);
});
test('the existing admitted cookie session avoids another login and does not import org state', async t => {
const f = fixture(t);
f.save([
cookie('pulse_session', 'saved-live'), cookie('pulse_csrf', 'csrf'),
cookie('pulse_org_id', 'another-test-org'), cookie('unrelated', 'another-value'),
], [{ origin: 'http://127.0.0.1:7655', localStorage: [{ name: 'pulse_org_id', value: 'another-test-org' }] }]);
const page = f.createPage();
await f.ensureAuthenticated(page);
assert.equal(f.counts().loginCalls, 0);
assert.deepEqual((await page.context().cookies()).map(item => item.name), ['pulse_session', 'pulse_csrf']);
assert.equal(f.counts().disposed, 1);
});
test('cookie-only reuse enters the protected workspace without manufacturing a local login hint', async t => {
const f = fixture(t);
f.save([cookie('pulse_session', 'saved-live'), cookie('pulse_csrf', 'csrf')]);
const page = f.createPage();
await f.ensureAuthenticated(page);
assert.deepEqual(page.navigations, ['/infrastructure']);
assert.equal(page.url(), 'http://127.0.0.1:7655/proxmox/overview');
assert.deepEqual(f.bootstrapChecks, ['authenticated']);
assert.deepEqual(f.counts(), { loginCalls: 0, probeCalls: 1, disposed: 1, primaryRevoked: false });
});
test('a protected URL and mounted loading root cannot complete delayed cookie bootstrap', async t => {
const f = fixture(t, { delayedBootstrap: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
let completed = false;
const authentication = f.ensureAuthenticated(page).then(() => { completed = true; });
await page.waitStarted;
assert.equal(page.url(), 'http://127.0.0.1:7655/infrastructure');
assert.deepEqual(f.bootstrapChecks, [false]);
assert.equal(completed, false);
assert.equal(f.counts().disposed, 0);
page.completeBootstrap();
await authentication;
assert.equal(completed, true);
assert.equal(f.counts().disposed, 1);
assert.equal(f.counts().loginCalls, 0);
});
test('a mounted cookie shell waits for its pending workspace redirect before admitting a deep link', async t => {
const f = fixture(t, { holdWorkspaceRedirect: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
let completed = false;
const authentication = f.ensureAuthenticated(page).then(() => { completed = true; });
await page.waitStarted;
assert.equal(page.url(), 'http://127.0.0.1:7655/infrastructure');
assert.deepEqual(f.bootstrapChecks, [false], 'a mounted shell on the entry route is still pending');
assert.equal(completed, false);
page.completeWorkspaceRedirect();
await authentication;
assert.equal(completed, true);
assert.equal(f.counts().loginCalls, 0);
await page.goto('/settings/infrastructure?add=agent');
assert.equal(page.url(), 'http://127.0.0.1:7655/settings/infrastructure?add=agent');
});
test('API admission cannot hide browser rejection at the unchanged protected URL', async t => {
const f = fixture(t, { browserRejectSession: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
await assert.rejects(() => f.ensureAuthenticated(page), /Session was rejected by browser bootstrap/);
assert.equal(page.url(), 'http://127.0.0.1:7655/infrastructure');
assert.deepEqual(page.navigations, ['/infrastructure']);
assert.deepEqual(f.bootstrapChecks, ['login']);
assert.equal(f.counts().loginCalls, 0);
assert.equal(f.counts().disposed, 1);
assert.equal(JSON.parse(fs.readFileSync(f.statePath)).cookies[0].value, 'saved-live');
});
test('an incomplete browser bootstrap propagates failure without another login or state replacement', async t => {
const f = fixture(t, { bootstrapFailure: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
await assert.rejects(() => f.ensureAuthenticated(page), /browser bootstrap timed out/);
assert.equal(f.counts().loginCalls, 0);
assert.equal(f.counts().disposed, 1);
assert.equal(JSON.parse(fs.readFileSync(f.statePath)).cookies[0].value, 'saved-live');
});
for (const scenario of ['revoked', 'expired', 'other-backend', 'missing-session', 'malformed-json']) {
test(`${scenario} saved state is not accepted; ordinary login replaces it`, async t => {
const f = fixture(t);
if (scenario === 'malformed-json') fs.writeFileSync(f.statePath, '{');
else {
const saved = cookie(scenario === 'missing-session' ? 'pulse_csrf' : 'pulse_session', scenario);
if (scenario === 'expired') Object.assign(saved, { value: 'saved-live', expires: 1 });
if (scenario === 'other-backend') Object.assign(saved, { value: 'saved-live', domain: 'another-backend.test' });
f.save([saved]);
}
const page = f.createPage();
await f.ensureAuthenticated(page);
assert.equal(f.counts().loginCalls, 1);
assert.equal((await page.context().cookies()).some(item => item.value === scenario), false);
assert.equal(JSON.parse(fs.readFileSync(f.statePath)).cookies[0].value, 'login-1');
assert.equal(f.counts().disposed, f.counts().probeCalls);
});
}
test('a denied shared session and denied password login remain a failure', async t => {
const f = fixture(t, { loginFailure: true });
f.save([cookie('pulse_session', 'revoked')]);
await assert.rejects(() => f.ensureAuthenticated(f.createPage()), /Login failed: fixture rejection/);
assert.equal(f.counts().disposed, 1);
assert.equal(JSON.parse(fs.readFileSync(f.statePath)).cookies[0].value, 'revoked');
});
test('a failed backend probe is not hidden by another login', async t => {
const f = fixture(t, { probeStatus: 500 });
f.save([cookie('pulse_session', 'saved-live')]);
await assert.rejects(() => f.ensureAuthenticated(f.createPage()), /Shared cookie session probe failed: 500/);
assert.equal(f.counts().loginCalls, 0);
assert.equal(f.counts().disposed, 1);
});
test('primary-token fixtures keep their token auth and import no ambient cookie session', async t => {
const f = fixture(t, { tokenAuth: true });
f.save([cookie('pulse_session', 'saved-live')]);
const page = f.createPage();
await f.ensureAuthenticated(page);
assert.equal(f.counts().loginCalls, 0);
assert.equal(f.counts().probeCalls, 0);
assert.equal((await page.context().cookies()).length, 0);
});
function primaryBootstrapFixture({ rejected = false, passwordLogin = false } = {}) {
const rememberedTokens = [];
let currentURL = 'about:blank';
let releaseBootstrap;
const bootstrap = new Promise(resolve => { releaseBootstrap = resolve; });
let notifyWaitStarted;
const waitStarted = new Promise(resolve => { notifyWaitStarted = resolve; });
let ready = false;
let submitted = false;
const page = {
goto: async target => { currentURL = new URL(target, 'http://127.0.0.1:7655').href; },
url: () => currentURL,
waitForURL: async () => {
if (passwordLogin && !submitted) return new Promise(() => {});
currentURL = 'http://127.0.0.1:7655/infrastructure';
},
locator: () => ({
waitFor: () => passwordLogin && !submitted ? Promise.resolve() : new Promise(() => {}),
first() { return this; },
isVisible: async () => false,
}),
fill: async () => {},
click: async () => { submitted = true; currentURL = 'http://127.0.0.1:7655/infrastructure'; },
waitForResponse: async () => response(200),
waitForFunction: async predicate => {
const evaluate = () => runInNewContext(`(${predicate.toString()})()`, {
window: { location: new URL(currentURL) },
document: { querySelector: selector => {
if (!ready) return null;
const present = selector === '.app-scroll-shell' ? !rejected : rejected;
return present ? { getClientRects: () => [1] } : null;
} },
getComputedStyle: () => ({ visibility: 'visible' }),
});
assert.equal(evaluate(), false, 'protected bootstrap is still loading');
notifyWaitStarted();
await bootstrap;
return { jsonValue: async () => evaluate(), dispose: async () => {} };
},
};
const { authenticate, authenticateExplicit, login } = runInNewContext(stripTypeScriptTypes([
source.match(/const AUTHENTICATED_URL =[\s\S]*?;\n/)[0],
fragment('async function waitForAuthenticatedAppShell(', 'export async function waitForPulseReady('),
fragment('export async function login(', 'export async function authenticateWithAPIToken('),
fragment('export async function authenticateWithAPIToken(', 'export async function ensureAuthenticated('),
'({ authenticate: authenticateWithPrimaryAPIToken, authenticateExplicit: authenticateWithAPIToken, login });',
].join('\n')), {
URL, encodeURIComponent,
primaryAPITokenCandidates: () => [{ token: 'fixture-token' }],
apiRequest: async () => response(200),
rememberPrimaryAPIToken: token => { rememberedTokens.push(token); },
waitForAppShell: async () => {},
});
return {
page, authenticate, authenticateExplicit, login, rememberedTokens, waitStarted,
completeBootstrap: () => {
ready = true;
currentURL = 'http://127.0.0.1:7655/proxmox/overview';
releaseBootstrap();
},
};
}
test('an admitted token and workspace URL cannot race a subsequent onboarding navigation', async () => {
const f = primaryBootstrapFixture();
let completed = false;
const authentication = f.authenticate(f.page).then(value => { completed = true; return value; });
await Promise.race([f.waitStarted, authentication]);
assert.equal(completed, false, 'token auth must wait for the mounted authenticated shell');
f.completeBootstrap();
assert.equal(await authentication, true);
await f.page.goto('/settings/infrastructure?add=agent');
assert.equal(f.page.url(), 'http://127.0.0.1:7655/settings/infrastructure?add=agent');
});
test('a valid token probe cannot hide a rejected browser bootstrap', async () => {
const f = primaryBootstrapFixture({ rejected: true });
const authentication = f.authenticate(f.page);
await Promise.race([f.waitStarted, authentication]);
f.completeBootstrap();
await assert.rejects(authentication, /Session was rejected by browser bootstrap/);
});
test('a test-owned token waits for bootstrap without replacing the primary fixture credential', async () => {
const f = primaryBootstrapFixture();
let completed = false;
const authentication = f.authenticateExplicit(f.page, 'test-owned-token').then(value => {
completed = true;
return value;
});
await Promise.race([f.waitStarted, authentication]);
assert.equal(completed, false);
f.completeBootstrap();
assert.equal(await authentication, true);
assert.deepEqual(f.rememberedTokens, []);
});
for (const passwordLogin of [false, true]) {
test(`${passwordLogin ? 'a submitted password' : 'an existing cookie'} login waits for mounted browser bootstrap`, async () => {
const f = primaryBootstrapFixture({ passwordLogin });
let completed = false;
const authentication = f.login(f.page, { username: 'fixture', password: 'fixture' }).then(() => { completed = true; });
await Promise.race([f.waitStarted, authentication]);
assert.equal(completed, false, 'a workspace URL alone must not finish login');
f.completeBootstrap();
await authentication;
assert.equal(completed, true);
});
}
test('explicit session fixtures still get independent password sessions', async t => {
const f = fixture(t, { tokenAuth: true });
f.save([cookie('pulse_session', 'saved-live')]);
await f.ensureSessionAuthenticated(f.createPage());
await f.ensureSessionAuthenticated(f.createPage());
assert.equal(f.counts().loginCalls, 2);
assert.equal(f.counts().probeCalls, 0);
assert.equal(JSON.parse(fs.readFileSync(f.statePath)).cookies[0].value, 'saved-live');
});
test('cookie-authenticated registration can verify deletion without binding the primary token', async t => {
const f = fixture(t, { primary: 'fixture-primary', tokenAuth: true });
const page = f.createPage();
await f.ensureSessionAuthenticated(page);
for (const [endpoint, method] of [
['/api/agents/agent/report', 'POST'], ['/api/agents/agent/fixture-host', 'DELETE'], ['/api/state', 'GET'],
]) assert.equal((await f.apiRequest(page, endpoint, { method })).status(), 200);
assert.equal(f.counts().primaryRevoked, false);
assert.equal(f.requests.some(item => item.headers['X-API-Token']), false);
assert.equal(f.requests[0].headers['X-CSRF-Token'], 'csrf');
});
test('negative control: deletion revokes a bound token and rejects its subsequent read', async t => {
const f = fixture(t, { primary: 'fixture-primary', tokenAuth: true });
const page = f.createPage();
await f.ensureAuthenticated(page);
assert.equal((await f.apiRequest(page, '/api/agents/agent/report', { method: 'POST' })).status(), 200);
assert.equal((await f.apiRequest(page, '/api/agents/agent/fixture-host', { method: 'DELETE' })).status(), 200);
assert.equal((await f.apiRequest(page, '/api/state')).status(), 401);
assert.equal(f.counts().primaryRevoked, true);
});
test('the agent journey selects session auth and retains deletion/state assertions', () => {
const spec = fs.readFileSync(path.join(root, 'tests/journeys/04-agent-install-registration.spec.ts'), 'utf8');
assert.match(spec, /ensureSessionAuthenticated/);
assert.doesNotMatch(spec, /ensureAuthenticated/);
assert.match(spec, /`Post-deletion state request failed: \$\{stateRes.status\(\)\}`/);
assert.match(spec, /findRegisteredAgentResource\(state\),\s*"Host should be removed from state after deletion",\s*\).toBeFalsy\(\)/);
});