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/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/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 a7fed8ccc5..2407f28bbf 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: - @@ -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: >- @@ -4267,7 +4273,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 +4308,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 +4358,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 +5036,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 +5204,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 +5258,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 +5312,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 +5370,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 +5491,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 +5552,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 @@ -6893,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 @@ -6903,6 +6909,7 @@ parameters: - id - messageBody - messageCode + - name - provider - status - tags 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/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/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" 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/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/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/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/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/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) { 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); + }); +});