yamltest 1.1.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yamltest",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Declarative YAML-based HTTP, command, and Kubernetes test runner",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
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
@@ -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);