fix(writeapi): tests

v1.18.x
Julian Lam 4 years ago
parent ec03af7a38
commit b092f65d95

@ -2,117 +2,4 @@ PostsObject:
description: One of the objects in the array returned from `Posts.getPostSummaryByPids` description: One of the objects in the array returned from `Posts.getPostSummaryByPids`
type: array type: array
items: items:
type: object $ref: ./PostObject.yaml#/PostObject
properties:
pid:
type: number
tid:
type: number
description: A topic identifier
content:
type: string
uid:
type: number
description: A user identifier
timestamp:
type: number
deleted:
type: boolean
upvotes:
type: number
downvotes:
type: number
votes:
type: number
timestampISO:
type: string
description: An ISO 8601 formatted date string (complementing `timestamp`)
user:
type: object
properties:
uid:
type: number
description: A user identifier
username:
type: string
description: A friendly name for a given user account
userslug:
type: string
description: An URL-safe variant of the username (i.e. lower-cased, spaces
removed, etc.)
picture:
type: string
nullable: true
status:
type: string
icon:text:
type: string
description: A single-letter representation of a username. This is used in the
auto-generated icon given to users without
an avatar
icon:bgColor:
type: string
description: A six-character hexadecimal colour code assigned to the user. This
value is used in conjunction with
`icon:text` for the user's auto-generated
icon
example: "#f44336"
topic:
type: object
properties:
uid:
type: number
description: A user identifier
tid:
type: number
description: A topic identifier
title:
type: string
cid:
type: number
description: A category identifier
slug:
type: string
deleted:
type: number
postcount:
type: number
mainPid:
type: number
description: The post id of the first post in this topic (also called the
"original post")
teaserPid:
type: number
description: The post id of the teaser (the most recent post, depending on settings)
nullable: true
titleRaw:
type: string
category:
type: object
properties:
cid:
type: number
description: A category identifier
name:
type: string
icon:
type: string
slug:
type: string
parentCid:
type: number
description: The category identifier for the category that is the immediate
ancestor of the current category
bgColor:
type: string
color:
type: string
backgroundImage:
nullable: true
imageClass:
nullable: true
type: string
isMainPost:
type: boolean
replies:
type: number

@ -1,35 +0,0 @@
UserRequest:
properties:
username:
type: string
example: Dragon Fruit
email:
type: string
example: dragonfruit@example.org
fullname:
type: string
example: Mr. Dragon Fruit Jr.
website:
type: string
example: 'https://example.org'
location:
type: string
example: 'Toronto, Canada'
groupTitle:
type: string
example: '["administrators","Staff"]'
birthday:
type: string
description: A birthdate given in an ISO format parseable by the Date object
example: 03/27/2020
signature:
type: string
example: |
This is an example signature
It can span multiple lines.
aboutme:
type: string
example: |
This is a paragraph all about how my life got twist-turned upside-down
and I'd like to take a minute and sit right here,
to tell you all about how I because the administrator of NodeBB

@ -28,6 +28,8 @@ tags:
- name: categories - name: categories
description: Administrative calls to manage categories description: Administrative calls to manage categories
paths: paths:
/users/:
$ref: 'write/users.yaml'
/users/{uid}: /users/{uid}:
$ref: 'write/users/uid.yaml' $ref: 'write/users/uid.yaml'
/users/{uid}/settings: /users/{uid}/settings:
@ -40,12 +42,14 @@ paths:
$ref: 'write/users/uid/follow.yaml' $ref: 'write/users/uid/follow.yaml'
/users/{uid}/ban: /users/{uid}/ban:
$ref: 'write/users/uid/ban.yaml' $ref: 'write/users/uid/ban.yaml'
/users/{uid}/tokens: /users/{uid}/tokens/{token}:
$ref: 'write/users/uid/tokens.yaml' $ref: 'write/users/uid/tokens/token.yaml'
/categories/: /categories/:
$ref: 'write/categories.yaml' $ref: 'write/categories.yaml'
/groups/: /groups/:
$ref: 'write/groups.yaml' $ref: 'write/groups.yaml'
/groups/{slug}:
$ref: 'write/groups/slug.yaml'
/groups/{slug}/membership/{uid}: /groups/{slug}/membership/{uid}:
$ref: 'write/groups/slug/membership/uid.yaml' $ref: 'write/groups/slug/membership/uid.yaml'
/topics: /topics:

@ -12,38 +12,37 @@ post:
properties: properties:
name: name:
type: string type: string
example: My New Category
description: description:
type: string type: string
example: Lorem ipsum, dolor sit amet
parentCid: parentCid:
type: number type: number
example: 0
cloneFromCid: cloneFromCid:
type: number type: number
example: 0
icon: icon:
type: string type: string
example: bullhorn
description: A ForkAwesome icon without the `fa-` prefix description: A ForkAwesome icon without the `fa-` prefix
bgColor: bgColor:
type: string type: string
example: '#ffffff'
color: color:
type: string type: string
example: '#000000'
link: link:
type: string type: string
example: 'https://example.org'
class: class:
type: string type: string
example: 'col-md-3 col-xs-6'
backgroundImage: backgroundImage:
type: string type: string
example: '/assets/relative/path/to/image'
required: required:
- name - name
example:
name: My New Category
description: Lorem ipsum, dolor sit amet
parentCid: 0
cloneFromCid: 0
icon: bullhorn
bgColor: '#ffffff'
color: '#000000'
link: 'https://example.org'
class: 'col-md-3 col-xs-6'
backgroundImage: '/assets/relative/path/to/image'
responses: responses:
'200': '200':
description: category successfully created description: category successfully created

@ -12,6 +12,7 @@ post:
properties: properties:
name: name:
type: string type: string
example: 'My Test Group'
timestamp: timestamp:
type: number type: number
disableJoinRequests: disableJoinRequests:
@ -23,6 +24,7 @@ post:
hidden: hidden:
type: number type: number
enum: [0, 1] enum: [0, 1]
example: 1
ownerUid: ownerUid:
type: number type: number
private: private:
@ -37,9 +39,6 @@ post:
type: number type: number
required: required:
- name - name
example:
name: 'My Test Group'
hidden: 1
responses: responses:
'200': '200':
description: group successfully created description: group successfully created
@ -51,22 +50,4 @@ post:
status: status:
$ref: ../components/schemas/Status.yaml#/Status $ref: ../components/schemas/Status.yaml#/Status
response: response:
$ref: ../components/schemas/GroupObject.yaml#/GroupDataObject $ref: ../components/schemas/GroupObject.yaml#/GroupDataObject
delete:
tags:
- groups
summary: Delete an existing group
description: This operation deletes an existing group, all members within this group will cease to be members after the group is deleted.
responses:
'200':
description: group successfully deleted
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../components/schemas/Status.yaml#/Status
response:
type: object
properties: {}

@ -10,10 +10,50 @@ put:
type: string type: string
required: true required: true
description: slug of the group you would like to join description: slug of the group you would like to join
example: my-group example: test-group
- in: path
name: uid
schema:
type: number
required: true
description: uid of the user to join the group
example: 1
responses: responses:
'200': '200':
description: group successfully joined, or membership requested description: group successfully joined, or membership requested
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../../components/schemas/Status.yaml#/Status
response:
type: object
properties: {}
delete:
tags:
- groups
summary: leave a group
description: This operation leaves a group.
parameters:
- in: path
name: slug
schema:
type: string
required: true
description: slug of the group you would like to leave
example: test-group
- in: path
name: uid
schema:
type: number
required: true
description: uid of the user to leave the group
example: 1
responses:
'200':
description: group successfully left
content: content:
application/json: application/json:
schema: schema:

@ -21,14 +21,13 @@ put:
content: content:
type: string type: string
description: New post content description: New post content
example: New post content
title: title:
type: string type: string
description: Topic title, only accepted for main posts description: Topic title, only accepted for main posts
example: New title
required: required:
- content - content
example:
content: 'New post content'
title: 'New title'
responses: responses:
'200': '200':
description: Post successfully edited description: Post successfully edited
@ -40,7 +39,7 @@ put:
status: status:
$ref: ../../components/schemas/Status.yaml#/Status $ref: ../../components/schemas/Status.yaml#/Status
response: response:
$ref: ../../components/schemas/PostsObject.yaml#/PostsObject $ref: ../../components/schemas/PostObject.yaml#/PostObject
delete: delete:
tags: tags:
- posts - posts

@ -12,22 +12,22 @@ post:
properties: properties:
cid: cid:
type: number type: number
example: 1
title: title:
type: string type: string
example: Test topic
content: content:
type: string type: string
example: This is the test topic's content
tags: tags:
type: array type: array
items: items:
type: string type: string
example: [test, topic]
required: required:
- cid - cid
- title - title
- content - content
example:
cid: 1
title: Test topic
content: This is the test topic's content
responses: responses:
'200': '200':
description: topic successfully created description: topic successfully created
@ -39,4 +39,8 @@ post:
status: status:
$ref: ../components/schemas/Status.yaml#/Status $ref: ../components/schemas/Status.yaml#/Status
response: response:
$ref: ../components/schemas/TopicObject.yaml#/TopicObject allOf:
- $ref: ../components/schemas/TopicObject.yaml#/TopicObject
- type: object
properties:
mainPost: {}

@ -1,7 +1,7 @@
post: post:
tags: tags:
- topics - topics
summary: peply to a topic summary: reply to a topic
description: This operation creates a new reply to an existing topic. description: This operation creates a new reply to an existing topic.
parameters: parameters:
- in: path - in: path
@ -10,7 +10,7 @@ post:
type: string type: string
required: true required: true
description: a valid topic id description: a valid topic id
example: 1 example: 2
requestBody: requestBody:
required: true required: true
content: content:
@ -20,14 +20,13 @@ post:
properties: properties:
content: content:
type: string type: string
example: This is a test reply
timestamp: timestamp:
type: number type: number
toPid: toPid:
type: number type: number
required: required:
- content - content
example:
content: This is a test reply
responses: responses:
'200': '200':
description: post successfully created description: post successfully created
@ -39,7 +38,7 @@ post:
status: status:
$ref: ../../components/schemas/Status.yaml#/Status $ref: ../../components/schemas/Status.yaml#/Status
response: response:
$ref: ../../components/schemas/PostsObject.yaml#/PostsObject $ref: ../../components/schemas/PostObject.yaml#/PostObject
delete: delete:
tags: tags:
- topics - topics
@ -52,7 +51,7 @@ delete:
type: string type: string
required: true required: true
description: a valid topic id description: a valid topic id
example: 1 example: 2
responses: responses:
'200': '200':
description: Topic successfully purged description: Topic successfully purged

@ -23,10 +23,7 @@ put:
description: 'An array of tags' description: 'An array of tags'
items: items:
type: string type: string
example: example: [test, foobar]
tags:
- test
- foobar
responses: responses:
'200': '200':
description: Topic tags successfully added description: Topic tags successfully added

@ -13,16 +13,15 @@ post:
username: username:
type: string type: string
description: 'If the username is taken, a number will be appended' description: 'If the username is taken, a number will be appended'
example: Dragon Fruit
password: password:
type: string type: string
example: s3cre7password
email: email:
type: string type: string
example: dragonfruit@example.org
required: required:
- username - username
example:
username: Dragon Fruit
password: s3cre7password
email: dragonfruit@example.org
responses: responses:
'200': '200':
description: user successfully created description: user successfully created
@ -62,11 +61,7 @@ delete:
description: A collection of uids description: A collection of uids
items: items:
type: number type: number
example: example: [5, 6]
uids:
- 1
- 2
- 3
responses: responses:
'200': '200':
description: user account(s) deleted description: user account(s) deleted

@ -9,7 +9,7 @@ delete:
type: integer type: integer
required: true required: true
description: uid of the user to delete description: uid of the user to delete
example: 1 example: 3
responses: responses:
'200': '200':
description: user account deleted description: user account deleted
@ -39,7 +39,35 @@ put:
content: content:
application/json: application/json:
schema: schema:
$ref: ../../components/schemas/UserRequest.yaml#/UserRequest type: object
properties:
fullname:
type: string
example: Mr. Dragon Fruit Jr.
website:
type: string
example: 'https://example.org'
location:
type: string
example: 'Toronto, Canada'
groupTitle:
type: string
example: '["administrators","Staff"]'
birthday:
type: string
description: A birthdate given in an ISO format parseable by the Date object
example: 03/27/2020
signature:
type: string
example: |
This is an example signature
It can span multiple lines.
aboutme:
type: string
example: |
This is a paragraph all about how my life got twist-turned upside-down
and I'd like to take a minute and sit right here,
to tell you all about how I because the administrator of NodeBB
responses: responses:
'200': '200':
description: user profile updated description: user profile updated

@ -9,7 +9,7 @@ put:
type: integer type: integer
required: true required: true
description: uid of the user to ban description: uid of the user to ban
example: 1 example: 2
requestBody: requestBody:
content: content:
application/json: application/json:
@ -46,7 +46,7 @@ delete:
type: integer type: integer
required: true required: true
description: uid of the user to unban description: uid of the user to unban
example: 1 example: 2
responses: responses:
'200': '200':
description: successfully unbanned user description: successfully unbanned user

@ -1,4 +1,4 @@
post: put:
tags: tags:
- users - users
summary: follow a user summary: follow a user
@ -9,7 +9,7 @@ post:
type: integer type: integer
required: true required: true
description: uid of the user to follow description: uid of the user to follow
example: 1 example: 2
responses: responses:
'200': '200':
description: successfully followed user description: successfully followed user
@ -33,7 +33,7 @@ delete:
type: integer type: integer
required: true required: true
description: uid of the user to unfollow description: uid of the user to unfollow
example: 1 example: 2
responses: responses:
'200': '200':
description: successfully unfollowed user description: successfully unfollowed user

@ -20,10 +20,10 @@ put:
currentPassword: currentPassword:
type: string type: string
description: test description: test
example: oldp455word example: '123456'
newPassword: newPassword:
type: string type: string
example: s3cre7password example: '123456'
required: required:
- newPassword - newPassword
responses: responses:

@ -3,33 +3,17 @@ post:
- users - users
summary: generate a user token summary: generate a user token
description: This route can only be used to generate tokens for the same user. In other words, you cannot use this route to generate a token for a different user than the one you are authenticated as. description: This route can only be used to generate tokens for the same user. In other words, you cannot use this route to generate a token for a different user than the one you are authenticated as.
responses:
'200':
description: successfully generated a user token
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
delete:
tags:
- users
summary: delete user token
parameters: parameters:
- in: path - in: path
name: token name: uid
schema: schema:
type: string type: integer
required: true required: true
description: a valid API token description: uid of the user to generate a token for
example: 6d03a630-86fd-4515-9a35-e957502f4f89 example: 1
responses: responses:
'200': '200':
description: successfully deleted user token description: successfully generated a user token
content: content:
application/json: application/json:
schema: schema:

@ -10,6 +10,7 @@ const wait = util.promisify(setTimeout);
const db = require('./mocks/databasemock'); const db = require('./mocks/databasemock');
const helpers = require('./helpers'); const helpers = require('./helpers');
const meta = require('../src/meta');
const user = require('../src/user'); const user = require('../src/user');
const groups = require('../src/groups'); const groups = require('../src/groups');
const categories = require('../src/categories'); const categories = require('../src/categories');
@ -17,7 +18,7 @@ const topics = require('../src/topics');
const plugins = require('../src/plugins'); const plugins = require('../src/plugins');
const flags = require('../src/flags'); const flags = require('../src/flags');
const messaging = require('../src/messaging'); const messaging = require('../src/messaging');
const utils = require('../src/utils');
describe('Read API', async () => { describe('Read API', async () => {
let readApi = false; let readApi = false;
@ -25,9 +26,30 @@ describe('Read API', async () => {
const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml'); const readApiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml'); const writeApiPath = path.resolve(__dirname, '../public/openapi/write.yaml');
let jar; let jar;
let csrfToken;
let setup = false; let setup = false;
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user 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) { async function dummySearchHook(data) {
return [1]; return [1];
} }
@ -44,8 +66,26 @@ describe('Read API', async () => {
// Create sample users // Create sample users
const adminUid = await user.create({ username: 'admin', password: '123456', email: 'test@example.org' }); 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' }); 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); 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 // Create a category
const testCategory = await categories.create({ name: 'test' }); const testCategory = await categories.create({ name: 'test' });
@ -78,6 +118,15 @@ describe('Read API', async () => {
}); });
jar = await helpers.loginUser('admin', '123456'); 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; setup = true;
} }
@ -93,87 +142,133 @@ describe('Read API', async () => {
readApi = await SwaggerParser.dereference(readApiPath); readApi = await SwaggerParser.dereference(readApiPath);
writeApi = await SwaggerParser.dereference(writeApiPath); 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 // generateTests(readApi, Object.keys(readApi.paths));
// const paths = Object.keys(writeApi.paths); generateTests(writeApi, Object.keys(writeApi.paths), writeApi.servers[0].url);
const paths = Object.keys(readApi.paths);
function generateTests(api, paths, prefix) {
paths.forEach((path) => { // Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
// const context = writeApi.paths[path]; paths.forEach((path) => {
const context = readApi.paths[path]; const context = api.paths[path];
let schema; let schema;
let response; let response;
let url; let url;
let method; let method;
const headers = {}; const headers = {};
const qs = {}; const qs = {};
Object.keys(context).forEach((_method) => { Object.keys(context).forEach((_method) => {
if (_method !== 'get') { // if (_method !== 'get') {
return; // return;
} // }
it('should have examples when parameters are present', () => { it('should have examples when parameters are present', () => {
method = _method; method = _method;
const parameters = context[method].parameters; let parameters = context[method].parameters;
let testPath = path; let testPath = path;
if (parameters) { if (parameters) {
parameters.forEach((param) => { // Use mock data if provided
assert(param.example !== null && param.example !== undefined, `${method.toUpperCase()} ${path} has parameters without examples`); parameters = mocks[method][path] || parameters;
switch (param.in) { parameters.forEach((param) => {
case 'path': assert(param.example !== null && param.example !== undefined, `${method.toUpperCase()} ${path} has parameters without examples`);
testPath = testPath.replace('{' + param.name + '}', param.example);
break; switch (param.in) {
case 'header': case 'path':
headers[param.name] = param.example; testPath = testPath.replace('{' + param.name + '}', param.example);
break; break;
case 'query': case 'header':
qs[param.name] = param.example; headers[param.name] = param.example;
break; break;
} case 'query':
}); qs[param.name] = param.example;
} break;
}
url = nconf.get('url') + testPath; });
}); }
it('should resolve with a 200 when called', async () => { url = nconf.get('url') + prefix + testPath;
await setupData(); });
try { it('may contain a request body with application/json type if POST/PUT/DELETE', () => {
response = await request(url, { if (['post', 'put', 'delete'].includes(method) && context[method].hasOwnProperty('requestBody')) {
method: method, assert(context[method].requestBody);
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined, assert(context[method].requestBody.content);
json: true, assert(context[method].requestBody.content['application/json']);
headers: headers, assert(context[method].requestBody.content['application/json'].schema);
qs: qs, assert(context[method].requestBody.content['application/json'].schema.properties);
}); }
} catch (e) { });
assert(!e, `${method.toUpperCase()} ${path} resolved with ${e.message}`);
} it('should resolve with a 200 when called', async () => {
}); await setupData();
console.log(response);
if (csrfToken) {
// Recursively iterate through schema properties, comparing type headers['x-csrf-token'] = csrfToken;
it('response should match schema definition', () => { }
const has200 = context[method].responses['200'];
if (!has200) { let body = {};
return; if (context[method].hasOwnProperty('requestBody')) {
} body = buildBody(context[method].requestBody.content['application/json'].schema.properties);
}
const hasJSON = has200.content && has200.content['application/json'];
if (hasJSON) { try {
schema = context[method].responses['200'].content['application/json'].schema; // console.log(`calling ${method} ${url} with`, body);
compare(schema, response, 'root'); response = await request(url, {
} method: method,
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
// TODO someday: text/csv, binary file type checking? 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 = []; let required = [];
const additionalProperties = schema.hasOwnProperty('additionalProperties'); const additionalProperties = schema.hasOwnProperty('additionalProperties');
@ -194,7 +289,7 @@ describe('Read API', async () => {
// Compare the schema to the response // Compare the schema to the response
required.forEach((prop) => { required.forEach((prop) => {
if (schema.hasOwnProperty(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) // 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) { 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) // 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) { switch (schema[prop].type) {
case 'string': 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; break;
case 'boolean': 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; break;
case 'object': 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 + ')'); 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], context ? [context, prop].join('.') : prop); compare(schema[prop], response[prop], method, path, context ? [context, prop].join('.') : prop);
break; break;
case 'array': 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) { if (schema[prop].items) {
// Ensure the array items have a schema defined // 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 // Compare types
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) { if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
response[prop].forEach((res) => { 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 } else if (response[prop].length) { // for now
response[prop].forEach((item) => { response[prop].forEach((item) => {

Loading…
Cancel
Save