conformance/src/scenarios/client/auth/helpers/testClient.ts
Paul Carleton 69f02a4557
[scenario] OAuth basic & PRM variation (#11)
* readme for auth tests

* wip prm test

* add example helper for local oauth w/o browser

* working through dangling timeout issue

* add better test function

* pretty printing

* add mcp method

* some more logging tweaks

* refactor logger into 1 class

* use common request logger

* fix base scenario

* some renaming

* refactor into helpers

* refactor to support second test

* rm readme

* add comment about www-authenticate
2025-11-10 11:51:30 +00:00

123 lines
3.6 KiB
TypeScript

import { getScenario } from '../../../../scenarios/index.js';
import { spawn } from 'child_process';
const CLIENT_TIMEOUT = 10000; // 10 seconds for client to complete
export async function runClientAgainstScenario(
clientPath: string,
scenarioName: string,
expectedFailureSlugs: string[] = []
): Promise<void> {
const scenario = getScenario(scenarioName);
if (!scenario) {
throw new Error(`Scenario ${scenarioName} not found`);
}
// Start the scenario server
const urls = await scenario.start();
const serverUrl = urls.serverUrl;
try {
// Run the client
await new Promise<void>((resolve, reject) => {
const clientProcess = spawn('npx', ['tsx', clientPath, serverUrl], {
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
clientProcess.stdout?.on('data', (data) => {
stdout += data.toString();
});
clientProcess.stderr?.on('data', (data) => {
stderr += data.toString();
});
const timeout = setTimeout(() => {
clientProcess.kill('SIGTERM');
reject(
new Error(
`Client failed to complete within ${CLIENT_TIMEOUT}ms\nStdout: ${stdout}\nStderr: ${stderr}`
)
);
}, CLIENT_TIMEOUT);
clientProcess.on('exit', (code) => {
clearTimeout(timeout);
if (code === 0) {
resolve();
} else {
reject(
new Error(
`Client exited with code ${code}\nStdout: ${stdout}\nStderr: ${stderr}`
)
);
}
});
clientProcess.on('error', (error) => {
clearTimeout(timeout);
reject(
new Error(
`Failed to start client: ${error.message}\nStdout: ${stdout}\nStderr: ${stderr}`
)
);
});
});
// Get checks from the scenario
const checks = scenario.getChecks();
// Verify checks were returned
if (checks.length === 0) {
throw new Error('No checks returned from scenario');
}
// Filter out INFO checks
const nonInfoChecks = checks.filter((c) => c.status !== 'INFO');
// Check for expected failures
if (expectedFailureSlugs.length > 0) {
// Verify that the expected failures are present
for (const slug of expectedFailureSlugs) {
const check = checks.find((c) => c.id === slug);
if (!check) {
throw new Error(`Expected failure check ${slug} not found`);
}
}
// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failureSlugs = failures.map((c) => c.id);
if (
failureSlugs.sort().join(',') !== expectedFailureSlugs.sort().join(',')
) {
throw new Error(
`Expected failures ${expectedFailureSlugs.sort().join(', ')} but got ${failureSlugs.sort().join(', ')}`
);
}
} else {
// Default: expect all checks to pass
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
if (failures.length > 0) {
const failureMessages = failures
.map((c) => `${c.name}: ${c.errorMessage || c.description}`)
.join('\n ');
throw new Error(`Scenario failed with checks:\n ${failureMessages}`);
}
// All non-INFO checks should be SUCCESS
const successes = nonInfoChecks.filter((c) => c.status === 'SUCCESS');
if (successes.length !== nonInfoChecks.length) {
throw new Error(
`Expected all checks to pass but got ${successes.length}/${nonInfoChecks.length}`
);
}
}
} finally {
// Stop the scenario server
await scenario.stop();
}
}