@ -10,6 +10,7 @@ const wait = util.promisify(setTimeout);
const db = require ( './mocks/databasemock' ) ;
const helpers = require ( './helpers' ) ;
const meta = require ( '../src/meta' ) ;
const user = require ( '../src/user' ) ;
const groups = require ( '../src/groups' ) ;
const categories = require ( '../src/categories' ) ;
@ -17,7 +18,7 @@ const topics = require('../src/topics');
const plugins = require ( '../src/plugins' ) ;
const flags = require ( '../src/flags' ) ;
const messaging = require ( '../src/messaging' ) ;
const utils = require ( '../src/utils' ) ;
describe ( 'Read API' , async ( ) => {
let readApi = false ;
@ -25,9 +26,30 @@ describe('Read API', async () => {
const readApiPath = path . resolve ( _ _dirname , '../public/openapi/read.yaml' ) ;
const writeApiPath = path . resolve ( _ _dirname , '../public/openapi/write.yaml' ) ;
let jar ;
let csrfToken ;
let setup = false ;
const unauthenticatedRoutes = [ '/api/login' , '/api/register' ] ; // Everything else will be called with the admin user
const mocks = {
get : { } ,
post : { } ,
put : { } ,
delete : {
'/users/{uid}/tokens/{token}' : [
{
in : 'path' ,
name : 'uid' ,
example : 1 ,
} ,
{
in : 'path' ,
name : 'token' ,
example : utils . generateUUID ( ) ,
} ,
] ,
} ,
} ;
async function dummySearchHook ( data ) {
return [ 1 ] ;
}
@ -44,8 +66,26 @@ describe('Read API', async () => {
// Create sample users
const adminUid = await user . create ( { username : 'admin' , password : '123456' , email : 'test@example.org' } ) ;
const unprivUid = await user . create ( { username : 'unpriv' , password : '123456' , email : 'unpriv@example.org' } ) ;
for ( let x = 0 ; x < 3 ; x ++ ) {
// eslint-disable-next-line no-await-in-loop
await user . create ( { username : 'deleteme' , password : '123456' } ) ; // for testing of user deletion routes (uids 4-6)
}
await groups . join ( 'administrators' , adminUid ) ;
// Create sample group
await groups . create ( {
name : 'Test Group' ,
} ) ;
await meta . settings . set ( 'core.api' , {
tokens : [ {
token : mocks . delete [ '/users/{uid}/tokens/{token}' ] [ 1 ] . example ,
uid : 1 ,
description : 'for testing of token deletion rotue' ,
timestamp : Date . now ( ) ,
} ] ,
} ) ;
// Create a category
const testCategory = await categories . create ( { name : 'test' } ) ;
@ -78,6 +118,15 @@ describe('Read API', async () => {
} ) ;
jar = await helpers . loginUser ( 'admin' , '123456' ) ;
// Retrieve CSRF token using cookie, to test Write API
const config = await request ( {
url : nconf . get ( 'url' ) + '/api/config' ,
json : true ,
jar : jar ,
} ) ;
csrfToken = config . csrf _token ;
setup = true ;
}
@ -93,87 +142,133 @@ describe('Read API', async () => {
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(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 ;
let method ;
const headers = { } ;
const qs = { } ;
Object . keys ( context ) . forEach ( ( _method ) => {
if ( _method !== 'get' ) {
return ;
}
it ( 'should have examples when parameters are present' , ( ) => {
method = _method ;
const parameters = context [ method ] . parameters ;
let testPath = path ;
if ( parameters ) {
parameters . forEach ( ( param ) => {
assert ( param . example !== null && param . example !== undefined , ` ${ method . toUpperCase ( ) } ${ 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 ;
}
} ) ;
}
url = nconf . get ( 'url' ) + testPath ;
} ) ;
it ( 'should resolve with a 200 when called' , async ( ) => {
await setupData ( ) ;
try {
response = await request ( url , {
method : method ,
jar : ! unauthenticatedRoutes . includes ( path ) ? jar : undefined ,
json : true ,
headers : headers ,
qs : qs ,
} ) ;
} catch ( e ) {
assert ( ! e , ` ${ method . toUpperCase ( ) } ${ path } resolved with ${ e . message } ` ) ;
}
} ) ;
console . log ( response ) ;
// Recursively iterate through schema properties, comparing type
it ( 'response should match schema definition' , ( ) => {
const has200 = context [ method ] . responses [ '200' ] ;
if ( ! has200 ) {
return ;
}
const hasJSON = has200 . content && has200 . content [ 'application/json' ] ;
if ( hasJSON ) {
schema = context [ method ] . responses [ '200' ] . content [ 'application/json' ] . schema ;
compare ( schema , response , 'root' ) ;
}
// TODO someday: text/csv, binary file type checking?
// generateTests(readApi, Object.keys(readApi.paths));
generateTests ( writeApi , Object . keys ( writeApi . paths ) , writeApi . servers [ 0 ] . url ) ;
function generateTests ( api , paths , prefix ) {
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
paths . forEach ( ( path ) => {
const context = api . paths [ path ] ;
let schema ;
let response ;
let url ;
let method ;
const headers = { } ;
const qs = { } ;
Object . keys ( context ) . forEach ( ( _method ) => {
// if (_method !== 'get') {
// return;
// }
it ( 'should have examples when parameters are present' , ( ) => {
method = _method ;
let parameters = context [ method ] . parameters ;
let testPath = path ;
if ( parameters ) {
// Use mock data if provided
parameters = mocks [ method ] [ path ] || parameters ;
parameters . forEach ( ( param ) => {
assert ( param . example !== null && param . example !== undefined , ` ${ method . toUpperCase ( ) } ${ 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 ;
}
} ) ;
}
url = nconf . get ( 'url' ) + prefix + testPath ;
} ) ;
it ( 'may contain a request body with application/json type if POST/PUT/DELETE' , ( ) => {
if ( [ 'post' , 'put' , 'delete' ] . includes ( method ) && context [ method ] . hasOwnProperty ( 'requestBody' ) ) {
assert ( context [ method ] . requestBody ) ;
assert ( context [ method ] . requestBody . content ) ;
assert ( context [ method ] . requestBody . content [ 'application/json' ] ) ;
assert ( context [ method ] . requestBody . content [ 'application/json' ] . schema ) ;
assert ( context [ method ] . requestBody . content [ 'application/json' ] . schema . properties ) ;
}
} ) ;
it ( 'should resolve with a 200 when called' , async ( ) => {
await setupData ( ) ;
if ( csrfToken ) {
headers [ 'x-csrf-token' ] = csrfToken ;
}
let body = { } ;
if ( context [ method ] . hasOwnProperty ( 'requestBody' ) ) {
body = buildBody ( context [ method ] . requestBody . content [ 'application/json' ] . schema . properties ) ;
}
try {
// console.log(`calling ${method} ${url} with`, body);
response = await request ( url , {
method : method ,
jar : ! unauthenticatedRoutes . includes ( path ) ? jar : undefined ,
json : true ,
headers : headers ,
qs : qs ,
body : body ,
} ) ;
} catch ( e ) {
assert ( ! e , ` ${ method . toUpperCase ( ) } ${ path } resolved with ${ e . message } ` ) ;
}
} ) ;
// Recursively iterate through schema properties, comparing type
it ( 'response should match schema definition' , ( ) => {
const has200 = context [ method ] . responses [ '200' ] ;
if ( ! has200 ) {
return ;
}
const hasJSON = has200 . content && has200 . content [ 'application/json' ] ;
if ( hasJSON ) {
schema = context [ method ] . responses [ '200' ] . content [ 'application/json' ] . schema ;
compare ( schema , response , method . toUpperCase ( ) , path , 'root' ) ;
}
// TODO someday: text/csv, binary file type checking?
} ) ;
it ( 'should successfully re-login if needed' , async ( ) => {
const reloginPaths = [ '/users/{uid}/password' ] ;
if ( method === 'put' && reloginPaths . includes ( path ) ) {
jar = await helpers . loginUser ( 'admin' , '123456' ) ;
// Retrieve CSRF token using cookie, to test Write API
const config = await request ( {
url : nconf . get ( 'url' ) + '/api/config' ,
json : true ,
jar : jar ,
} ) ;
csrfToken = config . csrf _token ;
}
} ) ;
} ) ;
} ) ;
} ) ;
}
function buildBody ( schema ) {
return Object . keys ( schema ) . reduce ( ( memo , cur ) => {
memo [ cur ] = schema [ cur ] . example ;
return memo ;
} , { } ) ;
}
function compare ( schema , response , context ) {
function compare ( schema , response , method, path , context) {
let required = [ ] ;
const additionalProperties = schema . hasOwnProperty ( 'additionalProperties' ) ;
@ -194,7 +289,7 @@ describe('Read API', async () => {
// 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 + ')' ) ;
assert ( response . hasOwnProperty ( prop ) , '"' + prop + '" is a required property (path: ' + method + ' ' + 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 ) {
@ -202,30 +297,30 @@ describe('Read API', async () => {
}
// 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 + ')' ) ;
assert ( response [ prop ] !== null , '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + method + ' ' + 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 + ')' ) ;
assert . strictEqual ( typeof response [ prop ] , 'string' , '"' + prop + '" was expected to be a string, but was ' + typeof response [ prop ] + ' instead (path: ' + method + ' ' + 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 + ')' ) ;
assert . strictEqual ( typeof response [ prop ] , 'boolean' , '"' + prop + '" was expected to be a boolean, but was ' + typeof response [ prop ] + ' instead (path: ' + method + ' ' + 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 ) ;
assert . strictEqual ( typeof response [ prop ] , 'object' , '"' + prop + '" was expected to be an object, but was ' + typeof response [ prop ] + ' instead (path: ' + method + ' ' + path + ', context: ' + context + ')' ) ;
compare ( schema [ prop ] , response [ prop ] , method, path , 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 + ')' ) ;
assert . strictEqual ( Array . isArray ( response [ prop ] ) , true , '"' + prop + '" was expected to be an array, but was ' + typeof response [ prop ] + ' instead (path: ' + method + ' ' + 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 + ')' ) ;
assert ( schema [ prop ] . items . type || schema [ prop ] . items . allOf , '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + method + ' ' + 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 ) ;
compare ( schema [ prop ] . items , res , method, path , context ? [ context , prop ] . join ( '.' ) : prop ) ;
} ) ;
} else if ( response [ prop ] . length ) { // for now
response [ prop ] . forEach ( ( item ) => {