@codefresh-io/yaml-validator
Version:
An NPM module/CLI for validating the Codefresh YAML
1,285 lines (1,188 loc) • 350 kB
JavaScript
/* eslint-env node, mocha */
/* eslint-disable no-unused-expressions,no-template-curly-in-string */
'use strict';
const _ = require('lodash');
const chai = require('chai');
const { randomUUID } = require('crypto');
const fs = require('fs');
const jsyaml = require('js-yaml');
const path = require('path');
const colors = require('colors');
const { expect } = chai;
const sinonChai = require('sinon-chai');
chai.use(sinonChai);
const Mustache = require('mustache');
const Validator = require('../validator');
const { isWebUri } = require('../schema/1.0/validations/registry');
const { CustomDocumentationLinks } = require('../schema/1.0/documentation-links');
const yamlTemplateForDuplicateStepNamesTest = JSON.stringify({
'version': '1.0',
'steps': {
'{{stepName0}}': {
'type': 'parallel',
'steps': {
'{{stepName0_1}}': {
'title': 'Step1A',
'image': 'alpine',
'commands': [
'echo "Step1A" > first.txt'
]
},
'{{stepName0_2}}': {
'title': 'Step1B',
'image': 'alpine',
'commands': [
'echo "Step1B" > second.txt'
]
}
}
},
'{{stepName1}}': {
'type': 'parallel',
'steps': {
'{{stepName1_1}}': {
'title': 'Step1A',
'image': 'alpine',
'commands': [
'echo "Step1A" > first.txt'
]
},
'{{stepName1_2}}': {
'title': 'Step1B',
'image': 'alpine',
'commands': [
'echo "Step1B" > second.txt'
]
}
}
}
}
});
const yamlTemplateForNestedDuplicateStepNamesTest = JSON.stringify({
'version': '1.0',
'steps': {
'{{stepName0}}': {
'type': 'parallel',
'steps': {
'{{stepName0_1}}': {
'title': 'Step1A',
'image': 'alpine',
'commands': [
'echo "Step1A" > first.txt'
]
},
'{{stepName0_2}}': {
'title': 'Step1B',
'image': 'alpine',
'commands': [
'echo "Step1B" > second.txt'
]
},
'{{stepName0_3}}': {
'type': 'parallel',
'steps': {
'{{stepName0_3_1}}': {
'title': 'Step1A',
'image': 'alpine',
'commands': [
'echo "Step1A" > first.txt'
]
}
}
}
}
},
'{{stepName1}}': {
'type': 'parallel',
'steps': {
'{{stepName1_1}}': {
'title': 'Step1A',
'image': 'alpine',
'commands': [
'echo "Step1A" > first.txt'
]
},
'{{stepName1_2}}': {
'title': 'Step1B',
'image': 'alpine',
'commands': [
'echo "Step1B" > second.txt'
]
}
}
}
}
});
function validate(model, outputFormat, yaml) {
return Validator.validate(model, outputFormat, yaml);
}
function validateWithContext(model, outputFormat, yaml, context, opts) {
return Validator.validateWithContext(model, outputFormat, yaml, context, opts);
}
function validateForErrorWithContext(model, expectedError, done, outputFormat = 'message', yaml, context, opts) {
try {
validateWithContext(model, outputFormat, yaml, context, opts);
done(new Error('should have failed'));
} catch (e) {
if (outputFormat === 'message') {
expect(e.details).to.deep.equal(expectedError.details);
expect(e.warningDetails).deep.equal(expectedError.warningDetails);
}
if (outputFormat === 'lint') {
expect(e.message).to.deep.equal(expectedError.message);
expect(e.warningMessage).deep.equal(expectedError.warningMessage);
expect(e.summarize).deep.equal(expectedError.summarize);
expect(e.documentationLinks).deep.equal(expectedError.documentationLinks);
}
done();
}
}
function validateForError(model, expectedMessage, done, outputFormat = 'message', yaml) {
try {
validate(model, outputFormat, yaml);
done(new Error('Validation should have failed'));
} catch (e) {
if (outputFormat === 'message') {
expect(e.message).to.match(new RegExp(`.*${expectedMessage}.*`));
}
if (outputFormat === 'printify' || outputFormat === 'lint') {
expect(e.details[0].message).to.equal(expectedMessage.message);
expect(e.details[0].type).to.equal(expectedMessage.type);
expect(e.details[0].level).to.equal(expectedMessage.level);
expect(e.details[0].stepName).to.equal(expectedMessage.stepName);
expect(e.details[0].docsLink).to.equal(expectedMessage.docsLink);
expect(e.details[0].actionItems).to.equal(expectedMessage.actionItems);
expect(e.details[0].lines).to.equal(expectedMessage.lines);
}
done();
}
}
const currentPath = './__tests__/';
describe('Validate Codefresh YAML', () => {
describe('Root elements', () => {
it('No version', (done) => {
validateForError({
steps: {
jim: {}
}
}, '"version" is required', done);
});
it('No steps', (done) => {
validateForError({
version: '1.0'
}, '"steps" is required', done);
});
it('Unknown root element', (done) => {
validateForError({
version: '1.0',
whatIsThisElement: '',
steps: {
jim: {
image: 'bob'
}
}
}, '"whatIsThisElement" is not allowed', done);
});
it('Unknown version', (done) => {
validateForError({
version: '0.1',
steps: {
jim: {
image: 'bob'
}
}
}, 'Current version: 0.1 is invalid. please change version to 1.0', done);
});
it('incorrect build_version', (done) => {
validateForError({
version: '1.0',
build_version: 'v3',
steps: {}
}, '"build_version" must be one of', done);
});
it('valid build_version', (done) => {
validate({
version: '1.0',
build_version: 'v2',
steps: {}
});
done();
});
describe('Hooks', () => {
it('valid hooks', (done) => {
validate({
version: '1.0',
steps: {},
hooks: {
on_elected: ['echo test'],
on_success: {
exec: ['echo test'],
},
on_finish: {
image: 'alpine',
commands: ['echo test'],
},
on_fail: {
exec: {
image: 'alpine',
commands: ['echo test'],
},
metadata: {
set: [
{
test: [
{
test: 'test'
}
]
}
]
},
annotations: {
set: [
{
entity_type: 'build',
annotations: [{ test: 'test' }]
}
],
unset: [
{
entity_type: 'build',
annotations: ['test']
}
]
}
}
}
});
done();
});
it('valid pipeline hooks with plugins / costume steps', (done) => {
validate({
version: '1.0',
steps: {},
hooks: {
on_elected: ['echo test'],
on_finish: {
steps: {
freestyle: {
title: 'some title',
type: 'freestyle',
arguments: {
image: 'ubuntu:latest',
commands: ['echo test']
},
},
clone: {
title: 'clone title',
type: 'git-clone',
repo: 'codefresh/repo'
},
},
},
on_success: {
steps: {
deploy: {
type: 'helm:1.0.0',
arguments: {
chart_name: 'test_chart',
release_name: 'first',
kube_context: 'my-kubernetes-context',
tiller_namespace: 'kube-system',
namespace: 'project',
custom_values: [
'KEY1=VAL1',
'KEY2=VAL2',
'KEY3=VAL3',
],
custom_value_files: [
'/path/to/values.yaml',
'/path/to/values2.yaml'
],
cmd_ps: '--wait --timeout 5'
}
}
}
},
on_fail: {
steps: {
exec: {
image: 'alpine',
commands: ['echo test'],
},
clone: {
title: 'clone title',
type: 'git-clone',
repo: 'codefresh/repo'
},
build: {
title: 'Building Docker image',
type: 'build',
image_name: 'user/sandbox',
working_directory: '${{clone}}',
tag: '${{CF_BRANCH_TAG_NORMALIZED}}',
dockerfile: 'Dockerfile',
}
},
}
}
});
done();
});
it('valid pipeline hooks that contain only metadata/annotations', (done) => {
validate({
version: '1.0',
steps: {},
hooks: {
on_success: {
metadata: {
set: [{
'${{steps.build.imageId}}': [{
'CF_QUALITY': true,
'COMMIT_HASH': '${{CF_SHORT_REVISION}}',
'COMMIT_URL': '${{CF_COMMIT_URL}}',
'DD_VERSION': '${{CF_SHORT_REVISION}}',
}]
}]
}
},
on_fail: {
annotations: {
set: [
{
entity_type: 'build',
annotations: [{ test: 'test' }]
}
],
unset: [
{
entity_type: 'build',
annotations: ['test']
}
]
}
},
},
});
done();
});
it('invalid hooks', (done) => {
validateForError({
version: '1.0',
steps: {},
hooks: {
on_elected: ['echo test'],
on_success: {
exec: ['echo test'],
},
on_finish: {
image: 'alpine',
commands: ['echo test'],
},
on_fail: {
exec: {
image: 'alpine',
commands: ['echo test'],
},
metadata: {
set: [
{
test: [
{
test: 'test'
}
]
}
]
},
annotations: {
set: [
{
entity_type: 'build',
annotations: [{ test: 'test' }]
}
],
unset: [
{
entity_type: 'build',
annotations: ['test']
}
]
}
},
on_something: {
image: 'alpine'
}
}
}, '"on_something" is not allowed', done);
});
});
describe('strict_fail_fast', () => {
it.each([
true,
false,
undefined,
])('should pass if "strict_fail_fast" is valid data type: %s', (strictFailFast) => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image' } },
strict_fail_fast: strictFailFast,
});
});
it.each([
0,
42,
'',
'mock',
null,
{},
[],
])(`should not pass if "strict_fail_fast" is invalid data type: %s`, (strictFailFast, done) => {
validateForError({
version: '1.0',
steps: { mock: { image: 'mock-image' } },
strict_fail_fast: strictFailFast,
}, `"strict_fail_fast" must be a boolean`, done);
});
});
});
describe('Steps', () => {
describe('Common step attributes', () => {
it('Unrecognized type', () => {
validate({
version: '1.0',
steps: {
jim: { type: 'invalid:1.0.0' }
}
});
});
it('Working directory on a push step', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'push',
'candidate': 'bob',
'working_directory': 'meow'
}
}
}, '"working_directory" is not allowed', done);
});
it('Credentials on a build step', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'credentials': {
username: 'jim',
password: 'bob'
}
}
}
}, '"credentials" is not allowed', done);
});
it('no_cache on a build step', () => {
validate({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'no_cache': true
}
}
});
});
it('no_cf_cache on a build step', () => {
validate({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'no_cf_cache': true
}
}
});
});
it('Non-bool no_cache on a build step', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'no_cache': 'please do sir'
}
}
}, '"no_cache" must be a boolean', done);
});
it('Non-bool no_cache on a build step', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'no_cf_cache': 'please do sir'
}
}
}, '"no_cf_cache" must be a boolean', done);
});
it('squash on a build step', () => {
validate({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'squash': true
}
}
});
});
it('Non-bool squash on a build step', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'squash': 'please do sir'
}
}
}, '"squash" must be a boolean', done);
});
it('Empty credentials', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
type: 'git-clone',
repo: 'jim',
credentials: {}
}
}
}, '"username" is required', done);
});
it('Non-string working directory', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'image': 'myimage',
'working_directory': {}
}
}
}, '"working_directory" must be a string', done);
});
it('Non-string description', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'description': {}
}
}
}, '"description" must be a string', done);
});
it('Non-string title', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'title': {}
}
}
}, '"title" must be a string', done);
});
it('Non object docker_machine', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'docker_machine': 'google'
}
}
}, '"docker_machine" must be an object', done);
});
it('Non-boolean fail-fast', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jimb',
'fail_fast': {}
}
}
}, '"fail_fast" must be a boolean', done);
});
it('Non-string tag', (done) => {
validateForError({
version: '1.0',
steps: {
jim: {
'type': 'build',
'image_name': 'owner/jim',
'tag': []
}
}
}, '"tag" must be a string', done);
});
it('Unknown post-step metadata operation', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
put: [
{
'${{build_prj.image}}': [
{ 'qa': 'pending' }
]
}
]
}
}
}
}
}, '"put" is not allowed', done);
});
it('Unknown post-step metadata entry', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
set: {
'${{build_prj.image}}': [
{ 'qa': 'pending' }
]
}
}
}
}
}
}, '"set" must be an array', done);
});
it('Unspecified image to annotate', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
set: [
{ 'qa': 'pending' }
]
}
}
}
}
}, '"qa" must be an array', done);
});
it('Invalid post-step metadata annotation key', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
set: [
{
'${{build_prj.image}}': [
'an invalid key'
]
}
]
}
}
}
}
}, '"an invalid key" fails to match', done);
});
it('Invalid post-step metadata annotation value', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
set: [
{
'${{build_prj.image}}': [
{ 'key1': [] }
]
}
]
}
}
}
}
}, '"key1" must be a', done);
});
it('Invalid post-step metadata annotation evaluation expression', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
metadata: {
set: [
{
'${{build_prj.image}}': [
{
'jimbob': {
eval: 'jimbob == jimbob'
}
}
]
}
]
}
}
}
}
}, '"evaluate" is required', done);
});
it('Unknown post-step annotate operation', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
put: [
{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'qa': 'pending' }
],
}
]
}
}
}
}
}, '"put" is not allowed', done);
});
it('Unknown build annotate operation', (done) => {
validateForError({
version: '1.0',
steps: {
build: {
type: 'build',
image_name: 'owner/name',
annotations: {
put: [
{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'qa': 'pending' }
],
}
]
}
}
}
}, '"put" is not allowed', done);
});
it('Unknown post-step annotation entry', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
set: {
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'qa': 'pending' }
],
},
}
}
}
}
}, '"set" must be an array', done);
});
it('Unknown post-step annotation entry', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
unset: {
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'qa': 'pending' }
],
},
}
}
}
}
}, '"unset" must be an array', done);
});
it('Unknown build annotation entry', (done) => {
validateForError({
version: '1.0',
steps: {
build: {
type: 'build',
image_name: 'owner/name',
annotations: {
set: {
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'qa': 'pending' }
],
},
}
}
}
}, '"set" must be an array', done);
});
it('Invalid post-step annotation key', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
set: [{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'an invalid key': 'pending' }
],
}]
}
}
}
}
}, '"an invalid key" is not allowed', done);
});
it('Invalid post-step annotation key', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
unset: [{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: ['an invalid key'],
}]
}
}
}
}
}, '"an invalid key" fails to match', done);
});
it('Invalid post-step metadata annotation value', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
set: [{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [
{ 'key': [] }
],
}]
}
}
}
}
}, '"key" must be a', done);
});
it('Invalid post-step annotation evaluation expression', (done) => {
validateForError({
version: '1.0',
steps: {
push: {
'type': 'push',
'candidate': 'teh-image',
'on_finish': {
annotations: {
set: [{
entity_type: 'image',
entity_id: '${{build_prj.image}}',
annotations: [{
'key': {
eval: 'jimbob == jimbob'
}
}],
}]
}
}
}
}
}, '"evaluate" is required', done);
});
describe('timeout', () => {
describe('not defined', () => {
it('should pass if timeout was not defined', () => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image' } },
});
});
it.each([
null,
undefined,
])('should pass if timeout is %s', (timeout) => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image', timeout } },
});
});
});
describe('defined', () => {
const units = ['s', 'm', 'h'];
const getRandomUnit = () => {
return units[Math.floor(Math.random() * units.length)];
};
const getRandomInt = () => Math.floor(Math.random() * 1000);
const getRandomFloat = () => Math.random() * 1000;
// TODO: Delete once timeout is required.
// eslint-disable-next-line no-unused-vars
const getInvalidUnit = () => {
const char = String.fromCharCode(Math.floor(Math.random() * 65535));
return units.includes(char) ? getInvalidUnit() : char;
};
const validIntegerTimeouts = [`0${getRandomUnit()}`];
for (let i = 0; i < 50; i += 1) {
validIntegerTimeouts.push(`${getRandomInt()}${getRandomUnit()}`);
}
it.each(validIntegerTimeouts)('should pass if timeout is valid: %s', (timeout) => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image', timeout } },
});
});
const validFloatTimeouts = [
`0.0${getRandomUnit()}`,
`.5${getRandomUnit()}`,
`1.${getRandomUnit()}`,
];
for (let i = 0; i < 50; i += 1) {
validFloatTimeouts.push(`${getRandomFloat()}${getRandomUnit()}`);
}
it.each(validFloatTimeouts)('should pass if timeout is valid: %s', (timeout) => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image', timeout } },
});
});
// TODO: Delete once timeout is required. ⬇️
it.each([
0,
42,
false,
true,
{},
[],
])(`should fail with warning if timeout is invalid data type: %s`, (timeout, done) => {
const expectedError = {
details: [],
warningDetails: [
{
// eslint-disable-next-line max-len
actionItems: 'Please adjust the timeout to match the format "<duration><units>" where duration is int|float and units are s|m|h (e.g. "1m", "30s", "1.5h"). It will be ignored otherwise.',
context: {
key: 'steps',
},
docsLink: 'https://codefresh.io/docs/docs/pipelines/steps/freestyle/',
level: 'step',
lines: undefined,
message: `"timeout" must be a string${timeout ? `. Current value: ${timeout} ` : ''}`,
path: 'steps',
stepName: 'mock',
type: 'Warning',
},
],
};
try {
Validator.validate({
version: '1.0',
steps: { mock: { image: 'mock-image', timeout } },
}, 'message');
done(new Error('should have failed'));
} catch (error) {
expect(error.details).to.deep.equal(expectedError.details);
expect(error.warningDetails).deep.equal(expectedError.warningDetails);
done();
}
});
// END: Delete once timeout is required. ⬆️
// TODO: Uncomment once timeout is required. ⬇️
// it.each([
// 0,
// 42,
// false,
// true,
// {},
// [],
// ])(`should not pass if timeout is invalid data type: %s`, (timeout, done) => {
// validateForError({
// version: '1.0',
// steps: { mock: { image: 'mock-image', timeout } },
// }, `"timeout" must be a string`, done);
// });
// it('should not pass if timeout is an empty string', (done) => {
// validateForError({
// version: '1.0',
// steps: { mock: { image: 'mock-image', timeout: '' } },
// }, `"timeout" is not allowed to be empty`, done);
// });
// const invalidUnits = [];
// for (let i = 0; i < 1000; i += 1) {
// invalidUnits.push(`${getRandomInt()}${getInvalidUnit()}`);
// }
// it.each(invalidUnits)('should not pass if timeout unit is invalid: %s', (timeout, done) => {
// validateForError({
// version: '1.0',
// steps: { mock: { image: 'mock-image', timeout } },
// }, `fails to match the "\\<duration\\>\\<units\\> where duration is int\\|float and units are s\\|m\\|h" pattern`, done);
// });
// const missedUnits = [];
// for (let i = 0; i < 50; i += 1) {
// missedUnits.push(i % 2 ? `${getRandomInt()}` : `${getRandomFloat()}`);
// }
// it.each(missedUnits)('should not pass if units are missed: %s', (timeout, done) => {
// validateForError({
// version: '1.0',
// steps: { mock: { image: 'mock-image', timeout } },
// }, `fails to match the "\\<duration\\>\\<units\\> where duration is int\\|float and units are s\\|m\\|h" pattern`, done);
// });
// it.each([
// `1.5.1${getRandomUnit()}`,
// `1,5${getRandomUnit()}`,
// ])('should not pass if timeout duration is invalid: %s', (timeout, done) => {
// validateForError({
// version: '1.0',
// steps: { mock: { image: 'mock-image', timeout } },
// }, `fails to match the "\\<duration\\>\\<units\\> where duration is int\\|float and units are s\\|m\\|h" pattern`, done);
// });
// END: Uncomment once timeout is required. ⬆️
});
});
describe('strict_fail_fast', () => {
it.each([
true,
false,
undefined,
])('should pass if "strict_fail_fast" is valid data type: %s', (strictFailFast) => {
validate({
version: '1.0',
steps: { mock: { image: 'mock-image', strict_fail_fast: strictFailFast } },
});
});
it.each([
0,
42,
'',
'mock',
null,
{},
[],
])(`should not pass if "strict_fail_fast" is invalid data type: %s`, (strictFailFast, done) => {
validateForError({
version: '1.0',
steps: { mock: { image: 'mock-image', strict_fail_fast: strictFailFast } },
}, `"strict_fail_fast" must be a boolean`, done);
});
});
describe('type', () => {
it(`should not fail if version is not specified for built-in step type`, () => {
Validator.validate({
version: '1.0',
steps: { mock: { image: 'mock-image', type: 'freestyle' } },
}, 'message');
});
it(`should not fail if type is not specified`, () => {
Validator.validate({
version: '1.0',
steps: { mock: { image: 'mock-image' } },
}, 'message');
});
it.each([
'${{TYPE}}',
'${{PART}}-of-type',
'type:${{VERSION}}',
'type:part-of-${{VERSION}}',
'${{PART}}-of-type:part-of-${{VERSION}}',
])('should not fail if type contains variables: "%s"', (type) => {
Validator.validate({
version: '1.0',
steps: { mock: { type } },
}, 'message');
});
it.each([
'foo',
42,
0,
false,
'1.0.foo',
'foo:bar'
])(`should fail with warning if version is not semver: "%s"`, (version, done) => {
const mockType = randomUUID();
const expectedError = {
details: [],
warningDetails: [
{
message: `Invalid semantic version "${version}" for step.`,
actionItems: `Use "type: <type_name>:<version-number>". For example, "type: ${mockType}:1.2.3"`,
context: {
key: 'steps',
},
docsLink: CustomDocumentationLinks['steps-versioning'],
level: 'step',
lines: undefined,