mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-10 19:52:53 +00:00
236 lines
7.2 KiB
TypeScript
236 lines
7.2 KiB
TypeScript
/*
|
|
* Copyright (C) 2018-2019 StApps
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the Free
|
|
* Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import {asyncPool} from 'async-pool-native/dist/async-pool';
|
|
import {PathLike} from 'fs';
|
|
import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema';
|
|
import * as mustache from 'mustache';
|
|
import {basename, join, resolve} from 'path';
|
|
import {
|
|
ExpectableValidationErrors,
|
|
globPromisified,
|
|
logger,
|
|
readFilePromisified,
|
|
writeFilePromisified,
|
|
} from './common';
|
|
|
|
/**
|
|
* StAppsCore validator
|
|
*/
|
|
export class Validator {
|
|
/**
|
|
* Map of schema names to schemas
|
|
*/
|
|
private readonly schemas: { [type: string]: Schema } = {};
|
|
|
|
/**
|
|
* JSONSchema validator instance
|
|
*/
|
|
private readonly validator: JSONSchemaValidator;
|
|
|
|
/**
|
|
* Create a new validator
|
|
*/
|
|
constructor() {
|
|
this.validator = new JSONSchemaValidator();
|
|
}
|
|
|
|
/**
|
|
* Feed the schema files to the validator
|
|
*
|
|
* @param schemaDir Path to directory that contains schema files
|
|
*/
|
|
public async addSchemas(schemaDir: PathLike): Promise<string[]> {
|
|
const schemaFiles = await globPromisified(join(schemaDir.toString(), '*.json'));
|
|
|
|
if (schemaFiles.length === 0) {
|
|
throw new Error(`No schema files in ${schemaDir.toString()}!`);
|
|
}
|
|
|
|
logger.log(`Adding schemas from ${schemaDir} to validator.`);
|
|
|
|
// Iterate over schema files
|
|
await asyncPool(2, schemaFiles, async (file) => {
|
|
// read schema file
|
|
const buffer = await readFilePromisified(file);
|
|
const schema = JSON.parse(buffer.toString());
|
|
|
|
// add schema to validator
|
|
this.validator.addSchema(schema);
|
|
|
|
// add schema to map
|
|
this.schemas[basename(file, '.json')] = schema;
|
|
|
|
logger.info(`Added ${file} to validator.`);
|
|
});
|
|
|
|
return schemaFiles;
|
|
}
|
|
|
|
/**
|
|
* Validates anything against a given schema name
|
|
*
|
|
* @param instance Instance to validate
|
|
* @param schema Name of schema to validate instance against or the schema itself
|
|
*/
|
|
public validate(instance: any, schema: string | Schema): ValidatorResult {
|
|
if (typeof schema === 'string') {
|
|
// if you want to access a schema that is contained in the validator object
|
|
if (typeof this.schemas[schema] !== 'object') {
|
|
throw new Error(`No schema available for ${schema}.`);
|
|
}
|
|
|
|
return this.validator.validate(instance, this.schemas[schema]);
|
|
} else {
|
|
// if you have a schema and want to validate it directly
|
|
return this.validator.validate(instance, schema);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate all test files in the given resources directory against schema files in the given (schema) directory
|
|
*
|
|
* @param schemaDir The directory where the JSON schema files are
|
|
* @param resourcesDir The directory where the test files are
|
|
*/
|
|
export async function validateFiles(schemaDir: string, resourcesDir: string): Promise<ExpectableValidationErrors> {
|
|
// instantiate new validator
|
|
const v = new Validator();
|
|
await v.addSchemas(schemaDir);
|
|
|
|
// get list of files to test
|
|
const testFiles = await globPromisified(join(resourcesDir, '*.json'));
|
|
|
|
if (testFiles.length === 0) {
|
|
throw new Error(`No test files in ${resourcesDir}!`);
|
|
}
|
|
|
|
logger.log(`Found ${testFiles.length} file(s) to test.`);
|
|
|
|
// map of errors per file
|
|
const errors: ExpectableValidationErrors = {};
|
|
|
|
// iterate over files to test
|
|
await asyncPool(2, testFiles, async (testFile) => {
|
|
const testFileName = basename(testFile);
|
|
|
|
const buffer = await readFilePromisified(join(resourcesDir, testFileName));
|
|
|
|
// read test description from file
|
|
const testDescription = JSON.parse(buffer.toString());
|
|
|
|
// validate instance from test description
|
|
const result = v.validate(testDescription.instance, testDescription.schema);
|
|
|
|
// list of expected errors for this test description
|
|
const expectedErrors: string[] = [];
|
|
expectedErrors.push.apply(expectedErrors, testDescription.errorNames);
|
|
|
|
// number of unexpected errors
|
|
let unexpectedErrors = 0;
|
|
|
|
if (result.errors.length > 0) {
|
|
errors[testFileName] = [];
|
|
|
|
// iterate over errors
|
|
result.errors.forEach((error) => {
|
|
// get idx of expected error
|
|
const errorIdx = expectedErrors.indexOf(error.name);
|
|
let expected = false;
|
|
|
|
if (errorIdx >= 0) {
|
|
// remove from list of expected errors
|
|
expectedErrors.splice(errorIdx, 1);
|
|
expected = true;
|
|
} else {
|
|
unexpectedErrors++;
|
|
logger.error(`Unexpected error ${error.name} in ${testFile}`);
|
|
}
|
|
|
|
// add error to list of errors
|
|
errors[testFileName].push({
|
|
...error,
|
|
expected,
|
|
});
|
|
});
|
|
}
|
|
|
|
if (expectedErrors.length > 0) {
|
|
expectedErrors.forEach((error) => {
|
|
logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
|
|
errors[testFileName].push({
|
|
argument: false,
|
|
expected: false,
|
|
instance: testDescription.instance,
|
|
message: `expected error ${error} did not occur`,
|
|
name: `expected ${error}`,
|
|
property: 'unknown',
|
|
schema: undefined as any,
|
|
});
|
|
});
|
|
} else if (unexpectedErrors === 0) {
|
|
logger.info(`Successfully validated ${testFile}.`);
|
|
}
|
|
});
|
|
|
|
return errors;
|
|
}
|
|
|
|
/**
|
|
* Write a report for errors that occurred in validation
|
|
*
|
|
* @param reportPath Path to write report to
|
|
* @param errors Errors that occurred in validation
|
|
*/
|
|
export async function writeReport(reportPath: PathLike, errors: ExpectableValidationErrors): Promise<void> {
|
|
let buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'file.html.mustache'));
|
|
const fileTemplate = buffer.toString();
|
|
|
|
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'error.html.mustache'));
|
|
const errorTemplate = buffer.toString();
|
|
|
|
let output = '';
|
|
|
|
Object.keys(errors).forEach((fileName) => {
|
|
let fileOutput = '';
|
|
|
|
errors[fileName].forEach((error, idx) => {
|
|
fileOutput += mustache.render(errorTemplate, {
|
|
idx: idx + 1,
|
|
instance: JSON.stringify(error.instance, null, 2),
|
|
message: error.message,
|
|
property: error.property,
|
|
schema: JSON.stringify(error.schema, null, 2),
|
|
status: (error.expected) ? 'alert-success' : 'alert-danger',
|
|
});
|
|
});
|
|
|
|
output += mustache.render(fileTemplate, {
|
|
errors: fileOutput,
|
|
testFile: fileName,
|
|
});
|
|
});
|
|
|
|
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache'));
|
|
const reportTemplate = buffer.toString();
|
|
|
|
await writeFilePromisified(reportPath, mustache.render(reportTemplate, {
|
|
report: output,
|
|
timestamp: (new Date()).toISOString(),
|
|
}));
|
|
|
|
logger.ok(`Wrote report to ${reportPath}.`);
|
|
}
|