just-api
Version:
Specification based API test framework for HTTP APIs (REST, GraphQL)
573 lines (480 loc) • 22 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _events = require('events');
var _events2 = _interopRequireDefault(_events);
var _loader = require('./schema/yaml/loader');
var _validator = require('./schema/validator');
var _utils = require('./utils');
var _spec = require('./spec');
var _spec2 = _interopRequireDefault(_spec);
var _suiteDependency = require('./suite-dependency');
var _suiteDependency2 = _interopRequireDefault(_suiteDependency);
var _errors = require('./errors');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const url = require('url');
let InvalidYAMLSuiteSchemaError = (0, _errors.customError)('InvalidYAMLSuiteSchemaError');
let DisabledSuiteError = (0, _errors.customError)('DisabledSuiteError');
let InvalidSuiteConfigurationError = (0, _errors.customError)('InvalidSuiteConfigurationError');
let NoSpecsFoundError = (0, _errors.customError)('NoSpecsFoundError');
let NoSpecFoundMatchingNameError = (0, _errors.customError)('NoSpecFoundMatchingNameError');
let InvalidSpecificationSchemaError = (0, _errors.customError)('InvalidSpecificationSchemaError');
class Suite extends _events2.default {
constructor(filePath, options) {
super();
this.file = filePath;
this.userContext = {};
this.targetConfiguration = {};
this.commonHeaders = {};
this.grep = options.grep;
this.specDependencies = [];
this.result = {};
this.result.specs = [];
this.result.status = null;
this.requests = [];
this.status = null;
}
async launch() {
try {
this.loadFileAndValidateSchema();
} catch (err) {
if (err.name === 'DisabledSuiteError') {
this.status = 'skip';
this.emit('end', this);
} else {
this.status = 'fail';
this.emit('end', this, err);
}
return;
}
try {
await this.loadSpecDependencies();
} catch (err) {
let LoadingSpecDependencySuiteError = (0, _errors.customError)('LoadingSpecDependencySuiteError');
let error = new LoadingSpecDependencySuiteError(`${err.name} error occurred while loading dependencies \n ${err.message}`);
this.status = 'fail';
this.emit('end', this, error);
return;
}
try {
await this.configure();
} catch (err) {
if (err.name === 'InvalidSuiteConfigurationError' || err.name === 'InvalidYAMLSuiteSchemaError') {
this.status = 'fail';
this.emit('end', this, err);
} else {
let SuiteConfigurationFailedError = (0, _errors.customError)('SuiteConfigurationFailedError');
let error = new SuiteConfigurationFailedError(`error occurred while configuring the suite '${this.file}' \n ${err.message}`);
this.status = 'fail';
this.emit('end', this, error);
}
return;
}
try {
await this.run();
} catch (error) {
return;
}
}
loadFileAndValidateSchema() {
const data = this.loadFile(this.file);
if (!data.meta) {
throw new InvalidYAMLSuiteSchemaError(`Metadata is not specified in suite '${this.file}'`);
} else {
const suiteFlag = typeof data.meta.enabled === 'undefined' || data.meta.enabled === true;
if (!suiteFlag) {
throw new DisabledSuiteError(`Suite '${this.file}' is disabled, skipping the suite`);
}
if (data.meta.locate_files_relative === true) {
this.areFilesRelativeToSuite = true;
}
}
const suiteSchemaDefinition = _path2.default.resolve(__dirname, './schema/yaml/suite.json');
const schemaValidation = (0, _validator.validateJSONSchema)(data, suiteSchemaDefinition);
if (schemaValidation.errors.length >= 1) {
let errorMessages = '';
for (let error of schemaValidation.errors) {
errorMessages += ` property - ${error.property}, message: ${error.message} \n`;
}
const InvalidSuiteSchemaError = (0, _errors.customError)('InvalidSuiteSchemaError');
throw new InvalidSuiteSchemaError(`invalid schema found in file ${this.file} \n ${errorMessages}`);
}
this.data = data;
this.hooks = this.data.hooks || {};
this.specs = this.data.specs;
this.name = this.data.meta.name;
}
loadFile(file) {
if (!_fs2.default.existsSync(file)) {
let FileDoesNotExistError = (0, _errors.customError)('FileDoesNotExist');
throw new FileDoesNotExistError(`Test suite file doesn't exist at '${file}'`);
}
try {
return (0, _loader.loadYAML)(file, { encoding: 'UTF-8', customTypes: ['asyncFunction'] });
} catch (err) {
let YAMLSuiteLoadingError = (0, _errors.customError)('YAMLSuiteLoadingError');
throw new YAMLSuiteLoadingError(`(${file}) \n ${err.message}`);
}
}
resolveFile(filePath) {
if (this.areFilesRelativeToSuite) {
let suiteDirectory = _path2.default.dirname(this.file);
return _path2.default.resolve(suiteDirectory, filePath);
} else {
return _path2.default.resolve(process.cwd(), filePath);
}
}
async loadSpecDependencies() {
if (this.data.spec_dependencies) {
for (let dependency of this.data.spec_dependencies) {
let file = this.resolveFile(dependency);
try {
let fileExistsAndFile = _fs2.default.lstatSync(file).isFile();
if (fileExistsAndFile) {
let dependencySuite = new _suiteDependency2.default(file, this);
dependencySuite.loadFileAndValidateSchema();
await dependencySuite.configure();
this.specDependencies.push(dependencySuite);
} else {
throw new Error(`dependency suite at '${file}' is not a file, Provide a valid file path`);
}
} catch (e) {
if (e.code == 'ENOENT') {
throw new Error(`dependency suite file at '${file}' does not exist, Provide a valid path`);
} else {
throw e;
}
}
}
}
}
async configure() {
const suiteConfigData = this.data.configuration;
let SuiteCustomConfigurationError = (0, _errors.customError)('SuiteCustomConfigurationError');
if (suiteConfigData.custom_configuration) {
let staticSuiteConfiguration = {
host: suiteConfigData.host,
port: suiteConfigData.port,
scheme: suiteConfigData.scheme,
base_path: suiteConfigData.base_path,
read_timeout: suiteConfigData.read_timeout
};
const configContext = {};
try {
if (suiteConfigData.custom_configuration.run_type === 'inline') {
let inlineFunction = suiteConfigData.custom_configuration.inline.function;
await (0, _utils.runInlineFunction)(inlineFunction, configContext);
} else if (suiteConfigData.custom_configuration.run_type === 'module') {
let modulePath = this.resolveFile(suiteConfigData.custom_configuration.module.module_path);
const module = (0, _utils.assertFileValidity)(modulePath, 'Custom Configuration module');
const customModule = (0, _utils.loadModule)(module);
await (0, _utils.runModuleFunction)(customModule, suiteConfigData.custom_configuration.module.function_name, configContext);
} else {
throw new InvalidYAMLSuiteSchemaError(`suite custom_configuration.run_type should be either inline or module`);
}
} catch (err) {
if (err.name === 'InvalidYAMLSuiteSchemaError') {
throw err;
} else {
throw new SuiteCustomConfigurationError(`${err.name || 'Error'} occurred while running the custom configuration function \n ${err.message || err}`);
}
}
let configuration = Object.assign({}, staticSuiteConfiguration, configContext);
this.scheme = configuration.scheme;
this.host = configuration.host;
this.port = configuration.port;
this.base_path = configuration.base_path || '';
this.read_timeout = configuration.read_timeout || 60000;
} else {
this.scheme = suiteConfigData.scheme;
this.host = suiteConfigData.host;
this.port = suiteConfigData.port;
this.base_path = suiteConfigData.base_path || '';
this.read_timeout = suiteConfigData.read_timeout || 60000;
}
const data = {
scheme: this.scheme,
host: this.host,
port: this.port,
base_path: this.base_path,
read_timeout: this.read_timeout
};
const suiteConfigSchemaDefinition = _path2.default.resolve(__dirname, './schema/yaml/suite-config.json');
const schemaValidation = (0, _validator.validateJSONSchema)(data, suiteConfigSchemaDefinition);
if (schemaValidation.errors.length >= 1) {
let errorMessages = '';
for (let error of schemaValidation.errors) {
errorMessages += ` property - ${error.property}, message: ${error.message} \n`;
}
throw new InvalidSuiteConfigurationError(`Invalid Suite configuration : ${this.file} \n ${errorMessages}`);
}
this.targetConfiguration = {
host: this.host,
port: this.port,
scheme: this.scheme,
base_path: this.base_path,
read_timeout: this.read_timeout
};
this.rootURL = url.format({
protocol: this.scheme,
hostname: this.host,
port: this.port,
pathname: this.base_path
});
if (suiteConfigData.common_headers && suiteConfigData.common_headers.constructor.name === 'Array') {
let defaultHeaders = {};
const specHeaders = suiteConfigData.common_headers;
for (let item of specHeaders) {
defaultHeaders[item['name']] = item['value'];
}
this.commonHeaders = defaultHeaders;
}
}
isSpecSkippable(specification) {
if (!(typeof specification.enabled === 'undefined' || specification.enabled === true)) return true;
if (this.grep) {
return !this.grep.test(specification.name);
}
return false;
}
addSpecResultToSuite(spec) {
this.result.specs.push(spec.result);
}
async run() {
const self = this;
try {
this.ensureSpecsExist();
await this.runBeforeAllHook();
let specs = this.specs;
for (const specData of specs) {
this.requests = [];
const specification = specData;
let spec;
/**
* Check if spec is skippable
*/
try {
if (this.isSpecSkippable(specification)) {
spec = new _spec2.default(specification, this);
spec.result.status = 'skip';
spec.setDuration();
this.addSpecResultToSuite(spec);
this.emit('test skip', spec);
continue;
}
} catch (error) {
spec = new _spec2.default(specification, this);
spec.result.status = 'fail';
spec.setDuration();
this.addSpecResultToSuite(spec);
this.emit('test fail', spec, error);
continue;
}
/**
* Handle specs with loop definitions
*/
if (specification.loop) {
let loopItems;
try {
loopItems = await this.fetchLoopItems(specification.loop);
} catch (error) {
spec = new _spec2.default(specification, this);
spec.result.status = 'fail';
spec.setDuration();
this.addSpecResultToSuite(spec);
this.emit('test fail', spec, error);
continue;
}
for (let loopItemIdx in loopItems) {
this.requests = [];
let loopSpec;
try {
let iterationSpecification = Object.assign({}, specification);
iterationSpecification.loopSpec = true;
iterationSpecification.loopData = { index: loopItemIdx, value: loopItems[loopItemIdx] };
loopSpec = new _spec2.default(iterationSpecification, this);
this.emit('test start', loopSpec);
await loopSpec.init();
loopSpec.result.status = 'pass';
loopSpec.requests = this.requests.slice(0);
this.addSpecResultToSuite(loopSpec);
this.emit('test pass', loopSpec);
} catch (error) {
loopSpec.result.status = 'fail';
loopSpec.requests = this.requests.slice(0);
loopSpec.setDuration();
this.addSpecResultToSuite(loopSpec);
this.emit('test fail', loopSpec, error);
}
}
continue;
}
/**
* When a spec is not loop-able, run it normally
*/
try {
spec = new _spec2.default(specification, this);
this.emit('test start', spec);
await spec.init();
spec.result.status = 'pass';
spec.requests = this.requests.slice(0);
this.addSpecResultToSuite(spec);
this.emit('test pass', spec);
} catch (error) {
spec.result.status = 'fail';
spec.setDuration();
spec.requests = this.requests.slice(0);
this.addSpecResultToSuite(spec);
this.emit('test fail', spec, error);
}
}
this.requests = [];
await this.runAfterAllHook();
this.updateSuiteStatus();
this.emit('end', this);
} catch (error) {
this.status = 'fail';
this.emit('end', this, error);
}
}
updateSuiteStatus() {
let failedSpecCount = 0;
for (let spec of this.result.specs) {
if (spec.status === 'fail') {
failedSpecCount += 1;
}
}
this.status = failedSpecCount > 0 ? 'fail' : 'pass';
}
async fetchLoopItems(loopData) {
let loopItems;
let LoopItemsBuilderError = (0, _errors.customError)('LoopItemsBuilderError');
try {
if (loopData.type === 'static') {
loopItems = loopData.static;
} else if (loopData.type === 'dynamic') {
if (loopData.dynamic.run_type === 'inline') {
let inlineFunction = loopData.dynamic.inline.function;
let inlineResult = await (0, _utils.runInlineFunction)(inlineFunction);
loopItems = inlineResult;
} else if (loopData.dynamic.run_type === 'module') {
let modulePath = this.resolveFile(loopData.dynamic.module.module_path);
const module = (0, _utils.assertFileValidity)(modulePath, 'Loop data module');
const customModule = (0, _utils.loadModule)(module);
let moduleResult = await (0, _utils.runModuleFunction)(customModule, loopData.dynamic.module.function_name);
loopItems = moduleResult;
} else {
throw new InvalidYAMLSuiteSchemaError(`Loop dynamic run_type should be either inline or module`);
}
} else {
throw new InvalidYAMLSuiteSchemaError('Loop data should be either static or dynamic');
}
if (loopItems.constructor.name !== 'Array') {
throw new InvalidSpecificationSchemaError('Loop dynamic function did not return an Array');
}
} catch (err) {
if (err.name === 'LoopItemsBuilderError') {
throw err;
} else {
throw new LoopItemsBuilderError(`${err.name || 'Error'} occurred while building loop items \n ${err.message || err}`);
}
}
return loopItems;
}
/*
options can include host, port, scheme, path_params, query_params, headers,
read_timeout, in case of body requirements ( body, form_data, form)
validateResponse option will enable/disable response validation
*/
async runDependencySpec(name) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let specData;
let dependencyFromSuite;
specData = this._sctx.specs.find(spec => spec.name === name);
if (!specData) {
if (this._sctx.specDependencies.length) {
for (let dependencySuite of this._sctx.specDependencies) {
specData = dependencySuite.specs.find(spec => spec.name === name);
if (specData) {
dependencyFromSuite = dependencySuite;
break;
}
}
if (!specData) {
throw new NoSpecFoundMatchingNameError(`No matching spec found with name '${name}'`);
}
} else {
throw new NoSpecFoundMatchingNameError(`No matching spec found with name '${name}'`);
}
}
let opts;
if (dependencyFromSuite) {
opts = Object.assign({ outerDependency: true, outerDependencySuite: dependencyFromSuite }, options);
} else {
opts = Object.assign({}, options);
}
let spec = new _spec2.default(specData, this._sctx, 'dependency', opts);
try {
return await spec.init();
} catch (error) {
throw error;
}
}
async runHook(type, hookData) {
const data = hookData;
const self = this;
const runSpecFunc = self.runDependencySpec;
let _context = { suite: this.userContext, runSpec: runSpecFunc, _sctx: self };
if (data.run_type === 'inline') {
if (data.inline) {
let inlineFunction = data.inline.function;
await (0, _utils.runInlineFunction)(inlineFunction, _context);
} else {
throw new InvalidYAMLSuiteSchemaError(`Suite ${type} hook inline function definition is not specified`);
}
} else if (data.run_type === 'module') {
if (data.module) {
let modulePath = this.resolveFile(data.module.module_path);
const module = (0, _utils.assertFileValidity)(modulePath, 'Suite ${type} hook module');
const customModule = (0, _utils.loadModule)(module);
await (0, _utils.runModuleFunction)(customModule, data.module.function_name, _context);
} else {
throw new InvalidYAMLSuiteSchemaError(`Suite ${type} hook module function definition is not specified`);
}
} else {
throw new InvalidYAMLSuiteSchemaError(`Suite ${type} hook run type should be inline or module`);
}
}
ensureSpecsExist() {
if (this.specs.length < 1) {
throw new NoSpecsFoundError(`No specs found in file '${this.file}'`);
}
}
async runBeforeAllHook() {
if (this.hooks.before_all) {
try {
await this.runHook('before all', this.hooks.before_all);
} catch (err) {
let BeforeAllHookError = (0, _errors.customError)('BeforeAllHookError');
throw new BeforeAllHookError(`${err.name || 'Error'} occurred while running the before all hook \n ${err.message || err}`);
}
}
}
async runAfterAllHook() {
if (this.hooks.after_all) {
try {
await this.runHook('after all', this.hooks.after_all);
} catch (err) {
let AfterAllHookError = (0, _errors.customError)('AfterAllHookError');
throw new AfterAllHookError(`${err.name || 'Error'} occurred while running the after all hook \n ${err.message || err}`);
}
}
}
}
exports.default = Suite;
module.exports = exports['default'];
;