212 строки
7.4 KiB
TypeScript
212 строки
7.4 KiB
TypeScript
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
import Ajv from 'ajv';
|
|
import * as url from 'url';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { readFile } from 'fs/promises';
|
|
import { getLanguageService } from 'vscode-json-languageservice';
|
|
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
import draft4MetaSchema from 'ajv/lib/refs/json-schema-draft-04.json';
|
|
import { findCycle } from '../src/cycleCheck';
|
|
|
|
const schemasFolder = path.join(__dirname, '../../schemas/');
|
|
const testSchemasFolder = path.join(__dirname, '../testSchemas/');
|
|
const templateTestsFolder = path.join(__dirname, '../templateTests/');
|
|
const armSchemasPrefix = /^https?:\/\/schema\.management\.azure\.com\/schemas\//
|
|
const jsonSchemaDraft4Prefix = /^https?:\/\/json-schema\.org\/draft-04\/schema/
|
|
|
|
const ajvInstance = new Ajv({
|
|
loadSchema: loadSchema,
|
|
strictDefaults: true,
|
|
schemaId: 'id',
|
|
meta: true,
|
|
}).addMetaSchema(draft4MetaSchema)
|
|
.addFormat('int32', /.*/)
|
|
.addFormat('duration', /.*/)
|
|
.addFormat('password', /.*/);
|
|
|
|
async function loadRawSchema(uri: string): Promise<string> {
|
|
const hashIndex = uri.indexOf("#");
|
|
if (hashIndex !== -1) {
|
|
uri = uri.substring(0, hashIndex);
|
|
}
|
|
|
|
let jsonPath: string;
|
|
if (uri.match(armSchemasPrefix)) {
|
|
jsonPath = uri.replace(armSchemasPrefix, schemasFolder);
|
|
}
|
|
else if (uri.match(jsonSchemaDraft4Prefix)) {
|
|
return JSON.stringify(draft4MetaSchema);
|
|
}
|
|
else {
|
|
jsonPath = uri;
|
|
}
|
|
|
|
if (jsonPath.startsWith("http:") || jsonPath.startsWith("https:")) {
|
|
throw new Error(`Unsupported JSON path ${jsonPath}`);
|
|
}
|
|
|
|
return await readFile(jsonPath, { encoding: "utf8" });
|
|
}
|
|
|
|
async function loadSchema(uri: string): Promise<object> {
|
|
const rawSchema = await loadRawSchema(uri);
|
|
|
|
return JSON.parse(rawSchema);
|
|
}
|
|
|
|
function listSchemaPaths(basePath: string): string[] {
|
|
let results: string[] = [];
|
|
|
|
for (const subPathName of fs.readdirSync(basePath)) {
|
|
const subPath = path.resolve(`${basePath}/${subPathName}`);
|
|
|
|
const fileStat = fs.statSync(subPath);
|
|
if (fileStat.isDirectory()) {
|
|
const pathResults = listSchemaPaths(subPath);
|
|
results = results.concat(pathResults);
|
|
continue;
|
|
}
|
|
|
|
if (!fileStat.isFile() || path.extname(subPath).toLowerCase() !== '.json') {
|
|
continue;
|
|
}
|
|
|
|
results.push(subPath);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
const metaSchemaPaths = [
|
|
'http://json-schema.org/draft-04/schema',
|
|
testSchemasFolder + 'ResourceMetaSchema.json',
|
|
];
|
|
|
|
// Cyclic schemas cause an issue for ARM export, but we have a few already
|
|
// 'known' bad schemas. Please do not add to this list unless you are sure
|
|
// this will not cause a problem in ARM.
|
|
const schemasToSkipForCyclicValidation = new Set([
|
|
'2017-09-01-preview/Microsoft.DataFactory.json',
|
|
'2018-06-01/Microsoft.DataFactory.json',
|
|
'2018-07-01/Microsoft.Media.json',
|
|
'2018-11-01-preview/Microsoft.Billing.json',
|
|
].map(p => path.resolve(`${schemasFolder}/${p}`)));
|
|
|
|
const schemasToSkip = [
|
|
'0.0.1-preview/CreateUIDefinition.CommonControl.json',
|
|
'0.0.1-preview/CreateUIDefinition.MultiVm.json',
|
|
'0.0.1-preview/CreateUIDefinition.ProviderControl.json',
|
|
'0.1.0-preview/CreateUIDefinition.CommonControl.json',
|
|
'0.1.0-preview/CreateUIDefinition.MultiVm.json',
|
|
'0.1.0-preview/CreateUIDefinition.ProviderControl.json',
|
|
'0.1.1-preview/CreateUIDefinition.CommonControl.json',
|
|
'0.1.1-preview/CreateUIDefinition.MultiVm.json',
|
|
'0.1.1-preview/CreateUIDefinition.ProviderControl.json',
|
|
'0.1.2-preview/CreateUIDefinition.CommonControl.json',
|
|
'0.1.2-preview/CreateUIDefinition.MultiVm.json',
|
|
'0.1.2-preview/CreateUIDefinition.ProviderControl.json',
|
|
'2014-04-01-preview/deploymentParameters.json',
|
|
'2014-04-01-preview/deploymentTemplate.json',
|
|
'2015-01-01/deploymentParameters.json',
|
|
'2015-01-01/deploymentTemplate.json',
|
|
'2015-10-01-preview/policyDefinition.json',
|
|
'2016-12-01/policyDefinition.json',
|
|
'2018-05-01/policyDefinition.json',
|
|
'2019-01-01/policyDefinition.json',
|
|
'2019-06-01/policyDefinition.json',
|
|
'2019-09-01/policyDefinition.json',
|
|
'2020-09-01/policyDefinition.json',
|
|
'2020-10-01/policyDefinition.json',
|
|
'2018-05-01/subscriptionDeploymentParameters.json',
|
|
'2018-05-01/subscriptionDeploymentTemplate.json',
|
|
'2019-04-01/deploymentParameters.json',
|
|
'2019-04-01/deploymentTemplate.json',
|
|
'2019-03-01-hybrid/deploymentTemplate.json',
|
|
'2019-03-01-hybrid/deploymentParameters.json',
|
|
'2019-08-01/managementGroupDeploymentParameters.json',
|
|
'2019-08-01/managementGroupDeploymentTemplate.json',
|
|
'2019-08-01/tenantDeploymentParameters.json',
|
|
'2019-08-01/tenantDeploymentTemplate.json',
|
|
'2021-09-09/uiFormDefinition.schema.json',
|
|
'common/definitions.json',
|
|
'common/manuallyAddedResources.json',
|
|
'common/autogeneratedResources.json',
|
|
'viewdefinition/0.0.1-preview/ViewDefinition.json',
|
|
].map(p => path.resolve(`${schemasFolder}/${p}`));
|
|
|
|
const schemaPaths = listSchemaPaths(schemasFolder).filter(path => schemasToSkip.indexOf(path) == -1);
|
|
const templateTestPaths = listSchemaPaths(templateTestsFolder);
|
|
const TIMEOUT_1_MINUTE = 60000;
|
|
|
|
describe('Validate individual resource schemas', () => {
|
|
it(`can be parsed with JSON.parse`, async function () {
|
|
for (const schemaPath of schemaPaths) {
|
|
const schema = await loadRawSchema(schemaPath);
|
|
|
|
expect(() => JSON.parse(schema)).not.toThrow();
|
|
}
|
|
}, TIMEOUT_1_MINUTE);
|
|
|
|
for (const metaSchemaPath of metaSchemaPaths) {
|
|
it(`validates against '${metaSchemaPath}'`, async function () {
|
|
for (const schemaPath of schemaPaths) {
|
|
const schema = await loadSchema(schemaPath);
|
|
const metaSchema = await loadSchema(metaSchemaPath);
|
|
|
|
const validate = await ajvInstance.compileAsync(metaSchema);
|
|
const result = await validate(schema);
|
|
|
|
if (!result) {
|
|
console.error(`Validating ${schemaPath} failed with errors ${JSON.stringify(validate.errors, null, 2)}`);
|
|
}
|
|
expect(result).toBeTruthy();
|
|
}
|
|
}, TIMEOUT_1_MINUTE);
|
|
}
|
|
|
|
it(`can be compiled`, async function () {
|
|
for (const schemaPath of schemaPaths) {
|
|
const schema = await loadSchema(schemaPath);
|
|
|
|
expect(() => ajvInstance.compile(schema)).not.toThrow();
|
|
}
|
|
}, TIMEOUT_1_MINUTE);
|
|
|
|
it(`does not contain any cycles`, async function () {
|
|
|
|
for (const schemaPath of schemaPaths) {
|
|
if (!schemasToSkipForCyclicValidation.has(schemaPath)) {
|
|
const schema = await loadSchema(schemaPath);
|
|
|
|
const cycle = findCycle(schema);
|
|
|
|
if (cycle) {
|
|
console.error(`Found ${schemaPath} cycle ${cycle?.join(' -> ')}`);
|
|
}
|
|
expect(cycle).toBeUndefined();
|
|
}
|
|
}
|
|
}, TIMEOUT_1_MINUTE);
|
|
});
|
|
|
|
describe('Validate test templates against VSCode language service', () => {
|
|
for (const templateTestFile of templateTestPaths) {
|
|
it(`running schema validation on '${templateTestFile}'`, async function () {
|
|
const service = getLanguageService({
|
|
schemaRequestService: loadRawSchema,
|
|
workspaceContext: {
|
|
resolveRelativePath: (relativePath, resource) => url.resolve(resource, relativePath)
|
|
},
|
|
});
|
|
|
|
const content = await readFile(templateTestFile, { encoding: 'utf8' });
|
|
const textDocument = TextDocument.create(templateTestFile, 'json', 0, content);
|
|
const jsonDocument = service.parseJSONDocument(textDocument);
|
|
|
|
const result = await service.doValidation(textDocument, jsonDocument);
|
|
expect(result).toEqual([]);
|
|
}, TIMEOUT_1_MINUTE);
|
|
}
|
|
}); |