"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /*--------------------------------------------------------------------------------------------- * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ const testHelper_1 = require("./utils/testHelper"); const verifyError_1 = require("./utils/verifyError"); const serviceSetup_1 = require("./utils/serviceSetup"); const errorMessages_1 = require("./utils/errorMessages"); const assert = require("assert"); const path = require("path"); const vscode_languageserver_types_1 = require("vscode-languageserver-types"); const chai_1 = require("chai"); const yamlSettings_1 = require("../src/yamlSettings"); const schemaUrls_1 = require("../src/languageservice/utils/schemaUrls"); const vscode_json_languageservice_1 = require("vscode-json-languageservice"); describe('Validation Tests', () => { let languageSettingsSetup; let validationHandler; let languageService; let yamlSettings; let telemetry; let schemaProvider; before(() => { languageSettingsSetup = new serviceSetup_1.ServiceSetup() .withValidate() .withCompletion() .withCustomTags(['!Test', '!Ref sequence']) .withSchemaFileMatch({ uri: schemaUrls_1.KUBERNETES_SCHEMA_URL, fileMatch: ['.drone.yml'] }) .withSchemaFileMatch({ uri: 'https://json.schemastore.org/drone', fileMatch: ['.drone.yml'] }) .withSchemaFileMatch({ uri: schemaUrls_1.KUBERNETES_SCHEMA_URL, fileMatch: ['test.yml'] }) .withSchemaFileMatch({ uri: 'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json', fileMatch: ['test.yml'], }); const { languageService: langService, validationHandler: valHandler, yamlSettings: settings, telemetry: testTelemetry, schemaProvider: testSchemaProvider, } = (0, testHelper_1.setupLanguageService)(languageSettingsSetup.languageSettings); languageService = langService; validationHandler = valHandler; yamlSettings = settings; telemetry = testTelemetry; schemaProvider = testSchemaProvider; }); function parseSetup(content, customSchemaID) { const testTextDocument = (0, testHelper_1.setupSchemaIDTextDocument)(content, customSchemaID); yamlSettings.documents = new yamlSettings_1.TextDocumentTestManager(); yamlSettings.documents.set(testTextDocument); return validationHandler.validateTextDocument(testTextDocument); } afterEach(() => { schemaProvider.deleteSchema(testHelper_1.SCHEMA_ID); }); describe('Boolean tests', () => { it('Boolean true test', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = 'analytics: true'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Basic false test', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = 'analytics: false'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Test that boolean value without quotations is valid', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = '%YAML 1.1\n---\nanalytics: no'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Test that boolean value in quotations is interpreted as string not boolean', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = 'analytics: "no"'; const validator = parseSetup(content); validator .then(function (result) { assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.BooleanTypeError, 0, 11, 0, 15, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); it('Error on incorrect value type (boolean)', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = 'cwd: False'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.StringTypeError, 0, 5, 0, 10, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); it('Test that boolean value can be used in enum', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { enum: [true, false], }, }, }); const content = 'analytics: true'; const validator = parseSetup(content); validator .then(function (result) { assert.deepStrictEqual(result, []); }) .then(done, done); }); it('Test that boolean value can be used in const', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { const: true, }, }, }); const content = 'analytics: true'; const validator = parseSetup(content); validator .then(function (result) { assert.deepStrictEqual(result, []); }) .then(done, done); }); it('Test that YAML 1.1 boolean "True" can be used in enum', async () => { // This test requires YAML 1.1 mode where "True" is parsed as a boolean languageService.configure(languageSettingsSetup.withYamlVersion('1.1').languageSettings); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { enabled: { enum: [true, false], }, }, }); const content = 'enabled: True'; const result = await parseSetup(content); assert.deepStrictEqual(result, []); // Reset to default YAML version languageService.configure(languageSettingsSetup.withYamlVersion('1.2').languageSettings); }); it('Test that YAML 1.1 boolean "False" can be used in enum', async () => { languageService.configure(languageSettingsSetup.withYamlVersion('1.1').languageSettings); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { enabled: { enum: [true, false], }, }, }); const content = 'enabled: False'; const result = await parseSetup(content); assert.deepStrictEqual(result, []); languageService.configure(languageSettingsSetup.withYamlVersion('1.2').languageSettings); }); it('Test that YAML 1.1 boolean "yes" can be used with const', async () => { languageService.configure(languageSettingsSetup.withYamlVersion('1.1').languageSettings); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { confirmed: { const: true, }, }, }); const content = 'confirmed: yes'; const result = await parseSetup(content); assert.deepStrictEqual(result, []); languageService.configure(languageSettingsSetup.withYamlVersion('1.2').languageSettings); }); it('Test that YAML 1.1 boolean "no" can be used with const', async () => { languageService.configure(languageSettingsSetup.withYamlVersion('1.1').languageSettings); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { disabled: { const: false, }, }, }); const content = 'disabled: no'; const result = await parseSetup(content); assert.deepStrictEqual(result, []); languageService.configure(languageSettingsSetup.withYamlVersion('1.2').languageSettings); }); }); describe('String tests', () => { it('Test that boolean inside of quotations is of type string', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'string', }, }, }); const content = 'analytics: "no"'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Type string validates under children', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { scripts: { type: 'object', properties: { register: { type: 'string', }, }, }, }, }); const content = 'registry:\n register: file://test_url'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Type String does not error on valid node', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = 'cwd: this'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Error on incorrect value type (string)', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = 'analytics: hello'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.BooleanTypeError, 0, 11, 0, 16, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); it('Test that boolean is invalid when no strings present and schema wants string', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = '%YAML 1.1\n---\ncwd: no'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.StringTypeError, 2, 5, 2, 7, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); }); describe('Pattern tests', () => { it('Test a valid Unicode pattern', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { prop: { type: 'string', pattern: '^tes\\p{Letter}$', }, }, }); parseSetup('prop: "tesT"') .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Test an invalid Unicode pattern', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { prop: { type: 'string', pattern: '^tes\\p{Letter}$', }, }, }); parseSetup('prop: "tes "') .then(function (result) { assert.equal(result.length, 1); assert.ok(result[0].message.startsWith('String does not match the pattern')); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(result[0].message, 0, 6, 0, 12, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); it('Test a valid Unicode patternProperty', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', patternProperties: { '^tes\\p{Letter}$': true, }, additionalProperties: false, }); parseSetup('tesT: true') .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Test an invalid Unicode patternProperty', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', patternProperties: { '^tes\\p{Letter}$': true, }, additionalProperties: false, }); parseSetup('tes9: true') .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)('Property tes9 is not allowed.', 0, 0, 0, 4, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`, vscode_json_languageservice_1.ErrorCode.PropertyExpected)); }) .then(done, done); }); }); describe('Number tests', () => { it('Type Number does not error on valid node', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { timeout: { type: 'number', }, }, }); const content = 'timeout: 60000'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Error on incorrect value type (number)', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = 'cwd: 100000'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.StringTypeError, 0, 5, 0, 11, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); }); describe('Null tests', () => { it('Basic test on nodes with null', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', additionalProperties: false, properties: { columns: { type: 'object', patternProperties: { '^[a-zA-Z]+$': { type: 'object', properties: { int: { type: 'null', }, long: { type: 'null', }, id: { type: 'null', }, unique: { type: 'null', }, }, oneOf: [ { required: ['int'], }, { required: ['long'], }, ], }, }, }, }, }); const content = 'columns:\n ColumnA: { int, id }\n ColumnB: { long, unique }\n ColumnC: { long, unique }'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); describe('Object tests', () => { it('Basic test on nodes with children', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { scripts: { type: 'object', properties: { preinstall: { type: 'string', }, postinstall: { type: 'string', }, }, }, }, }); const content = 'scripts:\n preinstall: test1\n postinstall: test2'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Test with multiple nodes with children', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, cwd: { type: 'string', }, scripts: { type: 'object', properties: { preinstall: { type: 'string', }, postinstall: { type: 'string', }, }, }, }, }); const content = 'analytics: true\ncwd: this\nscripts:\n preinstall: test1\n postinstall: test2'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Type Object does not error on valid node', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { registry: { type: 'object', properties: { search: { type: 'string', }, }, }, }, }); const content = 'registry:\n search: file://test_url'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Error on incorrect value type (object)', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', title: 'Object', properties: { scripts: { type: 'object', properties: { search: { type: 'string', }, }, }, }, }); const content = 'scripts: test'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)('Incorrect type. Expected "object(Object)".', 0, 9, 0, 13, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: Object`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); }); describe('Array tests', () => { it('Type Array does not error on valid node', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { resolvers: { type: 'array', items: { type: 'string', }, }, }, }); const content = 'resolvers:\n - test\n - test\n - test'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Error on incorrect value type (array)', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { resolvers: { type: 'array', }, }, }); const content = 'resolvers: test'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.ArrayTypeError, 0, 11, 0, 15, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); }); describe('Anchor tests', () => { it('Anchor should not error', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { default: { type: 'object', properties: { name: { type: 'string', }, }, }, }, }); const content = 'default: &DEFAULT\n name: Anchor\nanchor_test:\n <<: *DEFAULT'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Anchor with multiple references should not error', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { default: { type: 'object', properties: { name: { type: 'string', }, }, }, }, }); const content = 'default: &DEFAULT\n name: Anchor\nanchor_test:\n <<: *DEFAULT\nanchor_test2:\n <<: *DEFAULT'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Multiple Anchor in array of references should not error', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { default: { type: 'object', properties: { name: { type: 'string', }, }, }, }, }); const content = 'default: &DEFAULT\n name: Anchor\ncustomname: &CUSTOMNAME\n custom_name: Anchor\nanchor_test:\n <<: [*DEFAULT, *CUSTOMNAME]'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Multiple Anchors being referenced in same level at same time for yaml 1.1', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { customize: { type: 'object', properties: { register: { type: 'string', }, }, }, }, }); const content = '%YAML 1.1\n---\ndefault: &DEFAULT\n name: Anchor\ncustomname: &CUSTOMNAME\n custom_name: Anchor\nanchor_test:\n <<: *DEFAULT\n <<: *CUSTOMNAME\n'; const result = await parseSetup(content); assert.strictEqual(result.length, 0); }); it('Multiple Anchors being referenced in same level at same time for yaml generate error for 1.2', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { customize: { type: 'object', properties: { register: { type: 'string', }, }, }, }, }); const content = 'default: &DEFAULT\n name: Anchor\ncustomname: &CUSTOMNAME\n custom_name: Anchor\nanchor_test:\n <<: *DEFAULT\n <<: *CUSTOMNAME\n'; const result = await parseSetup(content); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0], (0, verifyError_1.createExpectedError)('Map keys must be unique', 6, 2, 6, 18, vscode_languageserver_types_1.DiagnosticSeverity.Error)); }); it('Nested object anchors should expand properly', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', additionalProperties: { type: 'object', additionalProperties: false, properties: { akey: { type: 'string', }, }, required: ['akey'], }, }); const content = ` l1: &l1 akey: avalue l2: &l2 <<: *l1 l3: &l3 <<: *l2 l4: <<: *l3 `; const validator = await parseSetup(content); assert.strictEqual(validator.length, 0); }); it('Anchor reference with a validation error in a sub-object emits the error in the right location', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { src: {}, dest: { type: 'object', properties: { outer: { type: 'object', required: ['otherkey'], }, }, }, }, required: ['src', 'dest'], }); const content = ` src: &src outer: akey: avalue dest: <<: *src `; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); // The key thing we're checking is *where* the validation error gets reported. // "outer" isn't required to contain "otherkey" inside "src", but it is inside // "dest". Since "outer" doesn't appear inside "dest" because of the alias, we // need to move the error into "src". assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.MissingRequiredPropWarning.replace('{0}', 'otherkey'), 2, 10, 2, 15, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); it('Array Anchor merge', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { arr: { type: 'array', items: { type: 'number', }, }, obj: { properties: { arr2: { type: 'array', items: { type: 'string', }, }, }, }, }, }); const content = ` arr: &a - 1 - 2 obj: <<: *a arr2: - << *a `; const result = await parseSetup(content); assert.equal(result.length, 0); }); }); describe('Custom tag tests', () => { it('Custom Tags without type', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { analytics: { type: 'boolean', }, }, }); const content = 'analytics: !Test false'; const result = await parseSetup(content); assert.equal(result.length, 1); assert.deepStrictEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.BooleanTypeError, 0, 17, 0, 22, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }); it('Custom Tags with type', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { resolvers: { type: 'array', items: { type: 'string', }, }, }, }); const content = 'resolvers: !Ref\n - test'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Include with value should not error', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { customize: { type: 'string', }, }, }); const content = 'customize: !include customize.yaml'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Include without value should error', (done) => { const content = 'customize: !include'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createExpectedError)(errorMessages_1.IncludeWithoutValueError, 0, 11, 0, 19)); }) .then(done, done); }); }); describe('Multiple type tests', function () { it('Do not error when there are multiple types in schema and theyre valid', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { license: { type: ['string', 'boolean'], }, }, }); const content = 'license: MIT'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); describe('Invalid YAML errors', function () { it('Error when theres a finished untyped item', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, analytics: { type: 'boolean', }, }, }); const content = 'cwd: hello\nan'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createExpectedError)(errorMessages_1.BlockMappingEntryError, 1, 0, 1, 2)); }) .then(done, done); }); it('Error when theres no value for a node', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = 'cwd:'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.StringTypeError, 0, 4, 0, 4, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }) .then(done, done); }); }); describe('Test with no schemas', () => { it('Duplicate properties are reported', (done) => { const content = 'kind: a\ncwd: b\nkind: c'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 1); assert.deepEqual(result[0], (0, verifyError_1.createExpectedError)(errorMessages_1.DuplicateKeyError, 2, 0, 2, 7)); }) .then(done, done); }); }); describe('Test anchors', function () { it('Test that anchors with a schema do not report Property << is not allowed', (done) => { const schema = { type: 'object', properties: { sample: { type: 'object', properties: { prop1: { type: 'string', }, prop2: { type: 'string', }, }, additionalProperties: false, }, }, $schema: 'http://json-schema.org/draft-07/schema#', }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'test: &test\n prop1: hello\nsample:\n <<: *test\n prop2: another_test'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); describe('Test with custom kubernetes schemas', function () { it('Test that properties that match multiple enums get validated properly', (done) => { languageService.configure(languageSettingsSetup.withKubernetes().languageSettings); yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml']; const schema = { definitions: { ImageStreamImport: { type: 'object', properties: { kind: { type: 'string', enum: ['ImageStreamImport'], }, }, }, ImageStreamLayers: { type: 'object', properties: { kind: { type: 'string', enum: ['ImageStreamLayers'], }, }, }, }, oneOf: [ { $ref: '#/definitions/ImageStreamImport', }, { $ref: '#/definitions/ImageStreamLayers', }, ], }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'kind: '; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 2); // eslint-disable-next-line assert.equal(result[1].message, `Value is not accepted. Valid values: "ImageStreamImport", "ImageStreamLayers".`); }) .then(done, done); }); }); // https://github.com/redhat-developer/yaml-language-server/issues/118 describe('Null literals', () => { ['NULL', 'Null', 'null', '~', ''].forEach((content) => { it(`Test type null is parsed from [${content}]`, (done) => { const schema = { type: 'object', properties: { nulltest: { type: 'null', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const validator = parseSetup('nulltest: ' + content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); it('Test type null is working correctly in array', (done) => { const schema = { properties: { values: { type: 'array', items: { type: 'null', }, }, }, required: ['values'], }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'values: [Null, NULL, null, ~,]'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); describe('Multi Document schema validation tests', () => { it('Document does not error when --- is present with schema', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = '---\n# this is a test\ncwd: this'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); it('Multi Document does not error when --- is present with schema', (done) => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { cwd: { type: 'string', }, }, }); const content = '---\n# this is a test\ncwd: this...\n---\n# second comment\ncwd: hello\n...'; const validator = parseSetup(content); validator .then(function (result) { assert.equal(result.length, 0); }) .then(done, done); }); }); describe('Schema with title', () => { it('validator uses schema title instead of url', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', title: 'Schema Super title', properties: { analytics: { type: 'string', }, }, }); const content = 'analytics: 1'; const result = await parseSetup(content); (0, chai_1.expect)(result[0]).deep.equal((0, verifyError_1.createDiagnosticWithData)(errorMessages_1.StringTypeError, 0, 11, 0, 12, vscode_languageserver_types_1.DiagnosticSeverity.Error, 'yaml-schema: Schema Super title', 'file:///default_schema_id.yaml')); }); }); describe('Multiple schema for single file', () => { after(() => { // remove Kubernetes setting not to affect next tests languageService.configure(languageSettingsSetup.withKubernetes(false).languageSettings); yamlSettings.specificValidatorPaths = []; }); it('should add proper source to diagnostic', async () => { const content = ` abandoned: v1 archive: exclude: asd: asd`; languageService.configure(languageSettingsSetup.withKubernetes().languageSettings); yamlSettings.specificValidatorPaths = ['*.yml', '*.yaml']; const result = await parseSetup(content, 'file://~/Desktop/vscode-yaml/test.yml'); (0, chai_1.expect)(result[0]).deep.equal((0, verifyError_1.createDiagnosticWithData)(errorMessages_1.ArrayTypeError, 4, 10, 4, 18, vscode_languageserver_types_1.DiagnosticSeverity.Error, 'yaml-schema: Composer Package', 'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json')); }); it('should add proper source to diagnostic in case of drone', async () => { const content = ` apiVersion: v1 kind: Deployment `; const result = await parseSetup(content, 'file://~/Desktop/vscode-yaml/.drone.yml'); (0, chai_1.expect)(result[5]).deep.equal((0, verifyError_1.createDiagnosticWithData)((0, errorMessages_1.propertyIsNotAllowed)('apiVersion'), 1, 6, 1, 16, vscode_languageserver_types_1.DiagnosticSeverity.Error, 'yaml-schema: Drone CI configuration file', 'https://json.schemastore.org/drone', vscode_json_languageservice_1.ErrorCode.PropertyExpected, { properties: [ 'type', 'environment', 'steps', 'volumes', 'services', 'image_pull_secrets', 'node', 'concurrency', 'name', 'platform', 'workspace', 'clone', 'trigger', 'depends_on', ], })); }); }); describe('Conditional Schema', () => { it('validator use "then" block if "if" valid', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', default: [], properties: { name: { type: 'string', }, var: { type: 'string', }, }, if: { properties: { var: { type: 'string', }, }, }, then: { required: ['pineapple'], }, else: { required: ['tomato'], }, additionalProperties: true, }); const content = ` name: aName var: something inputs:`; const result = await parseSetup(content); (0, chai_1.expect)(result[0].message).to.eq('Missing property "pineapple".'); }); it('should add errorMessage from schema when the property is missing', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { icon: { type: 'string', }, }, required: ['title'], errorMessage: 'Custom message', }); const content = ''; const result = await parseSetup(content); (0, chai_1.expect)(result[0].message).to.eq('Custom message'); }); it('should add errorMessage from sub-schema when the property is missing', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { icon: { type: 'string', }, title: { type: 'string', }, }, anyOf: [ { required: ['title'], errorMessage: 'At least one of `title` or `icon` must be defined.', }, { required: ['icon'], }, ], }); const content = ''; const result = await parseSetup(content); (0, chai_1.expect)(result[0].message).to.eq('At least one of `title` or `icon` must be defined.'); }); describe('filePatternAssociation', () => { const schema = { type: 'object', properties: { name: { type: 'string', }, }, if: { filePatternAssociation: testHelper_1.SCHEMA_ID, }, then: { required: ['pineapple'], }, else: { required: ['tomato'], }, }; it('validator use "then" block if "if" match filePatternAssociation', async () => { schema.if.filePatternAssociation = testHelper_1.SCHEMA_ID; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'name: aName'; const result = await parseSetup(content); (0, chai_1.expect)(result.map((r) => r.message)).to.deep.eq(['Missing property "pineapple".']); }); it('validator use "then" block if "if" match filePatternAssociation - regexp', async () => { schema.if.filePatternAssociation = '*.yaml'; // SCHEMA_ID: "default_schema_id.yaml" schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'name: aName'; const result = await parseSetup(content); (0, chai_1.expect)(result.map((r) => r.message)).to.deep.eq(['Missing property "pineapple".']); }); it('validator use "else" block if "if" not match filePatternAssociation', async () => { schema.if.filePatternAssociation = 'wrong'; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = 'name: aName'; const result = await parseSetup(content); (0, chai_1.expect)(result.map((r) => r.message)).to.deep.eq(['Missing property "tomato".']); }); }); }); describe('Schema with uri-reference', () => { it('should validate multiple uri-references', async () => { const schemaWithURIReference = { type: 'object', properties: { one: { type: 'string', format: 'uri-reference', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schemaWithURIReference); let content = ` one: '//foo/bar' `; let result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); content = ` one: '#/components/schemas/service' `; result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); content = ` one: 'some/relative/path/foo.schema.yaml' `; result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); content = ` one: 'http://foo/bar' `; result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); }); it('should not validate empty uri-reference', async () => { const schemaWithURIReference = { type: 'object', properties: { one: { type: 'string', format: 'uri-reference', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schemaWithURIReference); const content = ` one: '' `; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(result[0].message).to.eq('String is not a URI: URI expected.'); }); }); describe('Multiple similar schemas validation', () => { const sharedSchemaId = 'sharedSchema.json'; before(() => { // remove Kubernetes setting set by previous test languageService.configure(languageSettingsSetup.withKubernetes(false).languageSettings); yamlSettings.specificValidatorPaths = []; }); afterEach(() => { schemaProvider.deleteSchema(testHelper_1.SCHEMA_ID); schemaProvider.deleteSchema(sharedSchemaId); }); it('should distinguish types in error "Incorrect type (Expected "type1 | type2 | type3")"', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///sharedSchema.json', schema.sharedSchema); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema.schema); const content = 'test_anyOf_objects:\n '; const result = await parseSetup(content); assert.strictEqual(result.length, 1); assert.strictEqual(result[0].message, 'Incorrect type. Expected "type1 | type2 | type3".'); assert.strictEqual(result[0].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); assert.deepStrictEqual(result[0].data.schemaUri, [ 'file:///sharedSchema.json', 'file:///default_schema_id.yaml', ]); }); it('should combine types in "Incorrect type error"', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///sharedSchema.json', schema.sharedSchema); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema.schema); const content = 'test_anyOf_objects:\n propA:'; const result = await parseSetup(content); assert.strictEqual(result.length, 3); assert.strictEqual(result[2].message, 'Incorrect type. Expected "string".'); assert.strictEqual(result[2].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); }); it('should combine const value', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///sharedSchema.json', schema.sharedSchema); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema.schema); const content = 'test_anyOf_objects:\n constA:'; const result = await parseSetup(content); assert.strictEqual(result.length, 4); assert.strictEqual(result[3].message, 'Value must be "constForType1" | "constForType3".'); assert.strictEqual(result[3].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); }); it('should distinguish types in error: "Missing property from multiple schemas"', async () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const schema = require(path.join(__dirname, './fixtures/testMultipleSimilarSchema.json')); schemaProvider.addSchemaWithUri(sharedSchemaId, 'file:///sharedSchema.json', schema.sharedSchema); schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema.schema); const content = 'test_anyOf_objects:\n someProp:'; const result = await parseSetup(content); assert.strictEqual(result.length, 3); assert.strictEqual(result[0].message, 'Missing property "objA".'); assert.strictEqual(result[0].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); assert.deepStrictEqual(result[0].data.schemaUri, [ 'file:///sharedSchema.json', 'file:///default_schema_id.yaml', ]); assert.strictEqual(result[1].message, 'Missing property "propA".'); assert.strictEqual(result[1].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); assert.deepStrictEqual(result[1].data.schemaUri, [ 'file:///sharedSchema.json', 'file:///default_schema_id.yaml', ]); assert.strictEqual(result[2].message, 'Missing property "constA".'); assert.strictEqual(result[2].source, 'yaml-schema: file:///sharedSchema.json | file:///default_schema_id.yaml'); assert.deepStrictEqual(result[2].data.schemaUri, [ 'file:///sharedSchema.json', 'file:///default_schema_id.yaml', ]); }); }); describe('Empty document validation', () => { it('should provide validation for empty document', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { scripts: { type: 'string', }, }, required: ['scripts'], }); const content = ''; const result = await parseSetup(content); assert.strictEqual(result.length, 1); assert.deepStrictEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.MissingRequiredPropWarning.replace('{0}', 'scripts'), 0, 0, 0, 0, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }); it('should provide validation for document which contains only whitespaces', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { scripts: { type: 'string', }, }, required: ['scripts'], }); const content = ' \n \n'; const result = await parseSetup(content); assert.deepStrictEqual(result[0], (0, verifyError_1.createDiagnosticWithData)(errorMessages_1.MissingRequiredPropWarning.replace('{0}', 'scripts'), 0, 0, 0, 1, vscode_languageserver_types_1.DiagnosticSeverity.Error, `yaml-schema: file:///${testHelper_1.SCHEMA_ID}`, `file:///${testHelper_1.SCHEMA_ID}`)); }); }); describe('Additional properties validation', () => { it('should allow additional props on object by default', async () => { const schema = { type: 'object', properties: { prop1: { type: 'string', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `prop2: you could be there 'prop2'`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); }); describe('Additional properties validation with enabled disableAdditionalProperties', () => { before(() => { languageSettingsSetup.languageSettings.disableAdditionalProperties = true; languageService.configure(languageSettingsSetup.languageSettings); }); after(() => { languageSettingsSetup.languageSettings.disableAdditionalProperties = false; }); it('should return additional prop error when there is extra prop', async () => { const schema = { type: 'object', properties: { prop1: { type: 'string', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `prop2: you should not be there 'prop2'`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(result[0].message).to.eq('Property prop2 is not allowed.'); (0, chai_1.expect)(result[0].data?.properties).to.deep.eq(['prop1']); }); it('should return additional prop error when there is unknown prop - suggest missing props)', async () => { const schema = { type: 'object', properties: { prop1: { type: 'string', }, prop2: { type: 'string', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `prop1: value1\npropX: you should not be there 'propX'`; const result = await parseSetup(content); (0, chai_1.expect)(result.map((r) => ({ message: r.message, properties: r.data?.properties, }))).to.deep.eq([ { message: 'Property propX is not allowed.', properties: ['prop2'], }, ]); }); it('should return error with possible props', async () => { const schema = { type: 'object', properties: { // prop0 is missing, should be added as a possible prop prop0: { type: 'string', }, // prop1 is already defined in the yaml prop1: { type: 'string', }, // prop2 is not suggested prop2: { type: 'string', doNotSuggest: true, }, // prop3 is deprecated prop3: { type: 'string', deprecationMessage: 'prop3 is deprecated', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `prop1: value1\npropX: you should not be there 'propX'`; const result = await parseSetup(content); (0, chai_1.expect)(result.map((r) => ({ message: r.message, properties: r.data?.properties, }))).to.deep.eq([ { message: 'Property propX is not allowed.', properties: ['prop0'], }, ]); }); it('should allow additional props on object when additionalProp is true on object', async () => { const schema = { type: 'object', properties: { prop1: { type: 'string', }, }, additionalProperties: true, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `prop2: you could be there 'prop2'`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); }); }); }); describe('Bug fixes', () => { describe('Base URI + $id resolution', () => { it('$id URI fragments in Draft-07 (plain-name fragment resolution)"', async () => { const schema = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'https://example.com/schema/root', type: 'object', properties: { phoneLocal: { $ref: '#Phone' }, phoneAbsolute: { $ref: 'https://example.com/schema/root#Phone' }, }, required: ['phoneLocal', 'phoneAbsolute'], $defs: { phoneDef: { $id: 'https://example.com/schema/root#Phone', type: 'string', pattern: '^[0-9]{3}-[0-9]{3}-[0-9]{4}$', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const invalid = `phoneLocal: "4165551234"\nphoneAbsolute: "4165551234"`; const result = await parseSetup(invalid); (0, chai_1.expect)(result).to.have.length(2); (0, chai_1.expect)(result[0].message).to.include('String does not match the pattern'); (0, chai_1.expect)(result[1].message).to.include('String does not match the pattern'); }); it('should resolve plain-name fragment via subschema $id (fragment form)', async () => { const schema = { $schema: 'http://json-schema.org/draft-07/schema#', definitions: { Name: { $id: '#Thing', type: 'string', minLength: 2, }, }, $ref: '#Thing', }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const result = await parseSetup('A'); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('String is shorter than the minimum length of 2.'); }); it('root $id updates base URI for resolving relative $ref targets', async () => { const other = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'http://example.com/other.json', definitions: { X: { $id: '#bar', type: 'string', minLength: 2, }, }, }; const root = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'http://example.com/root.json', type: 'object', properties: { x: { $ref: 'other.json#bar' }, }, required: ['x'], }; schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'http://example.com/other.json', other); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///root.schema.json', root); const yaml = `# yaml-language-server: $schema=file:///root.schema.json x: A `; const result = await parseSetup(yaml, 'file:///root.schema.json'); (0, chai_1.expect)(result.some((d) => /Problems loading reference/i.test(d.message))).to.eq(false); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('String is shorter than the minimum length of 2.'); }); it('subschema $id updates base URI for relative $ref inside that subschema', async () => { const nestedTarget = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'string', minLength: 2, }; const root = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'http://example.com/root.json', type: 'object', properties: { x: { $id: 'http://example.com/other/', $ref: 'sub.json', }, }, required: ['x'], }; schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'http://example.com/other/sub.json', nestedTarget); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///nested-base.schema.json', root); const yaml = `# yaml-language-server: $schema=file:///nested-base.schema.json x: A `; const result = await parseSetup(yaml, 'file:///nested-base.schema.json'); (0, chai_1.expect)(result.some((d) => /Problems loading reference/i.test(d.message))).to.eq(false); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('String is shorter than the minimum length of 2.'); }); it('should resolve embedded resource $id for relative $ref without external load', async () => { const root = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { x: { $ref: 'other.json#bar' }, }, required: ['x'], definitions: { B: { $id: 'other.json', definitions: { X: { $id: '#bar', type: 'string', minLength: 2, }, }, }, }, }; schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///embedded-resource.schema.json', root); const yaml = `# yaml-language-server: $schema=file:///embedded-resource.schema.json x: A `; const result = await parseSetup(yaml, 'file:///embedded-resource.schema.json'); (0, chai_1.expect)(result.some((d) => /Problems loading reference/i.test(d.message))).to.eq(false); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('String is shorter than the minimum length of 2.'); }); it('root $id can switch base scheme/host for resolution', async () => { const remotePackageSchema = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', }; const baseOk = { $schema: 'http://json-schema.org/draft-07/schema#', $id: 'https://www.schemastore.org/', type: 'object', properties: { pkg: { $ref: 'package.json' }, }, required: ['pkg'], }; schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'https://www.schemastore.org/package.json', remotePackageSchema); schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///baseuri-ok.schema.json', baseOk); const yaml = `# yaml-language-server: $schema=file:///baseuri-ok.schema.json pkg: 123 `; const result = await parseSetup(yaml, 'file:///baseuri-ok.schema.json'); (0, chai_1.expect)(result.some((d) => /Problems loading reference/i.test(d.message))).to.eq(false); (0, chai_1.expect)(result[0].message).to.include('Incorrect type.'); }); it('without root $id, relative $ref resolves against the retrieval URI and can fail', async () => { const baseFail = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { pkg: { $ref: 'package.json' }, }, required: ['pkg'], }; schemaProvider.addSchemaWithUri(testHelper_1.SCHEMA_ID, 'file:///baseuri-fail.schema.json', baseFail); const yaml = `# yaml-language-server: $schema=file:///baseuri-fail.schema.json pkg: 123 `; const result = await parseSetup(yaml, 'file:///baseuri-fail.schema.json'); (0, chai_1.expect)(result).to.not.be.empty; (0, chai_1.expect)(result.some((d) => /Problems loading reference/i.test(d.message) && /No content/i.test(d.message))).to.eq(true); }); }); it('Resolving $refs: should ignore sibling keywords next to $ref', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', properties: { value: { $ref: '#/definitions/A', type: 'number', }, }, definitions: { A: { type: 'string' }, }, }); (0, chai_1.expect)(await parseSetup(`value: hello`)).to.be.empty; const bad = await parseSetup(`value: 1`); (0, chai_1.expect)(bad[0].message).to.include('Incorrect type. Expected'); (0, chai_1.expect)(bad[0].message).to.include('string'); }); it('schema should validate additionalProp oneOf', async () => { const schema = { properties: { env: { $ref: 'https://json.schemastore.org/github-workflow.json#/definitions/env', }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `env: \${{ matrix.env1 }`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.be.not.empty; (0, chai_1.expect)(telemetry.messages).to.be.empty; (0, chai_1.expect)(result.length).to.eq(1); assert.deepStrictEqual(result[0].message, 'String does not match the pattern of "^.*\\$\\{\\{(.|[\r\n])*\\}\\}.*$".'); }); it('schema should validate ipv4 format - Negative Case', async () => { const schema = { type: 'array', items: { type: 'string', format: 'ipv4', }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `- 10.15.12.500`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.be.not.empty; (0, chai_1.expect)(telemetry.messages).to.be.empty; (0, chai_1.expect)(result.length).to.eq(1); assert.deepStrictEqual(result[0].message, 'String does not match IPv4 format.'); }); it('schema should validate ipv4 format - Positive Case', async () => { const schema = { type: 'array', items: { type: 'string', format: 'ipv4', }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `- 255.255.255.255`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.be.empty; (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('schema should validate ipv6 format - Negative Case', async () => { const schema = { type: 'array', items: { type: 'string', format: 'ipv6', }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `- 10.15.12.500`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.be.not.empty; (0, chai_1.expect)(telemetry.messages).to.be.empty; (0, chai_1.expect)(result.length).to.eq(1); assert.deepStrictEqual(result[0].message, 'String does not match IPv6 format.'); }); it('schema should validate ipv6 format - Positive Case', async () => { const schema = { type: 'array', items: { type: 'string', format: 'ipv6', }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `- 2001:0db8:85a3:0000:0000:8a2e:0370:7334\n- 2001:0db8:85a3:0000:0000:8a2e:0370:7334\n- FEDC:BA98:7654:3210:FEDC:BA98:7654:3210\n- 1080::8:800:200C:417A\n- FF01::101\n- ::1`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.be.empty; (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('should handle bad schema refs', async () => { const schema = { type: 'object', properties: { bar: { oneOf: ['array', 'boolean'], }, }, additionalProperties: true, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `bar: ddd`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('should not use same AST for completion and validation', async () => { const schema = { type: 'object', properties: { container: { type: 'object', properties: { image: { type: 'string', }, command: { type: 'array', items: { type: 'string', }, }, }, }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `container: image: alpine command: - aaa - bbb - dddddd - ccc`; const testTextDocument = (0, testHelper_1.setupSchemaIDTextDocument)(content); yamlSettings.documents = new yamlSettings_1.TextDocumentTestManager(); yamlSettings.documents.set(testTextDocument); await languageService.doComplete(testTextDocument, vscode_languageserver_types_1.Position.create(6, 8), false); const result = await validationHandler.validateTextDocument(testTextDocument); (0, chai_1.expect)(result).to.be.empty; }); }); describe('Enum tests', () => { afterEach(() => { schemaProvider.deleteSchema(testHelper_1.SCHEMA_ID); }); it('Enum Validation with invalid enum value', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { first: { type: 'string', enum: ['a', 'b'], }, second: { type: 'number', enum: [1, 2], }, }, }); const content = 'first: c\nsecond: 3'; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(2); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('Enum Validation with invalid type', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { first: { type: 'string', enum: ['a', 'b'], }, second: { type: 'number', enum: [1, 2], }, }, }); const content = 'first: c\nsecond: a'; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(3); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('Enum Validation with invalid data', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { definitions: { rule: { description: 'A rule', type: 'object', properties: { kind: { description: 'The kind of rule', type: 'string', enum: ['tested'], }, }, required: ['kind'], additionalProperties: false, }, }, properties: { rules: { description: 'Rule list', type: 'array', items: { $ref: '#/definitions/rule', }, minProperties: 1, additionalProperties: false, }, }, }); const content = 'rules:\n - kind: test'; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(result[0].message).to.eq('Value is not accepted. Valid values: "tested".'); }); it('value matches more than one schema in oneOf - but among one is format matches', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { repository: { oneOf: [ { type: 'string', format: 'uri', }, { type: 'string', pattern: '^@', }, ], }, }, }); const content = `repository: '@bittrr'`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); it('value matches more than one schema in oneOf', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { foo: {}, bar: {}, }, oneOf: [ { required: ['foo'], }, { required: ['bar'], }, ], }); const content = `foo: bar\nbar: baz`; const result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(result[0].message).to.eq('Matches multiple schemas when only one must validate.'); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); }); describe('Schema meta-validation', () => { it('should handle not valid schema object', async () => { const schema = 'Foo'; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `foo: bar`; const result = await parseSetup(content); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('default_schema_id.yaml'); (0, chai_1.expect)(result[0].message).to.include('is not valid:'); (0, chai_1.expect)(result[0].message).to.include('expected a JSON Schema object or boolean, got'); (0, chai_1.expect)(result[0].message).to.include('string'); (0, chai_1.expect)(telemetry.messages).to.be.empty; }); const content = '6'; it('draft-04: exclusiveMinimum must be boolean', async () => { const schema = { $schema: 'http://json-schema.org/draft-04/schema#', type: 'number', minimum: 5, exclusiveMinimum: 5, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const result = await parseSetup(content); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('default_schema_id.yaml'); (0, chai_1.expect)(result[0].message).to.include('is not valid:'); (0, chai_1.expect)(result[0].message).to.include('exclusiveMinimum'); }); it('draft-07: exclusiveMinimum must be number', async () => { const schema = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'number', minimum: 5, exclusiveMinimum: true, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const result = await parseSetup(content); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('default_schema_id.yaml'); (0, chai_1.expect)(result[0].message).to.include('is not valid:'); (0, chai_1.expect)(result[0].message).to.include('exclusiveMinimum'); }); it('draft-2019-09: should handle invalid type in $defs', async () => { const schema = { $schema: 'https://json-schema.org/draft/2019-09/schema', $defs: { foo: { type: 'object', properties: { bar: { type: ['foo', 'bar'], }, }, }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const result = await parseSetup(content); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('default_schema_id.yaml'); (0, chai_1.expect)(result[0].message).to.include('is not valid:'); (0, chai_1.expect)(result[0].message).to.include('$defs'); }); it('draft-2020-12: prefixItems must be an array', async () => { const schema = { $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'number', prefixItems: 'foo', }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const result = await parseSetup(content); (0, chai_1.expect)(result).to.have.length(1); (0, chai_1.expect)(result[0].message).to.include('default_schema_id.yaml'); (0, chai_1.expect)(result[0].message).to.include('is not valid:'); (0, chai_1.expect)(result[0].message).to.include('prefixItems'); }); }); it('Nested AnyOf const should correctly evaluate and merge problems', async () => { // note that 'missing form property' is necessary to trigger the bug (there has to be some problem in both subSchemas) // order of the object in `anyOf` is also important const schema = { type: 'object', properties: { options: { anyOf: [ { type: 'object', properties: { form: { type: 'string', }, provider: { type: 'string', const: 'test1', }, }, required: ['form', 'provider'], }, { type: 'object', properties: { form: { type: 'string', }, provider: { anyOf: [ { type: 'string', const: 'testX', }, ], }, }, required: ['form', 'provider'], }, ], }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `options:\n provider: testX`; const result = await parseSetup(content); assert.deepEqual(result.map((e) => e.message), ['Missing property "form".'] // not inclide provider error ); }); it('URL-encoded characters in $ref', async () => { // note that 'missing form property' is necessary to trigger the bug (there has to be some problem in both subSchemas) // order of the object in `anyOf` is also important const schema = { type: 'object', properties: { myProperty: { $ref: '#/definitions/Interface%3Ctype%3E', }, }, definitions: { 'Interface': { type: 'object', properties: { foo: { type: 'string', }, }, }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `myProperty:\n foo: bar`; const result = await parseSetup(content); assert.equal(result.length, 0); }); it('value should match as per schema const on boolean', async () => { schemaProvider.addSchema(testHelper_1.SCHEMA_ID, { type: 'object', properties: { prop: { const: true, type: 'boolean', }, }, }); let content = `prop: false`; let result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(1); (0, chai_1.expect)(result[0].message).to.eq('Value must be true.'); content = `prop: true`; result = await parseSetup(content); (0, chai_1.expect)(result.length).to.eq(0); }); it('draft-04 schema', async () => { const schema = { $schema: 'http://json-schema.org/draft-04/schema#', type: 'object', properties: { myProperty: { $ref: '#/definitions/Interface%3Ctype%3E', }, }, definitions: { 'Interface': { type: 'object', properties: { foo: { type: 'string', }, multipleOf: { type: 'number', minimum: 0, exclusiveMinimum: true, }, }, }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `myProperty:\n foo: bar\n multipleOf: 1`; const result = await parseSetup(content); assert.equal(result.length, 0); }); it('draft-04 schema with https in metaschema URI', async () => { const schema = { $schema: 'https://json-schema.org/draft-04/schema#', type: 'object', properties: { myProperty: { $ref: '#/definitions/Interface%3Ctype%3E', }, }, definitions: { 'Interface': { type: 'object', properties: { foo: { type: 'string', }, multipleOf: { type: 'number', minimum: 0, exclusiveMinimum: true, }, }, }, }, }; schemaProvider.addSchema(testHelper_1.SCHEMA_ID, schema); const content = `myProperty:\n foo: bar\n multipleOf: 1`; const result = await parseSetup(content); assert.equal(result.length, 0); }); }); //# sourceMappingURL=schemaValidation.test.js.map