yamltest 1.1.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +18 -1
- package/src/core.js +20 -0
- package/src/validate.js +41 -3
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ const c = {
|
|
|
36
36
|
// ── Argument parsing ──────────────────────────────────────────────────────────
|
|
37
37
|
function parseArgs(argv) {
|
|
38
38
|
const args = argv.slice(2); // strip node + script
|
|
39
|
-
const opts = { file: null };
|
|
39
|
+
const opts = { file: null, check: false };
|
|
40
40
|
|
|
41
41
|
for (let i = 0; i < args.length; i++) {
|
|
42
42
|
if (args[i] === '-f' || args[i] === '--file') {
|
|
@@ -46,6 +46,8 @@ function parseArgs(argv) {
|
|
|
46
46
|
opts.file = args[i].slice(3);
|
|
47
47
|
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
48
48
|
opts.help = true;
|
|
49
|
+
} else if (args[i] === '--check') {
|
|
50
|
+
opts.check = true;
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -75,6 +77,7 @@ function printUsage() {
|
|
|
75
77
|
'',
|
|
76
78
|
c.bold('OPTIONS'),
|
|
77
79
|
' -f, --file <path|-> YAML file to run, or - for stdin',
|
|
80
|
+
' --check Validate YAML structure only; do not run tests',
|
|
78
81
|
' -h, --help Show this help',
|
|
79
82
|
'',
|
|
80
83
|
c.bold('ENVIRONMENT'),
|
|
@@ -185,6 +188,20 @@ async function main() {
|
|
|
185
188
|
process.exit(1);
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
if (opts.check) {
|
|
192
|
+
try {
|
|
193
|
+
const { parseTestDefinitions } = require('./runner');
|
|
194
|
+
const { validateTestDefinitions } = require('./validate');
|
|
195
|
+
const defs = parseTestDefinitions(yamlContent);
|
|
196
|
+
validateTestDefinitions(defs);
|
|
197
|
+
process.stdout.write(c.green('ok') + '\n');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
process.stderr.write(c.red('invalid: ') + err.message + '\n');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
188
205
|
let result;
|
|
189
206
|
try {
|
|
190
207
|
result = await runTests(yamlContent);
|
package/src/core.js
CHANGED
|
@@ -1067,6 +1067,26 @@ async function executePodHttpRequestViaPodExec(test) {
|
|
|
1067
1067
|
targetUrl += httpConfig.path;
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
+
// If the URL is an IP address and a Host header is set, use --resolve so curl
|
|
1071
|
+
// uses the hostname for TLS SNI — matching how axios behaves for local requests.
|
|
1072
|
+
const parsedTargetUrl = new URL(targetUrl);
|
|
1073
|
+
const hostHeader = httpConfig.headers && Object.entries(httpConfig.headers).find(
|
|
1074
|
+
([k]) => k.toLowerCase() === 'host'
|
|
1075
|
+
);
|
|
1076
|
+
const urlHostIsIp = net.isIP(parsedTargetUrl.hostname) !== 0;
|
|
1077
|
+
if (urlHostIsIp && hostHeader) {
|
|
1078
|
+
const sniHostname = hostHeader[1];
|
|
1079
|
+
const originalIp = parsedTargetUrl.hostname;
|
|
1080
|
+
const port = parsedTargetUrl.port || (parsedTargetUrl.protocol === 'https:' ? '443' : '80');
|
|
1081
|
+
// Rewrite the URL to use the hostname so curl sends SNI correctly
|
|
1082
|
+
parsedTargetUrl.hostname = sniHostname;
|
|
1083
|
+
parsedTargetUrl.port = port;
|
|
1084
|
+
targetUrl = parsedTargetUrl.toString();
|
|
1085
|
+
// --resolve maps hostname:port to the original IP so the connection still goes to the right place
|
|
1086
|
+
curlCmd += ` --resolve '${sniHostname}:${port}:${originalIp}'`;
|
|
1087
|
+
debugLog(`Added --resolve for SNI: ${sniHostname}:${port}:${originalIp}`);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1070
1090
|
curlCmd += ` '${targetUrl}'`;
|
|
1071
1091
|
|
|
1072
1092
|
// Build kubectl exec command
|
package/src/validate.js
CHANGED
|
@@ -202,7 +202,7 @@ const httpConfigSchema = {
|
|
|
202
202
|
type: 'object',
|
|
203
203
|
properties: {
|
|
204
204
|
url: { type: 'string' },
|
|
205
|
-
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'get', 'post', 'put', 'delete', 'patch'] },
|
|
205
|
+
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'get', 'post', 'put', 'delete', 'patch', 'options'] },
|
|
206
206
|
path: { type: 'string' },
|
|
207
207
|
headers: {
|
|
208
208
|
type: 'object',
|
|
@@ -458,18 +458,41 @@ const testDefinitionSchema = {
|
|
|
458
458
|
{
|
|
459
459
|
if: { required: ['http'] },
|
|
460
460
|
then: {
|
|
461
|
-
required: ['source'],
|
|
461
|
+
required: ['source', 'expect'],
|
|
462
462
|
properties: {
|
|
463
463
|
expect: httpExpectSchema,
|
|
464
464
|
setVars: httpSetVarsSchema,
|
|
465
465
|
},
|
|
466
466
|
},
|
|
467
467
|
},
|
|
468
|
+
// HTTP: url is required unless source is a local Service (auto-discovery)
|
|
469
|
+
{
|
|
470
|
+
if: { required: ['http'] },
|
|
471
|
+
then: {
|
|
472
|
+
anyOf: [
|
|
473
|
+
{ type: 'object', properties: { http: { type: 'object', required: ['url'] } } },
|
|
474
|
+
{
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
source: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: {
|
|
480
|
+
type: { const: 'local' },
|
|
481
|
+
selector: { type: 'object', properties: { kind: { const: 'Service' } }, required: ['kind'] },
|
|
482
|
+
},
|
|
483
|
+
required: ['type', 'selector'],
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
errorMessage: 'http.url is required (or use source.selector.kind: Service for auto-discovery)',
|
|
489
|
+
},
|
|
490
|
+
},
|
|
468
491
|
// Command: validate expect and setVars shapes
|
|
469
492
|
{
|
|
470
493
|
if: { required: ['command'] },
|
|
471
494
|
then: {
|
|
472
|
-
required: ['source'],
|
|
495
|
+
required: ['source', 'expect'],
|
|
473
496
|
properties: {
|
|
474
497
|
expect: commandExpectSchema,
|
|
475
498
|
setVars: commandSetVarsSchema,
|
|
@@ -533,10 +556,25 @@ function formatValidationErrors(errors, definitions) {
|
|
|
533
556
|
const seen = new Set();
|
|
534
557
|
const meaningful = [];
|
|
535
558
|
|
|
559
|
+
// Collect instance paths that have a custom errorMessage so we can suppress
|
|
560
|
+
// the raw sub-errors from their anyOf branches (they are noise).
|
|
561
|
+
const errorMessagePaths = errors
|
|
562
|
+
.filter(e => e.keyword === 'errorMessage')
|
|
563
|
+
.map(e => e.instancePath);
|
|
564
|
+
|
|
565
|
+
// Returns true when a custom errorMessage covers this instancePath (exact or ancestor).
|
|
566
|
+
const coveredByErrorMessage = (instancePath) =>
|
|
567
|
+
errorMessagePaths.some(p => instancePath === p || instancePath.startsWith(p + '/'));
|
|
568
|
+
|
|
536
569
|
for (const err of errors) {
|
|
537
570
|
// Skip generic wrapper messages that aren't actionable
|
|
538
571
|
if (err.keyword === 'if' || err.keyword === 'ifThen') continue;
|
|
539
572
|
|
|
573
|
+
// Skip raw sub-errors that originate inside an anyOf branch when a custom
|
|
574
|
+
// errorMessage already covers the same instance path — the errorMessage is
|
|
575
|
+
// the actionable line; the branch sub-errors are misleading noise.
|
|
576
|
+
if (err.schemaPath.includes('/anyOf/') && coveredByErrorMessage(err.instancePath)) continue;
|
|
577
|
+
|
|
540
578
|
const key = `${err.instancePath}|${err.keyword}|${err.message}`;
|
|
541
579
|
if (seen.has(key)) continue;
|
|
542
580
|
seen.add(key);
|