service-template-node
Version:
A blueprint for MediaWiki REST API services
304 lines (250 loc) • 9.05 kB
JavaScript
/* global describe, it, before, after */
;
const preq = require('preq');
const assert = require('../../utils/assert.js');
const server = require('../../utils/server.js');
const URI = require('swagger-router').URI;
const yaml = require('js-yaml');
const fs = require('fs');
const OpenAPISchemaValidator = require('openapi-schema-validator').default;
const validator = new OpenAPISchemaValidator({ version: 3 });
if (!server.stopHookAdded) {
server.stopHookAdded = true;
after(() => server.stop());
}
function staticSpecLoad() {
let spec;
const myService = server.config.conf.services[server.config.conf.services.length - 1].conf;
const specPath = `${__dirname}/../../../${myService.spec ? myService.spec : 'spec.yaml'}`;
try {
spec = yaml.safeLoad(fs.readFileSync(specPath));
} catch (e) {
// this error will be detected later, so ignore it
spec = { paths: {}, 'x-default-params': {} };
}
return spec;
}
function validateExamples(pathStr, defParams, mSpec) {
const uri = new URI(pathStr, {}, true);
if (!mSpec) {
try {
uri.expand(defParams);
return true;
} catch (e) {
throw new Error(`Missing parameter for route ${pathStr} : ${e.message}`);
}
}
if (!Array.isArray(mSpec)) {
throw new Error(`Route ${pathStr} : x-amples must be an array!`);
}
mSpec.forEach((ex, idx) => {
if (!ex.title) {
throw new Error(`Route ${pathStr}, example ${idx}: title missing!`);
}
ex.request = ex.request || {};
try {
uri.expand(Object.assign({}, defParams, ex.request.params || {}));
} catch (e) {
throw new Error(
`Route ${pathStr}, example ${idx} (${ex.title}): missing parameter: ${e.message}`
);
}
});
return true;
}
function constructTestCase(title, path, method, request, response) {
return {
title,
request: {
uri: server.config.uri + (path[0] === '/' ? path.substr(1) : path),
method,
headers: request.headers || {},
query: request.query,
body: request.body,
followRedirect: false
},
response: {
status: response.status || 200,
headers: response.headers || {},
body: response.body
}
};
}
function constructTests(paths, defParams) {
const ret = [];
Object.keys(paths).forEach((pathStr) => {
Object.keys(paths[pathStr]).forEach((method) => {
const p = paths[pathStr][method];
if ({}.hasOwnProperty.call(p, 'x-monitor') && !p['x-monitor']) {
return;
}
const uri = new URI(pathStr, {}, true);
if (!p['x-amples']) {
ret.push(constructTestCase(
pathStr,
uri.toString({ params: defParams }),
method,
{},
{}
));
return;
}
p['x-amples'].forEach((ex) => {
ex.request = ex.request || {};
ret.push(constructTestCase(
ex.title,
uri.toString({ params: Object.assign({}, defParams, ex.request.params || {}) }),
method,
ex.request,
ex.response || {}
));
});
});
});
return ret;
}
function cmp(result, expected, errMsg) {
if (expected === null || expected === undefined) {
// nothing to expect, so we can return
return true;
}
if (result === null || result === undefined) {
result = '';
}
if (expected.constructor === Object) {
Object.keys(expected).forEach((key) => {
const val = expected[key];
assert.deepEqual({}.hasOwnProperty.call(result, key), true,
`Body field ${key} not found in response!`);
cmp(result[key], val, `${key} body field mismatch!`);
});
return true;
} else if (expected.constructor === Array) {
if (result.constructor !== Array) {
assert.deepEqual(result, expected, errMsg);
return true;
}
// only one item in expected - compare them all
if (expected.length === 1 && result.length > 1) {
result.forEach((item) => {
cmp(item, expected[0], errMsg);
});
return true;
}
// more than one item expected, check them one by one
if (expected.length !== result.length) {
assert.deepEqual(result, expected, errMsg);
return true;
}
expected.forEach((item, idx) => {
cmp(result[idx], item, errMsg);
});
return true;
}
if (expected.length > 1 && expected[0] === '/' && expected[expected.length - 1] === '/') {
if ((new RegExp(expected.slice(1, -1))).test(result)) {
return true;
}
} else if (expected.length === 0 && result.length === 0) {
return true;
} else if (result === expected || result.startsWith(expected)) {
return true;
}
assert.deepEqual(result, expected, errMsg);
return true;
}
function validateTestResponse(testCase, res) {
const expRes = testCase.response;
// check the status
assert.status(res, expRes.status);
// check the headers
Object.keys(expRes.headers).forEach((key) => {
const val = expRes.headers[key];
assert.deepEqual({}.hasOwnProperty.call(res.headers, key), true,
`Header ${key} not found in response!`);
cmp(res.headers[key], val, `${key} header mismatch!`);
});
// check the body
if (!expRes.body) {
return true;
}
res.body = res.body || '';
if (Buffer.isBuffer(res.body)) { res.body = res.body.toString(); }
if (expRes.body.constructor !== res.body.constructor) {
if (expRes.body.constructor === String) {
res.body = JSON.stringify(res.body);
} else {
res.body = JSON.parse(res.body);
}
}
// check that the body type is the same
if (expRes.body.constructor !== res.body.constructor) {
throw new Error(
`Expected body type ${expRes.body.constructor} but got ${res.body.constructor}`
);
}
// compare the bodies
cmp(res.body, expRes.body, 'Body mismatch!');
return true;
}
describe('Swagger spec', function() {
// the variable holding the spec
let spec = staticSpecLoad();
// default params, if given
let defParams = spec['x-default-params'] || {};
this.timeout(20000); // eslint-disable-line no-invalid-this
before(() => {
return server.start();
});
it('get the spec', () => {
return preq.get(`${server.config.uri}?spec`)
.then((res) => {
assert.status(200);
assert.contentType(res, 'application/json');
assert.notDeepEqual(res.body, undefined, 'No body received!');
spec = res.body;
});
});
it('should expose valid OpenAPI spec', () => {
return preq.get({ uri: `${server.config.uri}?spec` })
.then((res) => {
assert.deepEqual({errors: []}, validator.validate(res.body), 'Spec must have no validation errors');
});
});
it('spec validation', () => {
if (spec['x-default-params']) {
defParams = spec['x-default-params'];
}
// check the high-level attributes
['info', 'openapi', 'paths'].forEach((prop) => {
assert.deepEqual(!!spec[prop], true, `No ${prop} field present!`);
});
// no paths - no love
assert.deepEqual(!!Object.keys(spec.paths), true, 'No paths given in the spec!');
// now check each path
Object.keys(spec.paths).forEach((pathStr) => {
assert.deepEqual(!!pathStr, true, 'A path cannot have a length of zero!');
const path = spec.paths[pathStr];
assert.deepEqual(!!Object.keys(path), true, `No methods defined for path: ${pathStr}`);
Object.keys(path).forEach((method) => {
const mSpec = path[method];
if ({}.hasOwnProperty.call(mSpec, 'x-monitor') && !mSpec['x-monitor']) {
return;
}
validateExamples(pathStr, defParams, mSpec['x-amples']);
});
});
});
describe('routes', () => {
constructTests(spec.paths, defParams).forEach((testCase) => {
it(testCase.title, () => {
return preq(testCase.request)
.then((res) => {
validateTestResponse(testCase, res);
}, (err) => {
validateTestResponse(testCase, err);
});
});
});
});
});