From b092f65d95af60aea8d42f8b328e9b4accfd6003 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 26 Oct 2020 21:51:25 -0400 Subject: [PATCH] fix(writeapi): tests --- .../components/schemas/PostsObject.yaml | 115 +------- .../components/schemas/UserRequest.yaml | 35 --- public/openapi/write.yaml | 8 +- public/openapi/write/categories.yaml | 21 +- public/openapi/write/groups.yaml | 25 +- .../write/groups/slug/membership/uid.yaml | 42 ++- public/openapi/write/posts/pid.yaml | 7 +- public/openapi/write/topics.yaml | 14 +- public/openapi/write/topics/tid.yaml | 11 +- public/openapi/write/topics/tid/tags.yaml | 5 +- public/openapi/write/users.yaml | 13 +- public/openapi/write/users/uid.yaml | 32 ++- public/openapi/write/users/uid/ban.yaml | 4 +- public/openapi/write/users/uid/follow.yaml | 6 +- public/openapi/write/users/uid/password.yaml | 4 +- public/openapi/write/users/uid/tokens.yaml | 26 +- test/api.js | 271 ++++++++++++------ 17 files changed, 308 insertions(+), 331 deletions(-) delete mode 100644 public/openapi/components/schemas/UserRequest.yaml diff --git a/public/openapi/components/schemas/PostsObject.yaml b/public/openapi/components/schemas/PostsObject.yaml index 5c68196b2c..b43d965888 100644 --- a/public/openapi/components/schemas/PostsObject.yaml +++ b/public/openapi/components/schemas/PostsObject.yaml @@ -2,117 +2,4 @@ PostsObject: description: One of the objects in the array returned from `Posts.getPostSummaryByPids` type: array items: - type: object - 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 \ No newline at end of file + $ref: ./PostObject.yaml#/PostObject \ No newline at end of file diff --git a/public/openapi/components/schemas/UserRequest.yaml b/public/openapi/components/schemas/UserRequest.yaml deleted file mode 100644 index 8181b52743..0000000000 --- a/public/openapi/components/schemas/UserRequest.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 982a26a8c1..3d880ce060 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -28,6 +28,8 @@ tags: - name: categories description: Administrative calls to manage categories paths: + /users/: + $ref: 'write/users.yaml' /users/{uid}: $ref: 'write/users/uid.yaml' /users/{uid}/settings: @@ -40,12 +42,14 @@ paths: $ref: 'write/users/uid/follow.yaml' /users/{uid}/ban: $ref: 'write/users/uid/ban.yaml' - /users/{uid}/tokens: - $ref: 'write/users/uid/tokens.yaml' + /users/{uid}/tokens/{token}: + $ref: 'write/users/uid/tokens/token.yaml' /categories/: $ref: 'write/categories.yaml' /groups/: $ref: 'write/groups.yaml' + /groups/{slug}: + $ref: 'write/groups/slug.yaml' /groups/{slug}/membership/{uid}: $ref: 'write/groups/slug/membership/uid.yaml' /topics: diff --git a/public/openapi/write/categories.yaml b/public/openapi/write/categories.yaml index 833c906602..dc90d8f41f 100644 --- a/public/openapi/write/categories.yaml +++ b/public/openapi/write/categories.yaml @@ -12,38 +12,37 @@ post: properties: name: type: string + example: My New Category description: type: string + example: Lorem ipsum, dolor sit amet parentCid: type: number + example: 0 cloneFromCid: type: number + example: 0 icon: type: string + example: bullhorn description: A ForkAwesome icon without the `fa-` prefix bgColor: type: string + example: '#ffffff' color: type: string + example: '#000000' link: type: string + example: 'https://example.org' class: type: string + example: 'col-md-3 col-xs-6' backgroundImage: type: string + example: '/assets/relative/path/to/image' required: - 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: '200': description: category successfully created diff --git a/public/openapi/write/groups.yaml b/public/openapi/write/groups.yaml index 4116201045..8ae4d703f8 100644 --- a/public/openapi/write/groups.yaml +++ b/public/openapi/write/groups.yaml @@ -12,6 +12,7 @@ post: properties: name: type: string + example: 'My Test Group' timestamp: type: number disableJoinRequests: @@ -23,6 +24,7 @@ post: hidden: type: number enum: [0, 1] + example: 1 ownerUid: type: number private: @@ -37,9 +39,6 @@ post: type: number required: - name - example: - name: 'My Test Group' - hidden: 1 responses: '200': description: group successfully created @@ -51,22 +50,4 @@ post: status: $ref: ../components/schemas/Status.yaml#/Status response: - $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: {} \ No newline at end of file + $ref: ../components/schemas/GroupObject.yaml#/GroupDataObject \ No newline at end of file diff --git a/public/openapi/write/groups/slug/membership/uid.yaml b/public/openapi/write/groups/slug/membership/uid.yaml index 43bbb0968d..9a5fd7df47 100644 --- a/public/openapi/write/groups/slug/membership/uid.yaml +++ b/public/openapi/write/groups/slug/membership/uid.yaml @@ -10,10 +10,50 @@ put: type: string required: true 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: '200': 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: application/json: schema: diff --git a/public/openapi/write/posts/pid.yaml b/public/openapi/write/posts/pid.yaml index bc0f531671..28a6bb0bde 100644 --- a/public/openapi/write/posts/pid.yaml +++ b/public/openapi/write/posts/pid.yaml @@ -21,14 +21,13 @@ put: content: type: string description: New post content + example: New post content title: type: string description: Topic title, only accepted for main posts + example: New title required: - content - example: - content: 'New post content' - title: 'New title' responses: '200': description: Post successfully edited @@ -40,7 +39,7 @@ put: status: $ref: ../../components/schemas/Status.yaml#/Status response: - $ref: ../../components/schemas/PostsObject.yaml#/PostsObject + $ref: ../../components/schemas/PostObject.yaml#/PostObject delete: tags: - posts diff --git a/public/openapi/write/topics.yaml b/public/openapi/write/topics.yaml index 5ba1a9809d..ba00cf0024 100644 --- a/public/openapi/write/topics.yaml +++ b/public/openapi/write/topics.yaml @@ -12,22 +12,22 @@ post: properties: cid: type: number + example: 1 title: type: string + example: Test topic content: type: string + example: This is the test topic's content tags: type: array items: type: string + example: [test, topic] required: - cid - title - content - example: - cid: 1 - title: Test topic - content: This is the test topic's content responses: '200': description: topic successfully created @@ -39,4 +39,8 @@ post: status: $ref: ../components/schemas/Status.yaml#/Status response: - $ref: ../components/schemas/TopicObject.yaml#/TopicObject \ No newline at end of file + allOf: + - $ref: ../components/schemas/TopicObject.yaml#/TopicObject + - type: object + properties: + mainPost: {} \ No newline at end of file diff --git a/public/openapi/write/topics/tid.yaml b/public/openapi/write/topics/tid.yaml index d4b6a801e3..adc5e02fe0 100644 --- a/public/openapi/write/topics/tid.yaml +++ b/public/openapi/write/topics/tid.yaml @@ -1,7 +1,7 @@ post: tags: - topics - summary: peply to a topic + summary: reply to a topic description: This operation creates a new reply to an existing topic. parameters: - in: path @@ -10,7 +10,7 @@ post: type: string required: true description: a valid topic id - example: 1 + example: 2 requestBody: required: true content: @@ -20,14 +20,13 @@ post: properties: content: type: string + example: This is a test reply timestamp: type: number toPid: type: number required: - content - example: - content: This is a test reply responses: '200': description: post successfully created @@ -39,7 +38,7 @@ post: status: $ref: ../../components/schemas/Status.yaml#/Status response: - $ref: ../../components/schemas/PostsObject.yaml#/PostsObject + $ref: ../../components/schemas/PostObject.yaml#/PostObject delete: tags: - topics @@ -52,7 +51,7 @@ delete: type: string required: true description: a valid topic id - example: 1 + example: 2 responses: '200': description: Topic successfully purged diff --git a/public/openapi/write/topics/tid/tags.yaml b/public/openapi/write/topics/tid/tags.yaml index 8f8624959e..9f229d9707 100644 --- a/public/openapi/write/topics/tid/tags.yaml +++ b/public/openapi/write/topics/tid/tags.yaml @@ -23,10 +23,7 @@ put: description: 'An array of tags' items: type: string - example: - tags: - - test - - foobar + example: [test, foobar] responses: '200': description: Topic tags successfully added diff --git a/public/openapi/write/users.yaml b/public/openapi/write/users.yaml index 1a0708fb3f..d5fcdf30e6 100644 --- a/public/openapi/write/users.yaml +++ b/public/openapi/write/users.yaml @@ -13,16 +13,15 @@ post: username: type: string description: 'If the username is taken, a number will be appended' + example: Dragon Fruit password: type: string + example: s3cre7password email: type: string + example: dragonfruit@example.org required: - username - example: - username: Dragon Fruit - password: s3cre7password - email: dragonfruit@example.org responses: '200': description: user successfully created @@ -62,11 +61,7 @@ delete: description: A collection of uids items: type: number - example: - uids: - - 1 - - 2 - - 3 + example: [5, 6] responses: '200': description: user account(s) deleted diff --git a/public/openapi/write/users/uid.yaml b/public/openapi/write/users/uid.yaml index 7f46b0e37a..f615c105a8 100644 --- a/public/openapi/write/users/uid.yaml +++ b/public/openapi/write/users/uid.yaml @@ -9,7 +9,7 @@ delete: type: integer required: true description: uid of the user to delete - example: 1 + example: 3 responses: '200': description: user account deleted @@ -39,7 +39,35 @@ put: content: application/json: 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: '200': description: user profile updated diff --git a/public/openapi/write/users/uid/ban.yaml b/public/openapi/write/users/uid/ban.yaml index 3532e28bbf..624c6e8eb7 100644 --- a/public/openapi/write/users/uid/ban.yaml +++ b/public/openapi/write/users/uid/ban.yaml @@ -9,7 +9,7 @@ put: type: integer required: true description: uid of the user to ban - example: 1 + example: 2 requestBody: content: application/json: @@ -46,7 +46,7 @@ delete: type: integer required: true description: uid of the user to unban - example: 1 + example: 2 responses: '200': description: successfully unbanned user diff --git a/public/openapi/write/users/uid/follow.yaml b/public/openapi/write/users/uid/follow.yaml index acdff6a09e..a993985333 100644 --- a/public/openapi/write/users/uid/follow.yaml +++ b/public/openapi/write/users/uid/follow.yaml @@ -1,4 +1,4 @@ -post: +put: tags: - users summary: follow a user @@ -9,7 +9,7 @@ post: type: integer required: true description: uid of the user to follow - example: 1 + example: 2 responses: '200': description: successfully followed user @@ -33,7 +33,7 @@ delete: type: integer required: true description: uid of the user to unfollow - example: 1 + example: 2 responses: '200': description: successfully unfollowed user diff --git a/public/openapi/write/users/uid/password.yaml b/public/openapi/write/users/uid/password.yaml index a58ada8238..1a52f85e53 100644 --- a/public/openapi/write/users/uid/password.yaml +++ b/public/openapi/write/users/uid/password.yaml @@ -20,10 +20,10 @@ put: currentPassword: type: string description: test - example: oldp455word + example: '123456' newPassword: type: string - example: s3cre7password + example: '123456' required: - newPassword responses: diff --git a/public/openapi/write/users/uid/tokens.yaml b/public/openapi/write/users/uid/tokens.yaml index 35be1af348..49b7e39185 100644 --- a/public/openapi/write/users/uid/tokens.yaml +++ b/public/openapi/write/users/uid/tokens.yaml @@ -3,33 +3,17 @@ post: - users 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. - 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: - in: path - name: token + name: uid schema: - type: string + type: integer required: true - description: a valid API token - example: 6d03a630-86fd-4515-9a35-e957502f4f89 + description: uid of the user to generate a token for + example: 1 responses: '200': - description: successfully deleted user token + description: successfully generated a user token content: application/json: schema: diff --git a/test/api.js b/test/api.js index 4e65e29245..d26c60ff9f 100644 --- a/test/api.js +++ b/test/api.js @@ -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) => {