2020-04-16 20:38:40 -04:00
'use strict' ;
2020-12-18 11:43:29 -05:00
const _ = require ( 'lodash' ) ;
2020-04-16 20:38:40 -04:00
const assert = require ( 'assert' ) ;
const path = require ( 'path' ) ;
2020-10-27 11:02:20 -04:00
const fs = require ( 'fs' ) ;
2020-04-16 20:38:40 -04:00
const SwaggerParser = require ( '@apidevtools/swagger-parser' ) ;
2020-04-23 21:50:08 -04:00
const request = require ( 'request-promise-native' ) ;
const nconf = require ( 'nconf' ) ;
2020-12-22 13:18:18 -05:00
const jwt = require ( 'jsonwebtoken' ) ;
2020-06-23 00:46:59 -04:00
const util = require ( 'util' ) ;
2021-02-03 23:53:16 -07:00
2020-06-23 00:46:59 -04:00
const wait = util . promisify ( setTimeout ) ;
2020-04-16 20:38:40 -04:00
2020-04-23 21:50:08 -04:00
const db = require ( './mocks/databasemock' ) ;
const helpers = require ( './helpers' ) ;
2020-10-26 21:51:25 -04:00
const meta = require ( '../src/meta' ) ;
2020-04-23 21:50:08 -04:00
const user = require ( '../src/user' ) ;
const groups = require ( '../src/groups' ) ;
const categories = require ( '../src/categories' ) ;
const topics = require ( '../src/topics' ) ;
2021-02-03 12:34:13 +03:00
const posts = require ( '../src/posts' ) ;
2020-04-23 21:50:08 -04:00
const plugins = require ( '../src/plugins' ) ;
const flags = require ( '../src/flags' ) ;
const messaging = require ( '../src/messaging' ) ;
2020-10-26 21:51:25 -04:00
const utils = require ( '../src/utils' ) ;
2020-04-23 21:50:08 -04:00
2020-11-12 14:32:49 -05:00
describe ( 'API' , async ( ) => {
2020-04-23 21:50:08 -04:00
let readApi = false ;
2020-10-24 11:01:53 -04:00
let writeApi = false ;
const readApiPath = path . resolve ( _ _dirname , '../public/openapi/read.yaml' ) ;
const writeApiPath = path . resolve ( _ _dirname , '../public/openapi/write.yaml' ) ;
2020-04-23 21:50:08 -04:00
let jar ;
2020-10-26 21:51:25 -04:00
let csrfToken ;
2020-04-23 21:50:08 -04:00
let setup = false ;
const unauthenticatedRoutes = [ '/api/login' , '/api/register' ] ; // Everything else will be called with the admin user
2020-10-26 21:51:25 -04:00
const mocks = {
2020-11-12 14:32:49 -05:00
head : { } ,
2020-12-22 13:18:18 -05:00
get : {
'/api/email/unsubscribe/{token}' : [
{
in : 'path' ,
name : 'token' ,
example : ( ( ) => jwt . sign ( {
template : 'digest' ,
uid : 1 ,
} , nconf . get ( 'secret' ) ) ) ( ) ,
} ,
] ,
} ,
2020-10-26 21:51:25 -04:00
post : { } ,
put : { } ,
delete : {
'/users/{uid}/tokens/{token}' : [
{
in : 'path' ,
name : 'uid' ,
example : 1 ,
} ,
{
in : 'path' ,
name : 'token' ,
example : utils . generateUUID ( ) ,
} ,
] ,
2020-11-12 15:53:15 -05:00
'/users/{uid}/sessions/{uuid}' : [
{
in : 'path' ,
name : 'uid' ,
example : 1 ,
} ,
{
in : 'path' ,
name : 'uuid' ,
example : '' , // to be defined below...
} ,
] ,
2021-02-03 12:34:13 +03:00
'/posts/{pid}/diffs/{timestamp}' : [
{
in : 'path' ,
name : 'pid' ,
example : '' , // to be defined below...
} ,
{
in : 'path' ,
name : 'timestamp' ,
example : '' , // to be defined below...
} ,
] ,
2020-10-26 21:51:25 -04:00
} ,
} ;
2020-04-23 21:50:08 -04:00
async function dummySearchHook ( data ) {
return [ 1 ] ;
}
2020-12-05 14:25:14 -07:00
async function dummyEmailerHook ( data ) {
// pretend to handle sending emails
}
2020-04-23 21:50:08 -04:00
2021-02-04 00:01:39 -07:00
after ( async ( ) => {
2021-01-27 17:36:58 -05:00
plugins . hooks . unregister ( 'core' , 'filter:search.query' , dummySearchHook ) ;
plugins . hooks . unregister ( 'emailer-test' , 'filter:email.send' ) ;
2020-04-23 21:50:08 -04:00
} ) ;
async function setupData ( ) {
if ( setup ) {
return ;
}
// 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' } ) ;
2020-11-17 17:29:50 -05:00
for ( let x = 0 ; x < 4 ; x ++ ) {
2020-10-26 21:51:25 -04:00
// eslint-disable-next-line no-await-in-loop
2020-11-17 17:29:50 -05:00
await user . create ( { username : 'deleteme' , password : '123456' } ) ; // for testing of DELETE /users (uids 5, 6) and DELETE /user/:uid/account (uid 7)
2020-10-26 21:51:25 -04:00
}
2020-04-23 21:50:08 -04:00
await groups . join ( 'administrators' , adminUid ) ;
2020-10-26 21:51:25 -04:00
// 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 ,
2020-11-12 15:52:33 -05:00
description : 'for testing of token deletion route' ,
2020-10-26 21:51:25 -04:00
timestamp : Date . now ( ) ,
} ] ,
} ) ;
2020-12-04 12:56:55 -05:00
meta . config . allowTopicsThumbnail = 1 ;
2020-12-29 10:31:25 -05:00
meta . config . termsOfUse = 'I, for one, welcome our new test-driven overlords' ;
2020-10-26 21:51:25 -04:00
2020-04-23 21:50:08 -04:00
// Create a category
const testCategory = await categories . create ( { name : 'test' } ) ;
// Post a new topic
2021-02-03 12:34:13 +03:00
await topics . post ( {
2020-04-23 21:50:08 -04:00
uid : adminUid ,
cid : testCategory . cid ,
title : 'Test Topic' ,
content : 'Test topic content' ,
} ) ;
2020-10-27 11:02:20 -04:00
const unprivTopic = await topics . post ( {
uid : unprivUid ,
cid : testCategory . cid ,
title : 'Test Topic 2' ,
content : 'Test topic 2 content' ,
} ) ;
2021-01-18 15:31:14 -05:00
await topics . post ( {
uid : unprivUid ,
cid : testCategory . cid ,
title : 'Test Topic 3' ,
content : 'Test topic 3 content' ,
} ) ;
2020-04-23 21:50:08 -04:00
2021-02-03 12:34:13 +03:00
// Create a post diff
await posts . edit ( {
uid : adminUid ,
pid : unprivTopic . postData . pid ,
content : 'Test topic 2 edited content' ,
req : { } ,
} ) ;
mocks . delete [ '/posts/{pid}/diffs/{timestamp}' ] [ 0 ] . example = unprivTopic . postData . pid ;
mocks . delete [ '/posts/{pid}/diffs/{timestamp}' ] [ 1 ] . example = ( await posts . diffs . list ( unprivTopic . postData . pid ) ) [ 0 ] ;
2020-04-23 21:50:08 -04:00
// Create a sample flag
2021-07-16 13:44:42 -04:00
const { flagId } = await flags . create ( 'post' , 1 , unprivUid , 'sample reasons' , Date . now ( ) ) ;
await flags . appendNote ( flagId , 1 , 'test note' , 1626446956652 ) ;
2020-04-23 21:50:08 -04:00
// Create a new chat room
await messaging . newRoom ( 1 , [ 2 ] ) ;
2020-12-04 12:56:55 -05:00
// Create an empty file to test DELETE /files and thumb deletion
2020-10-27 11:02:20 -04:00
fs . closeSync ( fs . openSync ( path . resolve ( nconf . get ( 'upload_path' ) , 'files/test.txt' ) , 'w' ) ) ;
2020-12-04 12:56:55 -05:00
fs . closeSync ( fs . openSync ( path . resolve ( nconf . get ( 'upload_path' ) , 'files/test.png' ) , 'w' ) ) ;
2020-10-27 11:02:20 -04:00
2021-02-16 12:18:25 -05:00
// Associate thumb with topic to test thumb reordering
await topics . thumbs . associate ( {
id : 2 ,
path : 'files/test.png' ,
} ) ;
2020-10-16 20:36:24 -04:00
const socketUser = require ( '../src/socket.io/user' ) ;
2020-11-27 16:15:01 -05:00
const socketAdmin = require ( '../src/socket.io/admin' ) ;
2020-06-23 00:46:59 -04:00
// export data for admin user
await socketUser . exportProfile ( { uid : adminUid } , { uid : adminUid } ) ;
await socketUser . exportPosts ( { uid : adminUid } , { uid : adminUid } ) ;
await socketUser . exportUploads ( { uid : adminUid } , { uid : adminUid } ) ;
2020-11-27 16:15:01 -05:00
await socketAdmin . user . exportUsersCSV ( { uid : adminUid } , { } ) ;
2020-06-23 00:46:59 -04:00
// wait for export child process to complete
2020-11-12 15:57:36 -05:00
await wait ( 5000 ) ;
2020-06-23 00:46:59 -04:00
2020-04-23 21:50:08 -04:00
// Attach a search hook so /api/search is enabled
2021-01-27 17:36:58 -05:00
plugins . hooks . register ( 'core' , {
2020-04-23 21:50:08 -04:00
hook : 'filter:search.query' ,
method : dummySearchHook ,
} ) ;
2020-12-05 14:25:14 -07:00
// Attach an emailer hook so related requests do not error
2021-01-27 17:36:58 -05:00
plugins . hooks . register ( 'emailer-test' , {
2020-12-05 14:25:14 -07:00
hook : 'filter:email.send' ,
method : dummyEmailerHook ,
} ) ;
2020-04-23 21:50:08 -04:00
jar = await helpers . loginUser ( 'admin' , '123456' ) ;
2020-10-26 21:51:25 -04:00
// Retrieve CSRF token using cookie, to test Write API
const config = await request ( {
2021-02-03 23:59:08 -07:00
url : ` ${ nconf . get ( 'url' ) } /api/config ` ,
2020-10-26 21:51:25 -04:00
json : true ,
jar : jar ,
} ) ;
csrfToken = config . csrf _token ;
2020-04-23 21:50:08 -04:00
setup = true ;
}
2020-04-16 20:38:40 -04:00
it ( 'should pass OpenAPI v3 validation' , async ( ) => {
try {
2020-10-24 11:01:53 -04:00
await SwaggerParser . validate ( readApiPath ) ;
await SwaggerParser . validate ( writeApiPath ) ;
2020-04-16 20:38:40 -04:00
} catch ( e ) {
assert . ifError ( e ) ;
}
} ) ;
2020-04-23 21:50:08 -04:00
2020-10-24 11:01:53 -04:00
readApi = await SwaggerParser . dereference ( readApiPath ) ;
writeApi = await SwaggerParser . dereference ( writeApiPath ) ;
2020-04-23 21:50:08 -04:00
2020-12-12 13:25:36 -05:00
it ( 'should grab all mounted routes and ensure a schema exists' , async ( ) => {
const webserver = require ( '../src/webserver' ) ;
const buildPaths = function ( stack , prefix ) {
2020-12-15 13:54:55 -05:00
const paths = stack . map ( ( dispatch ) => {
if ( dispatch . route && dispatch . route . path && typeof dispatch . route . path === 'string' ) {
if ( ! prefix && ! dispatch . route . path . startsWith ( '/api/' ) ) {
return null ;
}
2020-12-18 12:28:32 -05:00
if ( prefix === nconf . get ( 'relative_path' ) ) {
prefix = '' ;
}
2020-12-15 13:54:55 -05:00
return {
method : Object . keys ( dispatch . route . methods ) [ 0 ] ,
path : ( prefix || '' ) + dispatch . route . path ,
} ;
} else if ( dispatch . name === 'router' ) {
const prefix = dispatch . regexp . toString ( ) . replace ( '/^' , '' ) . replace ( '\\/?(?=\\/|$)/i' , '' ) . replace ( /\\\//g , '/' ) ;
return buildPaths ( dispatch . handle . stack , prefix ) ;
2020-12-12 13:25:36 -05:00
}
2020-12-15 13:54:55 -05:00
// Drop any that aren't actual routes (middlewares, error handlers, etc.)
return null ;
} ) ;
2020-12-18 11:43:29 -05:00
return _ . flatten ( paths ) ;
2020-12-12 13:25:36 -05:00
} ;
2020-12-15 13:54:55 -05:00
2021-02-04 00:01:39 -07:00
let paths = buildPaths ( webserver . app . _router . stack ) . filter ( Boolean ) . map ( ( pathObj ) => {
2020-12-14 15:24:46 -05:00
pathObj . path = pathObj . path . replace ( /\/:([^\\/]+)/g , '/{$1}' ) ;
return pathObj ;
} ) ;
2020-12-18 11:43:29 -05:00
const exclusionPrefixes = [ '/api/admin/plugins' , '/api/compose' , '/debug' ] ;
2021-02-04 00:01:39 -07:00
paths = paths . filter ( path => path . method !== '_all' && ! exclusionPrefixes . some ( prefix => path . path . startsWith ( prefix ) ) ) ;
2020-12-15 13:54:55 -05:00
2020-12-12 13:25:36 -05:00
// For each express path, query for existence in read and write api schemas
paths . forEach ( ( pathObj ) => {
describe ( ` ${ pathObj . method . toUpperCase ( ) } ${ pathObj . path } ` , ( ) => {
it ( 'should be defined in schema docs' , ( ) => {
2020-12-15 13:54:55 -05:00
let schema = readApi ;
if ( pathObj . path . startsWith ( '/api/v3' ) ) {
schema = writeApi ;
pathObj . path = pathObj . path . replace ( '/api/v3' , '' ) ;
}
2020-12-17 15:02:09 -05:00
// Don't check non-GET routes in Read API
if ( schema === readApi && pathObj . method !== 'get' ) {
return ;
}
2020-12-12 13:25:36 -05:00
const normalizedPath = pathObj . path . replace ( /\/:([^\\/]+)/g , '/{$1}' ) . replace ( /\?/g , '' ) ;
2020-12-22 10:26:02 -05:00
assert ( schema . paths . hasOwnProperty ( normalizedPath ) , ` ${ pathObj . path } is not defined in schema docs ` ) ;
assert ( schema . paths [ normalizedPath ] . hasOwnProperty ( pathObj . method ) , ` ${ pathObj . path } was found in schema docs, but ${ pathObj . method . toUpperCase ( ) } method is not defined ` ) ;
2020-12-12 13:25:36 -05:00
} ) ;
} ) ;
} ) ;
} ) ;
2021-07-16 13:44:42 -04:00
// generateTests(readApi, Object.keys(readApi.paths));
2020-12-18 11:23:16 -05:00
generateTests ( writeApi , Object . keys ( writeApi . paths ) , writeApi . servers [ 0 ] . url ) ;
2020-10-26 21:51:25 -04:00
function generateTests ( api , paths , prefix ) {
2021-02-04 02:07:29 -07:00
// Iterate through all documented paths, make a call to it,
// and compare the result body with what is defined in the spec
2020-12-04 12:56:55 -05:00
const pathLib = path ; // for calling path module from inside this forEach
2020-10-26 21:51:25 -04:00
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 ) => {
2020-12-16 20:10:15 -05:00
// Only test GET routes in the Read API
2020-10-27 11:02:20 -04:00
if ( api . info . title === 'NodeBB Read API' && _method !== 'get' ) {
return ;
}
2020-10-26 21:51:25 -04:00
2020-11-12 15:52:33 -05:00
it ( 'should have each path parameter defined in its context' , ( ) => {
2020-10-26 21:51:25 -04:00
method = _method ;
2020-11-12 15:52:33 -05:00
if ( ! context [ method ] . parameters ) {
return ;
}
2020-12-18 11:23:16 -05:00
const pathParams = ( path . match ( /{[\w\-_*]+}?/g ) || [ ] ) . map ( match => match . slice ( 1 , - 1 ) ) ;
const schemaParams = context [ method ] . parameters . map ( param => ( param . in === 'path' ? param . name : null ) ) . filter ( Boolean ) ;
assert ( pathParams . every ( param => schemaParams . includes ( param ) ) , ` ${ method . toUpperCase ( ) } ${ path } has path parameters specified but not defined ` ) ;
2020-11-12 15:52:33 -05:00
} ) ;
it ( 'should have examples when parameters are present' , ( ) => {
2021-02-06 14:10:15 -07:00
let { parameters } = context [ method ] ;
2020-10-26 21:51:25 -04:00
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' :
2021-02-03 23:59:08 -07:00
testPath = testPath . replace ( ` { ${ param . name } } ` , param . example ) ;
2020-10-26 21:51:25 -04:00
break ;
case 'header' :
headers [ param . name ] = param . example ;
break ;
case 'query' :
qs [ param . name ] = param . example ;
break ;
}
} ) ;
}
2020-10-27 11:02:20 -04:00
url = nconf . get ( 'url' ) + ( prefix || '' ) + testPath ;
2020-10-26 21:51:25 -04:00
} ) ;
2020-12-04 12:56:55 -05:00
it ( 'should contain a valid request body (if present) with application/json or multipart/form-data type if POST/PUT/DELETE' , ( ) => {
2020-10-26 21:51:25 -04:00
if ( [ 'post' , 'put' , 'delete' ] . includes ( method ) && context [ method ] . hasOwnProperty ( 'requestBody' ) ) {
2020-12-16 20:10:15 -05:00
const failMessage = ` ${ method . toUpperCase ( ) } ${ path } has a malformed request body ` ;
assert ( context [ method ] . requestBody , failMessage ) ;
assert ( context [ method ] . requestBody . content , failMessage ) ;
2020-12-04 12:56:55 -05:00
if ( context [ method ] . requestBody . content . hasOwnProperty ( 'application/json' ) ) {
2020-12-16 20:10:15 -05:00
assert ( context [ method ] . requestBody . content [ 'application/json' ] , failMessage ) ;
assert ( context [ method ] . requestBody . content [ 'application/json' ] . schema , failMessage ) ;
assert ( context [ method ] . requestBody . content [ 'application/json' ] . schema . properties , failMessage ) ;
2020-12-04 12:56:55 -05:00
} else if ( context [ method ] . requestBody . content . hasOwnProperty ( 'multipart/form-data' ) ) {
2020-12-16 20:10:15 -05:00
assert ( context [ method ] . requestBody . content [ 'multipart/form-data' ] , failMessage ) ;
assert ( context [ method ] . requestBody . content [ 'multipart/form-data' ] . schema , failMessage ) ;
assert ( context [ method ] . requestBody . content [ 'multipart/form-data' ] . schema . properties , failMessage ) ;
2020-12-04 12:56:55 -05:00
}
2020-10-26 21:51:25 -04:00
}
} ) ;
2021-01-07 14:44:51 -05:00
it ( 'should not error out when called' , async ( ) => {
2020-10-26 21:51:25 -04:00
await setupData ( ) ;
if ( csrfToken ) {
headers [ 'x-csrf-token' ] = csrfToken ;
}
let body = { } ;
2020-12-04 12:56:55 -05:00
let type = 'json' ;
if ( context [ method ] . hasOwnProperty ( 'requestBody' ) && context [ method ] . requestBody . content [ 'application/json' ] ) {
2020-10-26 21:51:25 -04:00
body = buildBody ( context [ method ] . requestBody . content [ 'application/json' ] . schema . properties ) ;
2020-12-04 12:56:55 -05:00
} else if ( context [ method ] . hasOwnProperty ( 'requestBody' ) && context [ method ] . requestBody . content [ 'multipart/form-data' ] ) {
type = 'form' ;
2020-10-26 21:51:25 -04:00
}
try {
2020-12-04 12:56:55 -05:00
if ( type === 'json' ) {
response = await request ( url , {
method : method ,
jar : ! unauthenticatedRoutes . includes ( path ) ? jar : undefined ,
json : true ,
2020-12-17 21:54:38 -05:00
followRedirect : false , // all responses are significant (e.g. 302)
simple : false , // don't throw on non-200 (e.g. 302)
resolveWithFullResponse : true , // send full request back (to check statusCode)
2020-12-04 12:56:55 -05:00
headers : headers ,
qs : qs ,
body : body ,
} ) ;
} else if ( type === 'form' ) {
response = await new Promise ( ( resolve , reject ) => {
2021-02-04 00:01:39 -07:00
helpers . uploadFile ( url , pathLib . join ( _ _dirname , './files/test.png' ) , { } , jar , csrfToken , ( err , res ) => {
2020-12-04 12:56:55 -05:00
if ( err ) {
return reject ( err ) ;
}
2020-12-18 11:23:16 -05:00
resolve ( res ) ;
2020-12-04 12:56:55 -05:00
} ) ;
} ) ;
}
2020-10-26 21:51:25 -04:00
} catch ( e ) {
2021-01-07 14:44:51 -05:00
assert ( ! e , ` ${ method . toUpperCase ( ) } ${ path } errored with: ${ e . message } ` ) ;
2020-10-26 21:51:25 -04:00
}
} ) ;
2020-12-17 21:54:38 -05:00
it ( 'response status code should match one of the schema defined responses' , ( ) => {
// HACK: allow HTTP 418 I am a teapot, for now 👇
assert ( context [ method ] . responses . hasOwnProperty ( '418' ) || Object . keys ( context [ method ] . responses ) . includes ( String ( response . statusCode ) ) , ` ${ method . toUpperCase ( ) } ${ path } sent back unexpected HTTP status code: ${ response . statusCode } ` ) ;
} ) ;
2020-10-26 21:51:25 -04:00
// Recursively iterate through schema properties, comparing type
2020-12-17 21:54:38 -05:00
it ( 'response body should match schema definition' , ( ) => {
const http302 = context [ method ] . responses [ '302' ] ;
if ( http302 && response . statusCode === 302 ) {
// Compare headers instead
const expectedHeaders = Object . keys ( http302 . headers ) . reduce ( ( memo , name ) => {
2020-12-18 12:28:32 -05:00
const value = http302 . headers [ name ] . schema . example ;
memo [ name ] = value . startsWith ( nconf . get ( 'relative_path' ) ) ? value : nconf . get ( 'relative_path' ) + value ;
2020-12-17 21:54:38 -05:00
return memo ;
} , { } ) ;
2021-02-04 01:34:30 -07:00
for ( const header of Object . keys ( expectedHeaders ) ) {
assert ( response . headers [ header . toLowerCase ( ) ] ) ;
assert . strictEqual ( response . headers [ header . toLowerCase ( ) ] , expectedHeaders [ header ] ) ;
2020-12-17 21:54:38 -05:00
}
return ;
}
const http200 = context [ method ] . responses [ '200' ] ;
if ( ! http200 ) {
2020-10-26 21:51:25 -04:00
return ;
}
2021-02-02 12:08:31 -05:00
assert . strictEqual ( response . statusCode , 200 , ` HTTP 200 expected (path: ${ method } ${ path } ` ) ;
2020-12-17 21:54:38 -05:00
const hasJSON = http200 . content && http200 . content [ 'application/json' ] ;
2020-10-26 21:51:25 -04:00
if ( hasJSON ) {
schema = context [ method ] . responses [ '200' ] . content [ 'application/json' ] . schema ;
2020-12-17 21:54:38 -05:00
compare ( schema , response . body , method . toUpperCase ( ) , path , 'root' ) ;
2020-10-26 21:51:25 -04:00
}
// TODO someday: text/csv, binary file type checking?
} ) ;
it ( 'should successfully re-login if needed' , async ( ) => {
2020-11-12 15:52:33 -05:00
const reloginPaths = [ 'PUT /users/{uid}/password' , 'DELETE /users/{uid}/sessions/{uuid}' ] ;
if ( reloginPaths . includes ( ` ${ method . toUpperCase ( ) } ${ path } ` ) ) {
2020-10-26 21:51:25 -04:00
jar = await helpers . loginUser ( 'admin' , '123456' ) ;
2020-11-12 15:52:33 -05:00
const sessionUUIDs = await db . getObject ( 'uid:1:sessionUUID:sessionId' ) ;
mocks . delete [ '/users/{uid}/sessions/{uuid}' ] [ 1 ] . example = Object . keys ( sessionUUIDs ) . pop ( ) ;
2020-10-26 21:51:25 -04:00
// Retrieve CSRF token using cookie, to test Write API
const config = await request ( {
2021-02-03 23:59:08 -07:00
url : ` ${ nconf . get ( 'url' ) } /api/config ` ,
2020-10-26 21:51:25 -04:00
json : true ,
jar : jar ,
} ) ;
csrfToken = config . csrf _token ;
}
} ) ;
2021-06-18 12:15:42 -04:00
it ( 'should back out of a registration interstitial if needed' , async ( ) => {
const affectedPaths = [ 'GET /api/user/{userslug}/edit/email' ] ;
if ( affectedPaths . includes ( ` ${ method . toUpperCase ( ) } ${ path } ` ) ) {
await request ( {
uri : ` ${ nconf . get ( 'url' ) } /register/abort ` ,
method : 'POST' ,
jar ,
simple : false ,
} ) ;
}
} ) ;
2020-10-24 12:41:26 -04:00
} ) ;
2020-04-23 21:50:08 -04:00
} ) ;
2020-10-26 21:51:25 -04:00
}
function buildBody ( schema ) {
return Object . keys ( schema ) . reduce ( ( memo , cur ) => {
memo [ cur ] = schema [ cur ] . example ;
return memo ;
} , { } ) ;
}
2020-10-09 16:38:37 -04:00
2020-10-26 21:51:25 -04:00
function compare ( schema , response , method , path , context ) {
2020-10-24 11:01:53 -04:00
let required = [ ] ;
const additionalProperties = schema . hasOwnProperty ( 'additionalProperties' ) ;
2020-12-29 10:31:25 -05:00
function flattenAllOf ( obj ) {
return obj . reduce ( ( memo , obj ) => {
if ( obj . allOf ) {
obj = { properties : flattenAllOf ( obj . allOf ) } ;
} else {
2021-02-24 12:28:08 -05:00
try {
required = required . concat ( obj . required ? obj . required : Object . keys ( obj . properties ) ) ;
} catch ( e ) {
assert . fail ( ` Syntax error re: allOf, perhaps you allOf'd an array? (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
}
2020-12-29 10:31:25 -05:00
}
return { ... memo , ... obj . properties } ;
2020-10-24 11:01:53 -04:00
} , { } ) ;
2020-12-29 10:31:25 -05:00
}
if ( schema . allOf ) {
schema = flattenAllOf ( schema . allOf ) ;
2020-10-24 11:01:53 -04:00
} else if ( schema . properties ) {
required = schema . required || Object . keys ( schema . properties ) ;
schema = schema . properties ;
} else {
// If schema contains no properties, check passes
return ;
2020-10-13 13:10:49 -04:00
}
2020-10-09 16:38:37 -04:00
2020-10-24 11:01:53 -04:00
// Compare the schema to the response
required . forEach ( ( prop ) => {
if ( schema . hasOwnProperty ( prop ) ) {
2021-02-03 23:59:08 -07:00
assert ( response . hasOwnProperty ( prop ) , ` " ${ prop } " is a required property (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
// 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)
2021-02-03 23:59:08 -07:00
assert ( response [ prop ] !== null , ` " ${ prop } " was null, but schema does not specify it to be a nullable property (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
switch ( schema [ prop ] . type ) {
case 'string' :
2021-02-03 23:59:08 -07:00
assert . strictEqual ( typeof response [ prop ] , 'string' , ` " ${ prop } " was expected to be a string, but was ${ typeof response [ prop ] } instead (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
break ;
case 'boolean' :
2021-02-03 23:59:08 -07:00
assert . strictEqual ( typeof response [ prop ] , 'boolean' , ` " ${ prop } " was expected to be a boolean, but was ${ typeof response [ prop ] } instead (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
break ;
case 'object' :
2021-02-03 23:59:08 -07:00
assert . strictEqual ( typeof response [ prop ] , 'object' , ` " ${ prop } " was expected to be an object, but was ${ typeof response [ prop ] } instead (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-26 21:51:25 -04:00
compare ( schema [ prop ] , response [ prop ] , method , path , context ? [ context , prop ] . join ( '.' ) : prop ) ;
2020-10-24 11:01:53 -04:00
break ;
case 'array' :
2021-02-03 23:59:08 -07:00
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 } ) ` ) ;
2020-10-24 11:01:53 -04:00
if ( schema [ prop ] . items ) {
2020-10-27 14:38:35 -04:00
// Ensure the array items have a schema defined
2021-02-03 23:59:08 -07:00
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 } ) ` ) ;
2020-10-24 11:01:53 -04:00
// Compare types
if ( schema [ prop ] . items . type === 'object' || Array . isArray ( schema [ prop ] . items . allOf ) ) {
response [ prop ] . forEach ( ( res ) => {
2020-10-26 21:51:25 -04:00
compare ( schema [ prop ] . items , res , method , path , context ? [ context , prop ] . join ( '.' ) : prop ) ;
2020-10-24 11:01:53 -04:00
} ) ;
} else if ( response [ prop ] . length ) { // for now
response [ prop ] . forEach ( ( item ) => {
2021-02-03 23:59:08 -07:00
assert . strictEqual ( typeof item , schema [ prop ] . items . type , ` " ${ prop } " should have ${ schema [ prop ] . items . type } items, but found ${ typeof items } instead (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
} ) ;
}
}
break ;
}
}
} ) ;
// Compare the response to the schema
Object . keys ( response ) . forEach ( ( prop ) => {
if ( additionalProperties ) { // All bets are off
return ;
}
2021-02-03 23:59:08 -07:00
assert ( schema [ prop ] , ` " ${ prop } " was found in response, but is not defined in schema (path: ${ method } ${ path } , context: ${ context } ) ` ) ;
2020-10-24 11:01:53 -04:00
} ) ;
}
2020-04-16 20:38:40 -04:00
} ) ;