mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 08:36:12 +01:00
feat: wip, write api tests framework
re-using read api tests if possible
This commit is contained in:
@@ -34,43 +34,6 @@ function authenticatedRoutes() {
|
||||
|
||||
setupApiRoute(router, 'post', '/:uid/tokens', [...middlewares, middleware.assert.user, middleware.exposePrivilegeSet], controllers.write.users.generateToken);
|
||||
setupApiRoute(router, 'delete', '/:uid/tokens/:token', [...middlewares, middleware.assert.user, middleware.exposePrivilegeSet], controllers.write.users.deleteToken);
|
||||
|
||||
/**
|
||||
* Implement this later...
|
||||
*/
|
||||
// app.route('/:uid/tokens')
|
||||
// .get(apiMiddleware.requireUser, function(req, res) {
|
||||
// if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) {
|
||||
// return errorHandler.respond(401, res);
|
||||
// }
|
||||
|
||||
// auth.getTokens(req.params.uid, function(err, tokens) {
|
||||
// return errorHandler.handle(err, res, {
|
||||
// tokens: tokens
|
||||
// });
|
||||
// });
|
||||
// })
|
||||
// .post(apiMiddleware.requireUser, function(req, res) {
|
||||
// if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid)) {
|
||||
// return errorHandler.respond(401, res);
|
||||
// }
|
||||
|
||||
// auth.generateToken(req.params.uid, function(err, token) {
|
||||
// return errorHandler.handle(err, res, {
|
||||
// token: token
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// app.delete('/:uid/tokens/:token', apiMiddleware.requireUser, function(req, res) {
|
||||
// if (parseInt(req.params.uid, 10) !== req.user.uid) {
|
||||
// return errorHandler.respond(401, res);
|
||||
// }
|
||||
|
||||
// auth.revokeToken(req.params.token, 'user', function(err) {
|
||||
// errorHandler.handle(err, res);
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
|
||||
226
test/api.js
226
test/api.js
@@ -21,7 +21,9 @@ const messaging = require('../src/messaging');
|
||||
|
||||
describe('Read API', async () => {
|
||||
let readApi = false;
|
||||
const apiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
||||
let writeApi = false;
|
||||
const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
|
||||
const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml');
|
||||
let jar;
|
||||
let setup = false;
|
||||
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user
|
||||
@@ -67,7 +69,7 @@ describe('Read API', async () => {
|
||||
await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid });
|
||||
await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid });
|
||||
// wait for export child process to complete
|
||||
await wait(20000);
|
||||
// await wait(20000);
|
||||
|
||||
// Attach a search hook so /api/search is enabled
|
||||
plugins.registerHook('core', {
|
||||
@@ -81,126 +83,55 @@ describe('Read API', async () => {
|
||||
|
||||
it('should pass OpenAPI v3 validation', async () => {
|
||||
try {
|
||||
await SwaggerParser.validate(apiPath);
|
||||
await SwaggerParser.validate(readApiPath);
|
||||
await SwaggerParser.validate(writeApiPath);
|
||||
} catch (e) {
|
||||
assert.ifError(e);
|
||||
}
|
||||
});
|
||||
|
||||
readApi = await SwaggerParser.dereference(apiPath);
|
||||
readApi = await SwaggerParser.dereference(readApiPath);
|
||||
writeApi = await SwaggerParser.dereference(writeApiPath);
|
||||
|
||||
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
|
||||
const paths = Object.keys(readApi.paths);
|
||||
const paths = Object.keys(writeApi.paths);
|
||||
// const paths = Object.keys(readApi.paths);
|
||||
|
||||
paths.forEach((path) => {
|
||||
const context = writeApi.paths[path];
|
||||
// const context = readApi.paths[path];
|
||||
let schema;
|
||||
let response;
|
||||
let url;
|
||||
const urls = [];
|
||||
const methods = [];
|
||||
const headers = {};
|
||||
const qs = {};
|
||||
|
||||
function compare(schema, response, context) {
|
||||
let required = [];
|
||||
const additionalProperties = schema.hasOwnProperty('additionalProperties');
|
||||
|
||||
if (schema.allOf) {
|
||||
schema = schema.allOf.reduce((memo, obj) => {
|
||||
required = required.concat(obj.required ? obj.required : Object.keys(obj.properties));
|
||||
memo = { ...memo, ...obj.properties };
|
||||
return memo;
|
||||
}, {});
|
||||
} else if (schema.properties) {
|
||||
required = schema.required || Object.keys(schema.properties);
|
||||
schema = schema.properties;
|
||||
} else {
|
||||
// If schema contains no properties, check passes
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare the schema to the response
|
||||
required.forEach((prop) => {
|
||||
if (schema.hasOwnProperty(prop)) {
|
||||
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec)
|
||||
if (response[prop] === null && schema[prop].nullable === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Therefore, if the value is actually null, that's a problem (nullable is probably missing)
|
||||
assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
switch (schema[prop].type) {
|
||||
case 'string':
|
||||
assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
break;
|
||||
case 'boolean':
|
||||
assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
break;
|
||||
case 'object':
|
||||
assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop);
|
||||
break;
|
||||
case 'array':
|
||||
assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
if (schema[prop].items) {
|
||||
// Ensure the array items have a schema defined
|
||||
assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
// Compare types
|
||||
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
|
||||
response[prop].forEach((res) => {
|
||||
compare(schema[prop].items, res, context ? [context, prop].join('.') : prop);
|
||||
});
|
||||
} else if (response[prop].length) { // for now
|
||||
response[prop].forEach((item) => {
|
||||
assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compare the response to the schema
|
||||
Object.keys(response).forEach((prop) => {
|
||||
if (additionalProperties) { // All bets are off
|
||||
return;
|
||||
}
|
||||
|
||||
assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
|
||||
// TOXO: fix -- premature exit for POST-only routes
|
||||
if (!readApi.paths[path].get) {
|
||||
return;
|
||||
}
|
||||
|
||||
it('should have examples when parameters are present', () => {
|
||||
const parameters = readApi.paths[path].get.parameters;
|
||||
let testPath = path;
|
||||
if (parameters) {
|
||||
parameters.forEach((param) => {
|
||||
assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples');
|
||||
Object.keys(context).forEach((method) => {
|
||||
const parameters = context[method].parameters;
|
||||
let testPath = path;
|
||||
if (parameters) {
|
||||
parameters.forEach((param) => {
|
||||
assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples');
|
||||
|
||||
switch (param.in) {
|
||||
case 'path':
|
||||
testPath = testPath.replace('{' + param.name + '}', param.example);
|
||||
break;
|
||||
case 'header':
|
||||
headers[param.name] = param.example;
|
||||
break;
|
||||
case 'query':
|
||||
qs[param.name] = param.example;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
switch (param.in) {
|
||||
case 'path':
|
||||
testPath = testPath.replace('{' + param.name + '}', param.example);
|
||||
break;
|
||||
case 'header':
|
||||
headers[param.name] = param.example;
|
||||
break;
|
||||
case 'query':
|
||||
qs[param.name] = param.example;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
url = nconf.get('url') + testPath;
|
||||
urls.push = nconf.get('url') + testPath;
|
||||
methods.push(method);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve with a 200 when called', async () => {
|
||||
@@ -220,32 +151,93 @@ describe('Read API', async () => {
|
||||
|
||||
// Recursively iterate through schema properties, comparing type
|
||||
it('response should match schema definition', () => {
|
||||
const has200 = readApi.paths[path].get.responses['200'];
|
||||
const has200 = context.get.responses['200'];
|
||||
if (!has200) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasJSON = has200.content && has200.content['application/json'];
|
||||
if (hasJSON) {
|
||||
schema = readApi.paths[path].get.responses['200'].content['application/json'].schema;
|
||||
schema = context.get.responses['200'].content['application/json'].schema;
|
||||
compare(schema, response, 'root');
|
||||
}
|
||||
|
||||
// TODO someday: text/csv, binary file type checking?
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Write API', async () => {
|
||||
const apiPath = path.resolve(__dirname, '../public/openapi/write.yaml');
|
||||
function compare(schema, response, context) {
|
||||
let required = [];
|
||||
const additionalProperties = schema.hasOwnProperty('additionalProperties');
|
||||
|
||||
it('should pass OpenAPI v3 validation', async () => {
|
||||
try {
|
||||
await SwaggerParser.validate(apiPath);
|
||||
} catch (e) {
|
||||
assert.ifError(e);
|
||||
if (schema.allOf) {
|
||||
schema = schema.allOf.reduce((memo, obj) => {
|
||||
required = required.concat(obj.required ? obj.required : Object.keys(obj.properties));
|
||||
memo = { ...memo, ...obj.properties };
|
||||
return memo;
|
||||
}, {});
|
||||
} else if (schema.properties) {
|
||||
required = schema.required || Object.keys(schema.properties);
|
||||
schema = schema.properties;
|
||||
} else {
|
||||
// If schema contains no properties, check passes
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(await SwaggerParser.dereference(apiPath));
|
||||
// Compare the schema to the response
|
||||
required.forEach((prop) => {
|
||||
if (schema.hasOwnProperty(prop)) {
|
||||
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec)
|
||||
if (response[prop] === null && schema[prop].nullable === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Therefore, if the value is actually null, that's a problem (nullable is probably missing)
|
||||
assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
switch (schema[prop].type) {
|
||||
case 'string':
|
||||
assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
break;
|
||||
case 'boolean':
|
||||
assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
break;
|
||||
case 'object':
|
||||
assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop);
|
||||
break;
|
||||
case 'array':
|
||||
assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
if (schema[prop].items) {
|
||||
// Ensure the array items have a schema defined
|
||||
assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')');
|
||||
|
||||
// Compare types
|
||||
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
|
||||
response[prop].forEach((res) => {
|
||||
compare(schema[prop].items, res, context ? [context, prop].join('.') : prop);
|
||||
});
|
||||
} else if (response[prop].length) { // for now
|
||||
response[prop].forEach((item) => {
|
||||
assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Compare the response to the schema
|
||||
Object.keys(response).forEach((prop) => {
|
||||
if (additionalProperties) { // All bets are off
|
||||
return;
|
||||
}
|
||||
|
||||
assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user