feat: add JSON-based settings file for authorization-server tests (supersedes #228) (#360)

* feat: add JSON-based setup option to authorization server conformance test

* refactor(auth): single-schema validation for --file; drop duplicate schema/merge

* chore: move authorization-server settings sample to examples/

* test(auth): cover --file merge precedence and validation

* fix(auth): validate --file standalone via .strict(); reject partial/typo'd files

The settings file must be a complete, valid AuthorizationServerOptions on
its own; CLI flags are optional overrides on top. .strict() rejects unknown
keys so a typo'd field surfaces as an error instead of being silently
ignored. ZodError from file parsing is reported with the file path.

* chore: drop placeholder --file merge test pending CLI test pattern

The test re-implemented the merge logic locally, which can drift from
src/index.ts. Proper coverage needs either programmatic commander.parse()
or an extracted loadAuthorizationServerOptions() helper; deferring that
decision to a follow-up rather than establishing the pattern here.

---------

Co-authored-by: Michito Okai <michito.okai.zn@hitachi.com>
This commit is contained in:
Paul Carleton 2026-06-24 15:07:32 +01:00 • committed by GitHub
commit 42fc3eb334
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 3 deletions

View file

@ -0,0 +1,3 @@
{
"url": "https://auth.example.com"
}

View file

@ -2,6 +2,7 @@
import { Command } from 'commander'; import { Command } from 'commander';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { promises as fs } from 'fs';
import { import {
runConformanceTest, runConformanceTest,
printClientResults, printClientResults,
@ -41,6 +42,7 @@ import {
ClientOptionsSchema, ClientOptionsSchema,
ServerOptionsSchema ServerOptionsSchema
} from './schemas'; } from './schemas';
import type { AuthorizationServerOptions } from './schemas';
import { import {
loadExpectedFailures, loadExpectedFailures,
evaluateBaseline, evaluateBaseline,
@ -514,7 +516,11 @@ program
.description( .description(
'Run conformance tests against an authorization server implementation' 'Run conformance tests against an authorization server implementation'
) )
.requiredOption('--url <url>', 'URL of the authorization server issuer') .option(
'--file <filename>',
'Path to JSON settings file (see examples/authorization-server-settings.example.json)'
)
.option('--url <url>', 'URL of the authorization server issuer')
.option('--scenario <scenario>', 'Test scenario to run') .option('--scenario <scenario>', 'Test scenario to run')
.option('-o, --output-dir <path>', 'Save results to this directory') .option('-o, --output-dir <path>', 'Save results to this directory')
.option( .option(
@ -524,8 +530,43 @@ program
.option('--verbose', 'Show verbose output (JSON instead of pretty print)') .option('--verbose', 'Show verbose output (JSON instead of pretty print)')
.action(async (options) => { .action(async (options) => {
try { try {
// Validate options with Zod let fileOptions: AuthorizationServerOptions | undefined;
const validated = AuthorizationServerOptionsSchema.parse(options); if (options.file) {
try {
const raw = JSON.parse(await fs.readFile(options.file, 'utf-8'));
// The file must be a complete, valid config on its own; CLI flags
// are optional overrides. .strict() rejects unknown keys so typos
// surface instead of being silently ignored.
fileOptions = AuthorizationServerOptionsSchema.strict().parse(raw);
} catch (error) {
if (error instanceof ZodError) {
const details = error.issues
.map((e) => ` ${e.path.join('.') || '(root)'}: ${e.message}`)
.join('\n');
console.error(
`Invalid settings file '${options.file}':\n${details}`
);
} else {
console.error(
`Failed to read settings file '${options.file}': ` +
(error instanceof Error ? error.message : String(error))
);
}
process.exit(1);
}
}
if (!fileOptions && !options.url) {
console.error('error: must provide --url or --file');
process.exit(1);
}
// CLI flags override file values; undefined CLI values must not clobber file values
const merged = {
...fileOptions,
...Object.fromEntries(
Object.entries(options).filter(([, v]) => v !== undefined)
)
};
const validated = AuthorizationServerOptionsSchema.parse(merged);
const verbose = options.verbose ?? false; const verbose = options.verbose ?? false;
const outputDir = options.outputDir; const outputDir = options.outputDir;
const specVersionFilter = options.specVersion const specVersionFilter = options.specVersion