From b56a910f5b77a41a4ec983f8905416d23067cfac Mon Sep 17 00:00:00 2001 From: Ian Goldstein Date: Tue, 27 Dec 2016 14:56:53 -0800 Subject: [PATCH 1/5] Fix the groupings (token/tokens/users) in the swagger-generated API documentation. (#154) --- api/v1/swagger.yaml | 54 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/api/v1/swagger.yaml b/api/v1/swagger.yaml index a7fed8ccc5..f1c0fdbae2 100644 --- a/api/v1/swagger.yaml +++ b/api/v1/swagger.yaml @@ -898,7 +898,7 @@ paths: security: - jwt: [] summary: Get the list of all authorized writers for an aspect - tags: [ aspects ] + tags: [ aspects, writers ] description: >- Get the list of all authorized writers for an aspect. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getAspectWriters @@ -933,7 +933,7 @@ paths: security: - jwt: [] summary: Add one or more users to an aspect’s list of authorized writers - tags: [ aspects ] + tags: [ aspects, writers ] description: >- Add one or more users to an aspect’s list of authorized writers. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/token`. operationId: postAspectWriters @@ -983,7 +983,7 @@ paths: security: - jwt: [] summary: Determine whether a user is an authorized writer for an aspect - tags: [ aspects ] + tags: [ aspects, writers ] description: >- Determine whether a user is an authorized writer for an aspect. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getAspectWriter @@ -1059,10 +1059,10 @@ paths: post: security: - jwt: [] - summary: Create a new global config item + summary: Create a new global config item (admin only) tags: [ globalconfig ] description: >- - Create a new global config item. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. + Create a new global config item. Requires user to have an admin profile. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: postGlobalConfig parameters: - @@ -1102,10 +1102,10 @@ paths: delete: security: - jwt: [] - summary: Delete the specified global config item + summary: Delete the specified global config item (admin only) tags: [ globalconfig ] description: >- - Delete the specified global config item. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. + Delete the specified global config item. Requires user to have an admin profile. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: deleteGlobalConfig parameters: - @@ -1158,10 +1158,10 @@ paths: patch: security: - jwt: [] - summary: Update the specified global config item + summary: Update the specified global config item (admin only) tags: [ globalconfig ] description: >- - Update the specified global config item. If a field is not included in the query body, that field will not be updated. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. + Update the specified global config item. Requires user to have an admin profile. If a field is not included in the query body, that field will not be updated. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: patchGlobalConfig parameters: - @@ -1612,7 +1612,7 @@ paths: security: - jwt: [] summary: Get the list of all authorized writers for a lens - tags: [ lenses ] + tags: [ lenses, writers ] description: >- Get the list of all authorized writers for a lens. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getLensWriters @@ -1647,7 +1647,7 @@ paths: security: - jwt: [] summary: Add one or more users to a lens' list of authorized writers - tags: [ lenses ] + tags: [ lenses, writers ] description: >- Add one or more users to a lens' list of authorized writers. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/token`. operationId: postLensWriters @@ -1697,7 +1697,7 @@ paths: security: - jwt: [] summary: Determine whether a user is an authorized writer for a lens - tags: [ lenses ] + tags: [ lenses, writers ] description: >- Determine whether a user is an authorized writer for lens. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getLensWriter @@ -2194,7 +2194,7 @@ paths: security: - jwt: [] summary: Get the list of all authorized writers for a perspective - tags: [ perspectives ] + tags: [ perspectives, writers ] description: >- Get the list of all authorized writers for a perspective. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getPerspectiveWriters @@ -2229,7 +2229,7 @@ paths: security: - jwt: [] summary: Add one or more users to a perspective's list of authorized writers - tags: [ perspectives ] + tags: [ perspectives, writers ] description: >- Add one or more users to a perspective's list of authorized writers. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/token`. operationId: postPerspectiveWriters @@ -2278,7 +2278,7 @@ paths: security: - jwt: [] summary: Determine whether a user is an authorized writer for a perspective - tags: [ perspectives ] + tags: [ perspectives, writers ] description: >- Determine whether a user is an authorized writer for perspective. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getPerspectiveWriter @@ -3340,10 +3340,10 @@ paths: delete: security: - jwt: [] - summary: Delete the specified related link of the specified sample. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. + summary: Delete the specified related link of the specified sample tags: [ samples ] description: >- - Delete the specified related link of the specified sample. + Delete the specified related link of the specified sample. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: deleteSampleRelatedLinks parameters: - @@ -4267,7 +4267,7 @@ paths: security: - jwt: [] summary: Get the list of all authorized writers for a subject - tags: [ subjects ] + tags: [ subjects, writers ] description: >- Get the list of all authorized writers for a subject. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getSubjectWriters @@ -4302,7 +4302,7 @@ paths: security: - jwt: [] summary: Add one or more users to a subjects list of authorized writers - tags: [ subjects ] + tags: [ subjects, writers ] description: >- Add one or more users to a subjects list of authorized writers. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/token`. operationId: postSubjectWriters @@ -4352,7 +4352,7 @@ paths: security: - jwt: [] summary: Determine whether a user is an authorized writer for a subject - tags: [ subjects ] + tags: [ subjects, writers ] description: >- Determine whether a user is an authorized writer for subject. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an `Authorization` request header with your [JSON Web Token](https://tools.ietf.org/html/rfc7519) (JWT) as the value. You can get a token using `POST /v1/register` or `POST /v1/tokens`. operationId: getSubjectWriter @@ -5030,7 +5030,7 @@ paths: security: - jwt: [] summary: Create a new API access token - tags: [ token ] + tags: [ tokens ] description: >- Create a new API access token by providing a token in header. If the Refocus configuration parameter `useAccessToken` is set to @@ -5198,7 +5198,7 @@ paths: security: - jwt: [] summary: Revoke access for the specified token - tags: [ token ] + tags: [ tokens ] description: >- Revoke access for the specified token. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an @@ -5252,7 +5252,7 @@ paths: security: - jwt: [] summary: Restore access for the specified token - tags: [ token ] + tags: [ tokens ] description: >- Restore access for the specified token if access had previously been revoked. If the Refocus configuration parameter `useAccessToken` is set @@ -5306,7 +5306,7 @@ paths: security: - jwt: [] summary: Retrieve metadata about the specified token. - tags: [ tokens ] + tags: [ users, tokens ] description: >- Retrieve metadata about the users' tokens. You may also optionally specify a list of fields to include in the response. If the Refocus @@ -5364,7 +5364,7 @@ paths: security: - jwt: [] summary: Delete the specified token - tags: [ tokens ] + tags: [ users, tokens ] description: >- Given a user name and token name, delete the specified token. operationId: deleteUserToken @@ -5485,7 +5485,7 @@ paths: security: - jwt: [] summary: Revoke access for the specified token - tags: [ token ] + tags: [ users, tokens ] description: >- Revoke access for the specified token. If the Refocus configuration parameter `useAccessToken` is set to `true`, you must include an @@ -5546,7 +5546,7 @@ paths: security: - jwt: [] summary: Restore access for the specified token - tags: [ token ] + tags: [ users, tokens ] description: >- Restore access for the specified token if access had previously been revoked. If the Refocus configuration parameter `useAccessToken` is set From 974f66ee971de1726cdfcd06add912e996b0f524 Mon Sep 17 00:00:00 2001 From: Ian Goldstein Date: Wed, 28 Dec 2016 09:13:02 -0800 Subject: [PATCH 2/5] =?UTF-8?q?Get=20rid=20of=20unnecessary=20=E2=80=9Ctes?= =?UTF-8?q?t=E2=80=9D=20environment=20(was=20only=20being=20used=20for=20v?= =?UTF-8?q?iew=20tests)=E2=80=94just=20use=20the=20=E2=80=9Cbuild=E2=80=9D?= =?UTF-8?q?=20env=20for=20this.=20(#153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.js | 17 ----------------- package.json | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/config.js b/config.js index 33a0c11b98..72c1d42496 100644 --- a/config.js +++ b/config.js @@ -13,7 +13,6 @@ */ 'use strict'; // eslint-disable-line strict require('./config/toggles'); // Loads the feature toggles -const featureToggles = require('feature-toggles'); const configUtil = require('./config/configUtil'); const defaultPort = 3000; const defaultPostgresPort = 5432; @@ -132,22 +131,6 @@ module.exports = { tokenSecret: pe.SECRET_TOKEN || '7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e', }, - test: { - checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS || - DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS, - dbLogging: false, // console.log | false | ... - dbUrl: pe.DATABASE_URL, - redisUrl: pe.REDIS_URL, - defaultNodePort: defaultPort, - ipWhitelist: iplist, - dialect: 'postgres', - protocol: 'postgres', - dialectOptions: { - ssl: true, - }, - tokenSecret: pe.SECRET_TOKEN || - '7265666f637573726f636b7377697468677265656e6f776c7373616e6672616e', - }, testWhitelistLocalhost: { checkTimeoutIntervalMillis: pe.CHECK_TIMEOUT_INTERVAL_MILLIS || DEFAULT_CHECK_TIMEOUT_INTERVAL_MILLIS, diff --git a/package.json b/package.json index 8328835143..08d8ad0897 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "test-disablehttp": "DISABLE_HTTP=true mocha -R dot --recursive tests/disableHttp", "test-enforced": "USE_ACCESS_TOKEN=true mocha -R dot --recursive tests/enforceToken", "test-db": "npm run checkdb && mocha -R dot --recursive tests/db", - "test-view": "NODE_ENV=test mocha -R dot --recursive --compilers js:babel-core/register --require ./tests/view/setup.js tests/view", + "test-view": "NODE_ENV=build mocha -R dot --recursive --compilers js:babel-core/register --require ./tests/view/setup.js tests/view", "test": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R dot --recursive tests/api tests/config tests/db tests/jobQueue tests/realtime tests/tokenNotReq && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage && npm run test-view && npm run test-disablehttp && npm run test-enforced", "undo-migratedb": "node db/migrateUndo.js", "view": "NODE_ENV=production gulp browserifyViews && npm start" From 01dd043ca8338b2bf8dd3f5ba940fe0f0b54a8d4 Mon Sep 17 00:00:00 2001 From: annyhe Date: Wed, 28 Dec 2016 13:10:43 -0800 Subject: [PATCH 3/5] subject tag modifications, with tests (#156) - POST, PUT and PATCH subjects with duplicate tags will fail with duplicate field error - case insensitive tag comparison - implement GET with multiple tag, and with single tag --- api/v1/apiErrors.js | 9 ++ api/v1/controllers/subjects.js | 70 +++++++-- api/v1/helpers/verbs/doFind.js | 12 +- api/v1/helpers/verbs/findUtils.js | 64 ++++++++ api/v1/swagger.yaml | 10 +- tests/api/v1/helpers/findUtils.js | 49 ++++++ tests/api/v1/subjects/get.js | 146 ++++++++++++++---- .../getHierarchyAspectAndTagsFilters.js | 6 +- tests/api/v1/subjects/patch.js | 28 +++- tests/api/v1/subjects/post.js | 29 +++- tests/api/v1/subjects/put.js | 42 ++++- 11 files changed, 406 insertions(+), 59 deletions(-) create mode 100644 tests/api/v1/helpers/findUtils.js diff --git a/api/v1/apiErrors.js b/api/v1/apiErrors.js index 9db01c72d9..82ac9864c8 100644 --- a/api/v1/apiErrors.js +++ b/api/v1/apiErrors.js @@ -52,6 +52,15 @@ apiErrors.create({ 'previously revoked, and vice versa.', }); +apiErrors.create({ + code: 11103, + status: 400, + name: 'DuplicateFieldError', + parent: apiErrors.ValidationError, + fields: ['tags'], + defaultMessage: 'Tags are case-insensitive. Duplicates found', +}); + // ---------------------------------------------------------------------------- // Not Found // ---------------------------------------------------------------------------- diff --git a/api/v1/controllers/subjects.js b/api/v1/controllers/subjects.js index ad3ba9da53..fdf9419a7e 100644 --- a/api/v1/controllers/subjects.js +++ b/api/v1/controllers/subjects.js @@ -23,6 +23,58 @@ const u = require('../helpers/verbs/utils'); const httpStatus = require('../constants').httpStatus; const apiErrors = require('../apiErrors'); const logAPI = require('../../../utils/loggingUtil').logAPI; +const ZERO = 0; +const ONE = 1; + +/** + * Given an array, return true if there + * are duplicates. False otherwise. + * + * @param {Array} tagsArr The input array + * @returns {Boolean} whether input array + * contains duplicates + */ +function checkDuplicates(tagsArr) { + const LEN = tagsArr.length - ONE; + // to store lowercase copies + const copyArr = []; + let toAdd; + for (let i = LEN; i >= ZERO; i--) { + toAdd = tagsArr[i].toLowerCase(); + // if duplicate found, return true + if (copyArr.indexOf(toAdd) > -ONE) { + return true; + } + copyArr.push(toAdd); + } + return false; +} + +/** + * Validates the given fields from request body or url. + * If fails, throws a corresponding error. + * @param {Object} requestBody Fields from request body + * @param {Object} params Fields from url + */ +function validateRequest(requestBody, params) { + let absolutePath = ''; + let tags = []; + if (requestBody) { + tags = requestBody.tags; + absolutePath = requestBody.absolutePath; + } else if (params) { + // params.tags.value is a comma delimited string, not empty. + tags = params.tags.value ? params.tags.value.split(',') : []; + } + if (absolutePath) { + throw new apiErrors.SubjectValidationError(); + } + if (tags && tags.length) { + if (checkDuplicates(tags)) { + throw new apiErrors.DuplicateFieldError(); + } + } +} module.exports = { @@ -72,6 +124,7 @@ module.exports = { * @param {Function} next - The next middleware function in the stack */ findSubjects(req, res, next) { + validateRequest(null, req.swagger.params); doFind(req, res, next, helper); }, @@ -105,7 +158,7 @@ module.exports = { u.findByKey(helper, params, ['hierarchy', 'samples']) .then((o) => { let retval = u.responsify(o, helper, req.method); - if (depth > 0) { + if (depth > ZERO) { retval = helper.deleteChildren(retval, depth); } @@ -208,10 +261,7 @@ module.exports = { * @param {Function} next - The next middleware function in the stack */ patchSubject(req, res, next) { - if (req.body.absolutePath) { - throw new apiErrors.SubjectValidationError(); - } - + validateRequest(req.body); doPatch(req, res, next, helper); }, @@ -225,10 +275,7 @@ module.exports = { * @param {Function} next - The next middleware function in the stack */ postSubject(req, res, next) { - if (req.body.absolutePath) { - throw new apiErrors.SubjectValidationError(); - } - + validateRequest(req.body); doPost(req, res, next, helper); }, @@ -268,10 +315,7 @@ module.exports = { * @param {Function} next - The next middleware function in the stack */ putSubject(req, res, next) { - if (req.body.absolutePath) { - throw new apiErrors.SubjectValidationError(); - } - + validateRequest(req.body); doPut(req, res, next, helper); }, diff --git a/api/v1/helpers/verbs/doFind.js b/api/v1/helpers/verbs/doFind.js index cb2303ec8e..8fbe7c67de 100644 --- a/api/v1/helpers/verbs/doFind.js +++ b/api/v1/helpers/verbs/doFind.js @@ -62,16 +62,26 @@ function doFindAndCountAll(reqResNext, props, opts) { * find command */ function doFindAll(reqResNext, props, opts) { + if (opts.where && opts.where.tags && opts.where.tags.$contains.length) { + // change to filter at the API level + opts.where.tags.$contains = []; + } + u.getScopedModel(props, opts.attributes).findAll(opts) .then((o) => { reqResNext.res.set(COUNT_HEADER_NAME, o.length); - const retval = o.map((row) => { + let retval = o.map((row) => { if (props.modelName === 'Lens') { delete row.dataValues.library; } return u.responsify(row, props, reqResNext.req.method); }); + + const { tags } = reqResNext.req.swagger.params; + if (tags && tags.value && tags.value.length) { + retval = fu.filterArrFromArr(retval, tags.value); + } reqResNext.res.status(httpStatus.OK).json(retval); }) .catch((err) => u.handleError(reqResNext.next, err, props.modelName)); diff --git a/api/v1/helpers/verbs/findUtils.js b/api/v1/helpers/verbs/findUtils.js index d479b5aae4..15315515fa 100644 --- a/api/v1/helpers/verbs/findUtils.js +++ b/api/v1/helpers/verbs/findUtils.js @@ -226,6 +226,69 @@ function options(params, props) { return opts; } // options +/** + * Returns a filtered resource array, + * according to the supplied tag string + * + * @param {Array} sArr The array of resources + * to filter from + * @param {String} tagsStr Comma delimited String with + * tags to check for. + * @returns {Array} The filtered array + */ +function filterArrFromArr(sArr, tagsStr) { + const tagsArr = tagsStr.split(','); + const TAGLEN = tagsArr.length; + // assume TAGLEN has > 0 tags, since if ther's + // 0 tags express would've thrown an error + const INCLUDE = tagsArr[ZERO].charAt(ZERO) !== '-'; + // if !INCLUDE, splice out the leading - in tags + // else throw exception if tag starts with - + for (let i = TAGLEN - ONE; i >= ZERO; i--) { + if (tagsArr[i].charAt(ZERO) === '-') { + if (INCLUDE) { + throw new Error('To specify EXCLUDE tags, ' + + 'prepend each tag with -'); + } + tagsArr[i] = tagsArr[i].slice(ONE); + } + } + + let filteredArr = []; + // append iff subject's tags contains all tags in tagsArr + if (INCLUDE) { + for (let i = ZERO; i < sArr.length; i++) { + let count = ZERO; + const tags = sArr[i].tags; + for (let j = TAGLEN - ONE; j >= ZERO; j--) { + if (tags.indexOf(tagsArr[j]) > -ONE) { + count++; + } + } + if (count === TAGLEN) { + filteredArr.push(sArr[i]); + } + } + } else { + // EXCLUDE: append iff none of subject's tags + // is in tagsArr + for (let i = ZERO; i < sArr.length; i++) { + let addToArr = true; + const tags = sArr[i].tags; + for (let j = TAGLEN - ONE; j >= ZERO; j--) { + if (tags.indexOf(tagsArr[j]) > -ONE) { + addToArr = false; + break; + } + } + if (addToArr) { + filteredArr.push(sArr[i]); + } + } + } + return filteredArr; +} + /** * Generates the "next" URL for paginated result sets. * @@ -244,4 +307,5 @@ function getNextUrl(url, limit, offset) { module.exports = { getNextUrl, options, + filterArrFromArr, // for testing }; // exports diff --git a/api/v1/swagger.yaml b/api/v1/swagger.yaml index f1c0fdbae2..95f8fc904c 100644 --- a/api/v1/swagger.yaml +++ b/api/v1/swagger.yaml @@ -3500,9 +3500,15 @@ paths: maxLength: 60 pattern: ^[0-9A-Za-z_][0-9A-Za-z_\\-]{1,59}$ description: >- - Filter by tags. + Comma-separated list of tags to include/exclude. Tag names are + case-insensitive. For example, ?tags=FOO,BAR will only return + subjects with tags FOO or BAR. Prefix each of the tag name with + a negative sign to indicate that a subject with that tag should + be excluded. For example, ?tags=-BAZ,-FOO will return only the + subjects with tag name not equal to BAZ or FOO. Subjects without + tags are not included in the result set. + type: string required: false - type: array responses: 200: description: >- diff --git a/tests/api/v1/helpers/findUtils.js b/tests/api/v1/helpers/findUtils.js new file mode 100644 index 0000000000..e34bd96d14 --- /dev/null +++ b/tests/api/v1/helpers/findUtils.js @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/api/v1/helpers/findUtils.js + */ +'use strict'; +const filterArrFromArr = require('../../../../api/v1/helpers/verbs/findUtils.js').filterArrFromArr; +const expect = require('chai').expect; +const ZERO = 0; +const ONE = 1; + +describe('filter subject array with tags array', () => { + it('filter returns some elements with INCLUDE', () => { + const sArr = [{ tags: ['a'] }, { tags: ['b'] }]; + const result = filterArrFromArr(sArr, 'a'); + expect(result).to.deep.equal([sArr[ZERO]]); + }); + + it('filter returns no elements with multiple INCLUDE', () => { + const sArr = [{ tags: ['a'] }, { tags: ['b'] }]; + const result = filterArrFromArr(sArr, 'a,b'); + expect(result).to.deep.equal([]); + }); + + it('filter returns no elements with EXCLUDE', () => { + const sArr = [{ tags: ['a'] }, { tags: ['b'] }]; + const result = filterArrFromArr(sArr, '-a,-b'); + expect(result.length).to.equal(ZERO); + }); + + it('filter returns no elements with EXCLUDE, ' + + 'with leading -', () => { + const sArr = [{ tags: ['a'] }, { tags: ['b'] }]; + const result = filterArrFromArr(sArr, '-a,b'); + expect(result.length).to.equal(ZERO); + }); + + it('filter returns some elements with EXCLUDE', () => { + const sArr = [{ tags: ['a'] }, { tags: ['b'] }]; + const result = filterArrFromArr(sArr, '-a'); + expect(result).to.deep.equal([sArr[ONE]]); + }); +}); diff --git a/tests/api/v1/subjects/get.js b/tests/api/v1/subjects/get.js index b821614ed1..3fa7a21a29 100644 --- a/tests/api/v1/subjects/get.js +++ b/tests/api/v1/subjects/get.js @@ -11,14 +11,17 @@ */ 'use strict'; const supertest = require('supertest'); - const api = supertest(require('../../../../index').app); +const filterArrFromArr = require('../../../../api/v1/helpers/verbs/findUtils.js').filterArrFromArr; const constants = require('../../../../api/v1/constants'); const tu = require('../../../testUtils'); const u = require('./utils'); const Subject = tu.db.Subject; const path = '/v1/subjects'; const expect = require('chai').expect; +const ZERO = 0; +const ONE = 1; +const TWO = 2; describe(`api: GET ${path}`, () => { let token; @@ -69,6 +72,61 @@ describe(`api: GET ${path}`, () => { after(u.forceDelete); after(tu.forceDeleteUser); + describe('duplicate tags fail', () => { + it('GET with tag EXCLUDE filter', (done) => { + api.get(`${path}?tags=-US,-US`) + .set('Authorization', token) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err, res) => { + if (err) { + return done(err); + } + done(); + }); + }); + + it('GET with tag EXCLUDE filter :: case-sensitive tags return ' + + 'non-case-sensitive result', (done) => { + api.get(`${path}?tags=-US,-us`) + .set('Authorization', token) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err, res) => { + if (err) { + return done(err); + } + done(); + }); + }); + + it('GET with tag INCLUDE filter :: duplicate tags pass', (done) => { + api.get(`${path}?tags=US,US`) + .set('Authorization', token) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err, res) => { + if (err) { + return done(err); + } + done(); + }); + }); + + it('GET with tag INCLUDE filter :: case-sensitive tags pass', (done) => { + api.get(`${path}?tags=US,us`) + .set('Authorization', token) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err, res) => { + if (err) { + return done(err); + } + done(); + }); + }); + }); + it('Check return result of get in alphabetical order of' + 'absolutePath by default', (done) => { api.get(`${path}`) @@ -79,11 +137,10 @@ describe(`api: GET ${path}`, () => { return done(err); } - expect(res.body[0].absolutePath).to.equal(na.name); - expect(res.body[1].absolutePath).to.equal(na.name + '.' + us.name); - expect(res.body[2].absolutePath).to.equal(na.name + '.' + us.name + + expect(res.body[ZERO].absolutePath).to.equal(na.name); + expect(res.body[ONE].absolutePath).to.equal(na.name + '.' + us.name); + expect(res.body[TWO].absolutePath).to.equal(na.name + '.' + us.name + '.' + vt.name); - done(); }); }); @@ -98,10 +155,9 @@ describe(`api: GET ${path}`, () => { return done(err); } - expect(res.body[0].name).to.equal(na.name); - expect(res.body[1].name).to.equal(us.name); - expect(res.body[2].name).to.equal(vt.name); - + expect(res.body[ZERO].name).to.equal(na.name); + expect(res.body[ONE].name).to.equal(us.name); + expect(res.body[TWO].name).to.equal(vt.name); done(); }); }); @@ -115,11 +171,9 @@ describe(`api: GET ${path}`, () => { if (err) { return done(err); } - - expect(res.body[2].name).to.equal(na.name); - expect(res.body[1].name).to.equal(us.name); - expect(res.body[0].name).to.equal(vt.name); - + expect(res.body[TWO].name).to.equal(na.name); + expect(res.body[ONE].name).to.equal(us.name); + expect(res.body[ZERO].name).to.equal(vt.name); done(); }); }); @@ -136,7 +190,6 @@ describe(`api: GET ${path}`, () => { const result = JSON.parse(res.text); expect(Object.keys(result)).to.contain('parentAbsolutePath'); expect(result.parentAbsolutePath).to.equal.null; - done(); }); }); @@ -154,12 +207,11 @@ describe(`api: GET ${path}`, () => { // get up to last period const expectedParAbsPath = - absPath.slice(0, absPath.lastIndexOf('.')); + absPath.slice(ZERO, absPath.lastIndexOf('.')); const result = JSON.parse(res.text); expect(Object.keys(result)).to.contain('parentAbsolutePath'); expect(result.parentAbsolutePath).to.equal(expectedParAbsPath); - done(); }); }); @@ -177,34 +229,75 @@ describe(`api: GET ${path}`, () => { // get up to last period const expectedParAbsPath = - absPath.slice(0, absPath.lastIndexOf('.')); + absPath.slice(ZERO, absPath.lastIndexOf('.')); const result = JSON.parse(res.text); expect(Object.keys(result)).to.contain('parentAbsolutePath'); expect(result.parentAbsolutePath).to.equal(expectedParAbsPath); + done(); + }); + }); + + it('GET with tag EXCLUDE filter :: single tag', (done) => { + api.get(`${path}?tags=-NE`) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res.body.length).to.equal(TWO); done(); }); }); - it('GET with tag filter :: one tag', (done) => { - api.get(`${path}?tags=US`) + it('GET with tag EXCLUDE filter :: multiple tags missing ' + + '- on subsequent tag should still EXCLUDE successfully', (done) => { + api.get(`${path}?tags=-US,NE`) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res.body.length).to.equal(ONE); + expect(res.body[ZERO].tags).to.deep.equal([]); + done(); + }); + }); + + it('GET with tag EXCLUDE filter :: multiple tags', (done) => { + api.get(`${path}?tags=-US,-NE`) .set('Authorization', token) .expect(constants.httpStatus.OK) .end((err, res) => { if (err) { return done(err); } + expect(res.body.length).to.equal(ONE); + expect(res.body[ZERO].tags).to.deep.equal([]); + done(); + }); + }); - expect(res.body.length).to.equal(2); - expect(res.body[0].tags).to.eql(['US']); - expect(res.body[1].tags).to.eql(['US', 'NE']); + it('GET with INCLUDE tag filter :: one tag', (done) => { + api.get(`${path}?tags=US`) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res.body.length).to.equal(TWO); + expect(res.body[ZERO].tags).to.eql(['US']); + expect(res.body[ONE].tags).to.eql(['US', 'NE']); done(); }); }); - it('GET with tag filter :: multiple tags', (done) => { + it('GET with INCLUDE tag filter :: multiple tags', (done) => { api.get(`${path}?tags=NE,US`) .set('Authorization', token) .expect(constants.httpStatus.OK) @@ -213,9 +306,8 @@ describe(`api: GET ${path}`, () => { return done(err); } - expect(res.body.length).to.equal(1); - expect(res.body[0].tags).to.eql(['US', 'NE']); - + expect(res.body.length).to.equal(ONE); + expect(res.body[ZERO].tags).to.eql(['US', 'NE']); done(); }); }); diff --git a/tests/api/v1/subjects/getHierarchyAspectAndTagsFilters.js b/tests/api/v1/subjects/getHierarchyAspectAndTagsFilters.js index 3ed8d59e16..8608c1190e 100644 --- a/tests/api/v1/subjects/getHierarchyAspectAndTagsFilters.js +++ b/tests/api/v1/subjects/getHierarchyAspectAndTagsFilters.js @@ -142,7 +142,7 @@ describe(`api: GET ${path}:`, () => { after(tu.forceDeleteUser); describe('SubjectTag filter on hierarchy', () => { - it('Only subjects matchihing the tag and its hierarchy should be returned', + it('Only subjects matching the tag and its hierarchy should be returned', (done) => { const endpoint = path.replace('{key}', gp.id)+'?subjectTags=cold'; api.get(endpoint) @@ -201,7 +201,7 @@ describe(`api: GET ${path}:`, () => { }); }); - it('Negation test: Subject with tags not matchihing the negated tag name ', + it('Negation test: Subject with tags not matching the negated tag name ', (done) => { const endpoint = path.replace('{key}', gp.id)+'?subjectTags=-verycold'; api.get(endpoint) @@ -219,7 +219,7 @@ describe(`api: GET ${path}:`, () => { }); }); - it('Negation test: Multiple Tags: Subject with tags not matchihing the' + + it('Negation test: Multiple Tags: Subject with tags not matching the' + ' negated tag name ', (done) => { const endpoint = path.replace('{key}', gp.id)+'?subjectTags=-cold,-ea'; api.get(endpoint) diff --git a/tests/api/v1/subjects/patch.js b/tests/api/v1/subjects/patch.js index 43969c2257..878604d223 100644 --- a/tests/api/v1/subjects/patch.js +++ b/tests/api/v1/subjects/patch.js @@ -324,7 +324,27 @@ describe(`api: PATCH ${path}`, () => { }); }); - it('patch tags with duplicate names should remove the duplicate', + it('patch tags with case sensitive names should throw an error', + (done) => { + const tags = [ + 'link1', + 'LINK1', + ]; + p1.tags = tags; + api.patch(`${path}/${i1}`) + .set('Authorization', token) + .send(p1) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err /* , res */) => { + if (err) { + return done(err); + } + done(); + }); + }); + + it('patch tags with duplicate names should throw an error', (done) => { const tags = [ 'link1', @@ -335,10 +355,8 @@ describe(`api: PATCH ${path}`, () => { api.patch(`${path}/${i1}`) .set('Authorization', token) .send(p1) - .expect((res) => { - expect(res.body.tags).to.have.length(2); - expect(res.body.tags).to.include.members(tags); - }) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) .end((err /* , res */) => { if (err) { return done(err); diff --git a/tests/api/v1/subjects/post.js b/tests/api/v1/subjects/post.js index b46af24115..1dc9c759cb 100644 --- a/tests/api/v1/subjects/post.js +++ b/tests/api/v1/subjects/post.js @@ -434,6 +434,7 @@ describe(`api: POST ${path}`, () => { done(); }); }); + it('should not be able to post tag names starting with dash(-)', (done) => { const subjectToPost = { name: `${tu.namePrefix}NorthAmerica` }; const tags = ['-na', '___continent']; @@ -454,19 +455,35 @@ describe(`api: POST ${path}`, () => { done(); }); }); - it('posting subject with duplicate tags', (done) => { + + it('posting subject with case sensitive (duplicate) tags should fail', + (done) => { const subjectToPost = { name: `${tu.namePrefix}Asia` }; + const tags = ['___na', '___NA']; + subjectToPost.tags = tags; + api.post(path) + .set('Authorization', token) + .send(subjectToPost) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err /* , res */) => { + if (err) { + return done(err); + } + done(); + }); + }); + it('posting subject with duplicate tags should fail', + (done) => { + const subjectToPost = { name: `${tu.namePrefix}Asia` }; const tags = ['___na', '___na']; subjectToPost.tags = tags; api.post(path) .set('Authorization', token) .send(subjectToPost) - .expect((res) => { - expect(res.body.tags).to.have.length(1); - expect(res.body.tags).to.include.members(tags); - - }) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) .end((err /* , res */) => { if (err) { return done(err); diff --git a/tests/api/v1/subjects/put.js b/tests/api/v1/subjects/put.js index f957f961fd..462fa2446f 100644 --- a/tests/api/v1/subjects/put.js +++ b/tests/api/v1/subjects/put.js @@ -369,7 +369,7 @@ describe('api: PUT subjects with tags', () => { .expect(constants.httpStatus.OK) .expect((res) => { expect(res.body.tags).to.have.length(1); - expect(res.body.tags).to.have.members(toPut.tags); + expect(res.body.tags).to.deep.equal(toPut.tags); }) .end((err /* , res */) => { if (err) { @@ -378,6 +378,7 @@ describe('api: PUT subjects with tags', () => { done(); }); }); + it('no putting tags with names starting with a dash(-)', (done) => { const toPut = { name: `${tu.namePrefix}newName`, @@ -399,6 +400,43 @@ describe('api: PUT subjects with tags', () => { done(); }); }); + + it('update to duplicate tags fails', (done) => { + const toPut = { + name: `${tu.namePrefix}newName`, + tags: ['tagX', 'tagX'], + }; + api.put(`${path}/${subjectId}`) + .set('Authorization', token) + .send(toPut) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err /* , res */) => { + if (err) { + return done(err); + } + done(); + }); + }); + + it('update to case-sensitive duplicate tags fails', (done) => { + const toPut = { + name: `${tu.namePrefix}newName`, + tags: ['TAGX', 'tagx'], + }; + api.put(`${path}/${subjectId}`) + .set('Authorization', token) + .send(toPut) + .expect(constants.httpStatus.BAD_REQUEST) + .expect(/DuplicateFieldError/) + .end((err /* , res */) => { + if (err) { + return done(err); + } + done(); + }); + }); + it('update to add existing tag', (done) => { const toPut = { name: `${tu.namePrefix}newName`, @@ -410,7 +448,7 @@ describe('api: PUT subjects with tags', () => { .expect(constants.httpStatus.OK) .expect((res) => { expect(res.body.tags).to.have.length(1); - expect(res.body.tags).to.have.members(toPut.tags); + expect(res.body.tags).to.deep.equal(toPut.tags); }) .end((err /* , res */) => { if (err) { From 1a097651090acd47b9d5175d93c6393cc2792c2f Mon Sep 17 00:00:00 2001 From: Ian Goldstein Date: Wed, 28 Dec 2016 13:18:10 -0800 Subject: [PATCH 4/5] =?UTF-8?q?Add=20=E2=80=9Cname=E2=80=9D=20to=20the=20l?= =?UTF-8?q?ist=20of=20allowed=20fields=20for=20GET=20/samples=3Ffields=3D?= =?UTF-8?q?=E2=80=A6=20(#158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/swagger.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1/swagger.yaml b/api/v1/swagger.yaml index 95f8fc904c..2407f28bbf 100644 --- a/api/v1/swagger.yaml +++ b/api/v1/swagger.yaml @@ -6899,7 +6899,7 @@ parameters: name: fields in: query description: >- - Comma-delimited list of field names to include in the response. + Comma-delimited list of field names to include in the response. required: false type: array collectionFormat: csv @@ -6909,6 +6909,7 @@ parameters: - id - messageBody - messageCode + - name - provider - status - tags From 5e856404c165c6326165bcdf21995d2443eac752 Mon Sep 17 00:00:00 2001 From: Ian Goldstein Date: Wed, 28 Dec 2016 13:51:23 -0800 Subject: [PATCH 5/5] User model's defaultScope should exclude password. (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By default, the API will now return User records *without* the encrypted password field. Explicitly use the db model scope “withSensitiveInfo” to include the encrypted password field in the response (e.g. for passport configuration). Add tests to confirm both the default and the scoped behavior. Unskip all the “getWriters” tests. --- api/v1/helpers/nouns/users.js | 3 - config/passportconfig.js | 2 +- db/model/user.js | 13 ++-- tests/api/v1/aspects/getWriters.js | 2 +- tests/api/v1/lenses/getWriters.js | 2 +- tests/api/v1/perspectives/getWriters.js | 2 +- tests/api/v1/subjects/getWriters.js | 2 +- tests/api/v1/userTokens/get.js | 2 +- tests/api/v1/users/get.js | 82 +++++++++++++++++++++++++ tests/api/v1/users/utils.js | 25 ++++++++ tests/db/model/user/create.js | 5 +- tests/db/model/user/find.js | 53 ++++++++++++++++ 12 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 tests/api/v1/users/get.js create mode 100644 tests/api/v1/users/utils.js create mode 100644 tests/db/model/user/find.js diff --git a/api/v1/helpers/nouns/users.js b/api/v1/helpers/nouns/users.js index 34d6932dbf..ec72f373e8 100644 --- a/api/v1/helpers/nouns/users.js +++ b/api/v1/helpers/nouns/users.js @@ -23,9 +23,6 @@ module.exports = { POST: `Create a new ${m}`, PUT: `Overwrite all attributes of this ${m}`, }, - scopeMap: { - withoutSensitiveInfo: 'withoutSensitiveInfo', - }, baseUrl: '/v1/users', model: User, modelName: 'User', diff --git a/config/passportconfig.js b/config/passportconfig.js index c3b062855d..a08e162d4a 100644 --- a/config/passportconfig.js +++ b/config/passportconfig.js @@ -13,7 +13,7 @@ 'use strict'; // eslint-disable-line strict const LocalStrategy = require('passport-local').Strategy; -const User = require('../db/index').User; +const User = require('../db/index').User.scope('withSensitiveInfo'); const Token = require('../db/index').Token; const Profile = require('../db/index').Profile; const bcrypt = require('bcrypt-nodejs'); diff --git a/db/model/user.js b/db/model/user.js index eb76e675ef..b7fa5645aa 100644 --- a/db/model/user.js +++ b/db/model/user.js @@ -91,20 +91,20 @@ module.exports = function user(seq, dataTypes) { foreignKey: 'userId', }); User.addScope('defaultScope', { - + attributes: { + exclude: ['password'], + }, include: [ { association: assoc.profile, attributes: ['name'], }, ], + order: ['User.name'], }, { override: true, }); - User.addScope('withoutSensitiveInfo', { - attributes: { - exclude: ['password'], - }, + User.addScope('withSensitiveInfo', { include: [ { association: assoc.profile, @@ -115,9 +115,6 @@ module.exports = function user(seq, dataTypes) { }); }, }, - defaultScope: { - order: ['User.name'], - }, hooks: { /** diff --git a/tests/api/v1/aspects/getWriters.js b/tests/api/v1/aspects/getWriters.js index 4238ce4fcf..871cc12781 100644 --- a/tests/api/v1/aspects/getWriters.js +++ b/tests/api/v1/aspects/getWriters.js @@ -82,7 +82,7 @@ describe('api: aspects: get writer(s)', () => { }); }); - it.skip('find Writers and make sure the passwords are not returned', (done) => { + it('find Writers and make sure the passwords are not returned', (done) => { api.get(getWritersPath.replace('{key}', aspect.name)) .set('Authorization', token) .expect(constants.httpStatus.OK) diff --git a/tests/api/v1/lenses/getWriters.js b/tests/api/v1/lenses/getWriters.js index 8d12ef1008..0ff546dab9 100644 --- a/tests/api/v1/lenses/getWriters.js +++ b/tests/api/v1/lenses/getWriters.js @@ -77,7 +77,7 @@ describe('api: lenses: get writers}', () => { }); }); - it.skip('find Writers and make sure the passwords are not returned', (done) => { + it('find Writers and make sure the passwords are not returned', (done) => { api.get(getWritersPath.replace('{key}', lens.name)) .set('Authorization', token) .expect(constants.httpStatus.OK) diff --git a/tests/api/v1/perspectives/getWriters.js b/tests/api/v1/perspectives/getWriters.js index 0a2c666109..1e21efcec9 100644 --- a/tests/api/v1/perspectives/getWriters.js +++ b/tests/api/v1/perspectives/getWriters.js @@ -85,7 +85,7 @@ describe('api: perspective: get writers', () => { }); }); - it.skip('find Writers and make sure the passwords are not returned', (done) => { + it('find Writers and make sure the passwords are not returned', (done) => { api.get(getWritersPath.replace('{key}', perspective.name)) .set('Authorization', token) .expect(constants.httpStatus.OK) diff --git a/tests/api/v1/subjects/getWriters.js b/tests/api/v1/subjects/getWriters.js index cea84e6b6e..9d0a2d2b65 100644 --- a/tests/api/v1/subjects/getWriters.js +++ b/tests/api/v1/subjects/getWriters.js @@ -81,7 +81,7 @@ describe('api: subjects: get writers}', () => { }); }); - it.skip('find Writers and make sure the passwords are not returned', (done) => { + it('find Writers and make sure the passwords are not returned', (done) => { api.get(getWritersPath.replace('{key}', subject.name)) .set('Authorization', token) .expect(constants.httpStatus.OK) diff --git a/tests/api/v1/userTokens/get.js b/tests/api/v1/userTokens/get.js index 5fe31daf08..af8abac5e3 100644 --- a/tests/api/v1/userTokens/get.js +++ b/tests/api/v1/userTokens/get.js @@ -78,4 +78,4 @@ describe(`api: GET ${path}/U/tokens/T`, () => { .expect(constants.httpStatus.NOT_FOUND) .end(() => done()); }); -}); \ No newline at end of file +}); diff --git a/tests/api/v1/users/get.js b/tests/api/v1/users/get.js new file mode 100644 index 0000000000..f73849d0e6 --- /dev/null +++ b/tests/api/v1/users/get.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/api/v1/users/get.js + */ +'use strict'; + +const supertest = require('supertest'); +const api = supertest(require('../../../../index').app); +const constants = require('../../../../api/v1/constants'); +const tu = require('../../../testUtils'); +const u = require('./utils'); +const path = '/v1/users'; +const expect = require('chai').expect; +const Profile = tu.db.Profile; +const User = tu.db.User; +const Token = tu.db.Token; + +describe(`api: GET ${path}`, () => { + const uname = `${tu.namePrefix}test@refocus.com`; + const tname = `${tu.namePrefix}Voldemort`; + + before((done) => { + Profile.create({ + name: `${tu.namePrefix}testProfile`, + }) + .then((profile) => + User.create({ + profileId: profile.id, + name: uname, + email: uname, + password: 'user123password', + }) + ) + .then(() => done()) + .catch(done); + }); + + after(u.forceDelete); + + it('user found', (done) => { + api.get(`${path}/${uname}`) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } else { + expect(res.body).to.have.property('name', uname); + expect(res.body).to.not.have.property('password'); + expect(res.body.isDeleted).to.not.equal(0); + done(); + } + }); + }); + + it('users array returned', (done) => { + api.get(`${path}`) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } else { + expect(res.body).to.be.instanceof(Array); + expect(res.body[0]).to.not.have.property('password'); + done(); + } + }); + }); + + it('user not found', (done) => { + api.get(`${path}/who@what.com`) + .set('Authorization', '???') + .expect(constants.httpStatus.NOT_FOUND) + .end(() => done()); + }); +}); diff --git a/tests/api/v1/users/utils.js b/tests/api/v1/users/utils.js new file mode 100644 index 0000000000..16e74892ce --- /dev/null +++ b/tests/api/v1/users/utils.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/api/v1/users/utils.js + */ +'use strict'; + +const tu = require('../../../testUtils'); + +const testStartTime = new Date(); + +module.exports = { + forceDelete(done) { + tu.forceDelete(tu.db.User, testStartTime) + .then(() => tu.forceDelete(tu.db.Profile, testStartTime)) + .then(() => done()) + .catch(done); + }, +}; diff --git a/tests/db/model/user/create.js b/tests/db/model/user/create.js index 4133491c6d..4fc1339647 100644 --- a/tests/db/model/user/create.js +++ b/tests/db/model/user/create.js @@ -48,10 +48,11 @@ describe('db: User: create', () => { expect(user).to.have.property('email').to.equal('user@example.com'); expect(user.password).to.not.equal('user123password'); bcrypt.compare('user123password', user.password, (err, res) => { - if(err){ + if (err) { throw err; } - expect(res).to.be.true; // eslint-disable-line no-unused-expressions + + expect(res).to.be.true; // eslint-disable-line no-unused-expressions }); done(); }); diff --git a/tests/db/model/user/find.js b/tests/db/model/user/find.js new file mode 100644 index 0000000000..6c0d6216b3 --- /dev/null +++ b/tests/db/model/user/find.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2016, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/db/model/user/find.js + */ +'use strict'; // eslint-disable-line strict +const tu = require('../../../testUtils'); +const u = require('./utils'); +const expect = require('chai').expect; +const User = tu.db.User; +const Profile = tu.db.Profile; + +describe('db: user: find: ', () => { + beforeEach((done) => { + Profile.create({ name: `${tu.namePrefix}1` }) + .then((createdProfile) => { + return User.create({ + profileId: createdProfile.id, + name: `${tu.namePrefix}1`, + email: 'user@example.com', + password: 'user123password', + }); + }) + .then(() => done()) + .catch(done); + }); + + afterEach(u.forceDelete); + + it('default scope no password', (done) => { + User.find({ name: `${tu.namePrefix}1` }) + .then((found) => { + expect(found.dataValues).to.not.have.property('password'); + done(); + }) + .catch(done); + }); + + it('withSensitiveInfo scope', (done) => { + User.scope('withSensitiveInfo').find({ name: `${tu.namePrefix}1` }) + .then((found) => { + expect(found.dataValues).to.have.property('password'); + done(); + }) + .catch(done); + }); +});