From 6b5a2e0f84b418702df8183a8a81d58b7547824b Mon Sep 17 00:00:00 2001 From: Ghost141 Date: Wed, 19 Mar 2014 22:13:03 +0800 Subject: [PATCH 1/2] Module Assembly - TopCoder NodeJS Reset Password API --- actions/resetPassword.js | 103 ++++++-- apiary.apib | 50 +++- common/stringUtils.js | 24 +- config.js | 8 +- deploy/ci.sh | 12 +- deploy/development.bat | 9 +- deploy/development.sh | 12 +- initializers/helper.js | 51 +++- initializers/ldapHelper.js | 66 ++++- package.json | 3 +- queries/get_user_information | 7 + queries/get_user_information.json | 5 + queries/update_password | 1 + queries/update_password.json | 5 + routes.js | 3 +- test/helpers/testHelper.js | 59 ++++- test/test.resetPassword.js | 230 ++++++++++++++++++ ...expected_reset_password_error_message.json | 13 + 18 files changed, 603 insertions(+), 58 deletions(-) create mode 100644 queries/get_user_information create mode 100644 queries/get_user_information.json create mode 100644 queries/update_password create mode 100644 queries/update_password.json create mode 100644 test/test.resetPassword.js create mode 100644 test/test_files/expected_reset_password_error_message.json diff --git a/actions/resetPassword.js b/actions/resetPassword.js index e11acbdd9..4ad614a38 100644 --- a/actions/resetPassword.js +++ b/actions/resetPassword.js @@ -1,42 +1,97 @@ /* * Copyright (C) 2014 TopCoder Inc., All Rights Reserved. * - * @version 1.0 - * @author LazyChild + * @version 1.1 + * @author LazyChild, Ghost_141 + * + * Changes in 1.1: + * - Implement the Reset Password API instead of mock it. */ "use strict"; var async = require('async'); +var _ = require('underscore'); var BadRequestError = require('../errors/BadRequestError'); var UnauthorizedError = require('../errors/UnauthorizedError'); var ForbiddenError = require('../errors/ForbiddenError'); +var NotFoundError = require('../errors/NotFoundError'); +var IllegalArgumentError = require('../errors/IllegalArgumentError'); /** - * This is the function that stub reset password + * Reset Password. * * @param {Object} api - The api object that is used to access the global infrastructure * @param {Object} connection - The connection object for the current request * @param {Function} next - The callback to be called after this function is done */ function resetPassword(api, connection, next) { - var result, helper = api.helper; + var result, helper = api.helper, sqlParams, userId, ldapEntryParams, oldPassword, + dbConnectionMap = connection.dbConnectionMap, + token = connection.params.token, + handle = decodeURI(connection.params.handle).toLowerCase(), + newPassword = connection.params.password, + tokenKey = handle + '-' + api.config.general.resetTokenSuffix; + async.waterfall([ - function(cb) { - if (connection.params.handle == "nonValid") { - cb(new BadRequestError("The handle you entered is not valid")); + function (cb) { + var error = helper.checkStringPopulated(token, 'token') || + helper.checkStringPopulated(handle, 'handle') || + helper.validatePassword(newPassword); + if (error) { + cb(error); return; - } else if (connection.params.handle == "badLuck") { - cb(new Error("Unknown server error. Please contact support.")); + } + sqlParams = { + handle: handle + }; + api.dataAccess.executeQuery('get_user_information', sqlParams, dbConnectionMap, cb); + }, + function (result, cb) { + if (result.length === 0) { + cb(new NotFoundError('The user is not exist.')); return; - } else if (connection.params.token == "unauthorized_token") { - cb(new UnauthorizedError("Authentication credentials were missing or incorrect.")); + } + userId = result[0].user_id; + oldPassword = helper.decodePassword(result[0].old_password, helper.PASSWORD_HASH_KEY); + sqlParams.handle = result[0].handle; + helper.getCachedValue(tokenKey, cb); + }, + function (cache, cb) { + if (!_.isDefined(cache)) { + // The token is either not assigned or is expired. + cb(new BadRequestError('The token is expired, not existed or incorrect. Please apply a new one.')); return; - } else if (connection.params.token == "forbidden_token") { - cb(new ForbiddenError("The request is understood, but it has been refused or access is not allowed.")); + } + if (cache !== token) { + // The token don't match + cb(new IllegalArgumentError('The token is incorrect.')); + return; + } + sqlParams.password = helper.encodePassword(newPassword, helper.PASSWORD_HASH_KEY); + api.dataAccess.executeQuery('update_password', sqlParams, dbConnectionMap, cb); + }, + function (count, cb) { + if (count !== 1) { + cb(new Error('password is not updated successfully')); return; } + ldapEntryParams = { + userId: userId, + handle: sqlParams.handle, + oldPassword: oldPassword, + newPassword: newPassword + }; + api.ldapHelper.updateMemberPasswordLDAPEntry(ldapEntryParams, cb); + }, + function (cb) { + // Delete the token from cache system. + api.cache.destroy(tokenKey, function (err) { + cb(err); + }); + }, + function (cb) { result = { - "description": "Your password has been reset!" + description: 'Your password has been reset!' }; cb(); } @@ -60,16 +115,17 @@ function resetPassword(api, connection, next) { function generateResetToken(api, connection, next) { var result, helper = api.helper; async.waterfall([ - function(cb) { - if (connection.params.handle == "nonValid" || connection.params.email == "nonValid@test.com") { + function (cb) { + if (connection.params.handle === "nonValid" || connection.params.email === "nonValid@test.com") { cb(new BadRequestError("The handle you entered is not valid")); return; - } else if (connection.params.handle == "badLuck" || connection.params.email == "badLuck@test.com") { + } + if (connection.params.handle === "badLuck" || connection.params.email === "badLuck@test.com") { cb(new Error("Unknown server error. Please contact support.")); return; } - if (connection.params.handle == "googleSocial" || connection.params.email == "googleSocial@test.com") { + if (connection.params.handle === "googleSocial" || connection.params.email === "googleSocial@test.com") { result = { "socialLogin": "Google" }; @@ -103,9 +159,16 @@ exports.resetPassword = { blockedConnectionTypes: [], outputExample: {}, version: 'v2', + transaction: 'write', + cacheEnabled: false, + databases: ["common_oltp"], run: function (api, connection, next) { - api.log("Execute resetPassword#run", 'debug'); - resetPassword(api, connection, next); + if (connection.dbConnectionMap) { + api.log("Execute resetPassword#run", 'debug'); + resetPassword(api, connection, next); + } else { + api.helper.handleNoConnection(api, connection, next); + } } }; diff --git a/apiary.apib b/apiary.apib index b12d9de32..acc1984fe 100644 --- a/apiary.apib +++ b/apiary.apib @@ -1049,23 +1049,55 @@ Register a new user. { "name":"Bad Request", "value":"400", - "description":"This message will explain why the request is invalid or cannot be served." + "description":"password should be non-null and non-empty string." } -+ Response 401 (application/json) ++ Response 400 (application/json) { - "name":"Unauthorized", - "value":"401", - "description":"Authentication credentials were missing or incorrect." + "name":"Bad Request", + "value":"400", + "description":"password must be at least 7 characters in length." } -+ Response 403 (application/json) ++ Response 400 (application/json) { - "name":"Forbidden", - "value":"403", - "description":"The request is understood, but it has been refused or access is not allowed." + "name":"Bad Request", + "value":"400", + "description":"password may contain at most 15 characters." + } + ++ Response 400 (application/json) + + { + "name":"Bad Request", + "value":"400", + "description":"Your password may contain only letters, numbers and -_.{}[]()" + } + ++ Response 400 (application/json) + + { + "name":"Bad Request", + "value":"400", + "description":"The token is expired or not existed. Please apply a new one." + } + ++ Response 400 (application/json) + + { + "name":"Bad Request", + "value":"400", + "description":"The token is incorrect." + } + ++ Response 404 (application/json) + + { + "name":"Not Found", + "value":"404", + "description":"The user is not exist." } + Response 500 (application/json) diff --git a/common/stringUtils.js b/common/stringUtils.js index 460eb2261..6a66b949f 100644 --- a/common/stringUtils.js +++ b/common/stringUtils.js @@ -1,8 +1,10 @@ /* - * Copyright (C) 2013 TopCoder Inc., All Rights Reserved. + * Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. * - * Version: 1.0 - * Author: TCSASSEMBLER + * Version: 1.1 + * Author: TCSASSEMBLER, Ghost_141 + * Changes in 1.1: + * - add PUNCTUATION and PASSWORD_ALPHABET. */ "use strict"; @@ -18,6 +20,18 @@ var ALPHABET_ALPHA_EN = ALPHABET_ALPHA_LOWER_EN + ALPHABET_ALPHA_UPPER_EN; var ALPHABET_DIGITS_EN = "0123456789"; +/** + * The valid characters for punctuation. + * @since 1.1 + */ +var PUNCTUATION = "-_.{}[]()"; + +/** + * The valid characters for password. + * @since 1.1 + */ +var PASSWORD_ALPHABET = ALPHABET_ALPHA_EN + ALPHABET_DIGITS_EN + PUNCTUATION; + /** * Checks if string has all its characters in alphabet given. * @@ -42,4 +56,6 @@ exports.containsOnly = function (string, alphabet) { exports.ALPHABET_ALPHA_UPPER_EN = ALPHABET_ALPHA_UPPER_EN; exports.ALPHABET_ALPHA_LOWER_EN = ALPHABET_ALPHA_LOWER_EN; exports.ALPHABET_ALPHA_EN = ALPHABET_ALPHA_EN; -exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN; \ No newline at end of file +exports.ALPHABET_DIGITS_EN = ALPHABET_DIGITS_EN; +exports.PUNCTUATION = PUNCTUATION; +exports.PASSWORD_ALPHABET = PASSWORD_ALPHABET; diff --git a/config.js b/config.js index 31d388bba..736024660 100644 --- a/config.js +++ b/config.js @@ -2,7 +2,7 @@ * Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. * * @author vangavroche, Ghost_141, kurtrips, Sky_, isv - * @version 1.12 + * @version 1.13 * changes in 1.1: * - add defaultCacheLifetime parameter * changes in 1.2: @@ -30,6 +30,9 @@ * - added designSubmissionsBasePath for design submissions * changes in 1.12: * - add defaultUserCacheLifetime property. + * Changes in 1.13: + * - add minPasswordLength and maxPasswordLength + * - add resetTokenSuffix */ "use strict"; @@ -71,6 +74,9 @@ config.general = { defaultCacheLifetime : process.env.CACHE_EXPIRY || 1000 * 60 * 10, //10 min default defaultAuthMiddlewareCacheLifetime : process.env.AUTH_MIDDLEWARE_CACHE_EXPIRY || 1000 * 60 * 10, //10 min default defaultUserCacheLifetime: process.env.USER_CACHE_EXPIRY || 1000 * 60 * 60 * 24, //24 hours default + resetTokenSuffix: 'reset-token', + minPasswordLength: 8, + maxPasswordLength: 30, cachePrefix: '', oauthClientId: process.env.OAUTH_CLIENT_ID || "CMaBuwSnY0Vu68PLrWatvvu3iIiGPh7t", //auth0 secret is encoded in base64! diff --git a/deploy/ci.sh b/deploy/ci.sh index 8a5a9319a..34e7b3257 100644 --- a/deploy/ci.sh +++ b/deploy/ci.sh @@ -1,10 +1,12 @@ #!/bin/bash # -# Copyright (C) 2013 TopCoder Inc., All Rights Reserved. +# Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. # -# Version: 1.0 -# Author: vangavroche, delemach +# Version: 1.1 +# Author: vangavroche, delemach, Ghost_141 +# Changes in 1.1 +# - add REDIS_HOST and REDIS_PORT. # export CACHE_EXPIRY=-1 @@ -67,3 +69,7 @@ export JIRA_USERNAME=api_test export JIRA_PASSWORD=8CDDp6BHLtUeUdD export ACTIONHERO_CONFIG=./config.js + +# Used in api cache. +export REDIS_HOST=localhost +export REDIS_PORT=6379 diff --git a/deploy/development.bat b/deploy/development.bat index 4d4fe9183..afe58735b 100644 --- a/deploy/development.bat +++ b/deploy/development.bat @@ -2,9 +2,11 @@ REM REM Copyright (C) 2014 TopCoder Inc., All Rights Reserved. REM -REM Version: 1.0 -REM Author: TrePe +REM Version: 1.1 +REM Author: TrePe, Ghost_141 REM +REM Changes in 1.1 +REM - Add REDIS_PORT and REDIS_HOST. REM tests rely on caching being off. But set this to a real value (or remove) while coding. @@ -66,3 +68,6 @@ set JIRA_PASSWORD=8CDDp6BHLtUeUdD set ACTIONHERO_CONFIG=./config.js +REM Used in API cache +set REDIS_HOST=localhost +set REDIS_PORT=6379 diff --git a/deploy/development.sh b/deploy/development.sh index 177ffa418..35cae7bce 100755 --- a/deploy/development.sh +++ b/deploy/development.sh @@ -1,12 +1,14 @@ #!/bin/bash # -# Copyright (C) 2013 TopCoder Inc., All Rights Reserved. +# Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. # -# Version: 1.1 -# Author: vangavroche, TCSASSEMBLER +# Version: 1.2 +# Author: vangavroche, Ghost_141 # changes in 1.1: # - add JIRA_USERNAME and JIRA_PASSWORD +# changes in 1.2: +# - add REDIS_HOST and REDIS_PORT. # # tests rely on caching being off. But set this to a real value (or remove) while coding. @@ -67,4 +69,8 @@ export TIMEOUT=3000 export JIRA_USERNAME=api_test export JIRA_PASSWORD=8CDDp6BHLtUeUdD +# Used in api cache. +export REDIS_HOST=localhost +export REDIS_PORT=6379 + export ACTIONHERO_CONFIG=./config.js diff --git a/initializers/helper.js b/initializers/helper.js index 383015b27..fc4ccfe69 100644 --- a/initializers/helper.js +++ b/initializers/helper.js @@ -6,8 +6,8 @@ /** * This module contains helper functions. * @author Sky_, Ghost_141, muzehyun, kurtrips, isv - * @version 1.13 - * changes in 1.15 + * @version 1.16 + * changes in 1.1 * - add mapProperties * changes in 1.2: * - add getPercent to underscore mixin @@ -48,6 +48,10 @@ * - add method checkMember to check if the caller have at least member access leve. * changes in 1.15 * - added checkUserExists function + * Changes in 1.16: + * - add validatePassword method. + * - introduce the stringUtils in this file. + * - add PASSWORD_HASH_KEY. */ "use strict"; @@ -65,6 +69,7 @@ if (typeof String.prototype.startsWith !== 'function') { var async = require('async'); var _ = require('underscore'); var moment = require('moment'); +var stringUtils = require('../common/stringUtils'); var IllegalArgumentError = require('../errors/IllegalArgumentError'); var NotFoundError = require('../errors/NotFoundError'); var BadRequestError = require('../errors/BadRequestError'); @@ -104,6 +109,13 @@ helper.both = { */ helper.MAX_INT = 2147483647; +/** + * HASH KEY For Password + * + * @since 1.16 + */ +helper.PASSWORD_HASH_KEY = process.env.PASSWORD_HASH_KEY || 'default'; + /** * The name in api response to database name map. */ @@ -928,9 +940,9 @@ helper.getPhaseId = function (phaseName) { */ helper.getColorStyle = function (rating) { - if (rating === null) { - return "color: #000000"; - } + if (rating === null) { + return "color: #000000"; + } if (rating < 0) { return "color: #FF9900"; // orange @@ -1132,6 +1144,35 @@ helper.checkUserExists = function (handle, api, dbConnectionMap, callback) { }); }; +/** + * Validate the given password value. + * @param {String} password - the password value. + * @returns {Object} - Return error if the given password is invalid. + * @since 1.16 + */ +helper.validatePassword = function (password) { + var value = password.trim(), + configGeneral = helper.api.config.general, + i, + error; + error = helper.checkStringPopulated(password, 'password'); + if (error) { + return error; + } + if (value.length > configGeneral.maxPasswordLength) { + return new IllegalArgumentError('password may contain at most ' + configGeneral.maxPasswordLength + ' characters.'); + } + if (value.length < configGeneral.minPasswordLength) { + return new IllegalArgumentError('password must be at least ' + configGeneral.minPasswordLength + ' characters in length.'); + } + for (i = 0; i < password.length; i += 1) { + if (!_.contains(stringUtils.PASSWORD_ALPHABET, password.charAt(i))) { + return new IllegalArgumentError('Your password may contain only letters, numbers and ' + stringUtils.PUNCTUATION); + } + } + + return null; +}; /** * Expose the "helper" utility. diff --git a/initializers/ldapHelper.js b/initializers/ldapHelper.js index f8de238d7..828c2f1be 100644 --- a/initializers/ldapHelper.js +++ b/initializers/ldapHelper.js @@ -1,9 +1,12 @@ /*jslint nomen: true */ /* - * Copyright (C) 2013 TopCoder Inc., All Rights Reserved. + * Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. * - * Version: 1.0 - * Author: TCSASSEMBLER + * Version: 1.1 + * Author: TCSASSEMBLER, Ghost_141 + * + * Changes in 1.1 + * - add updateMemberPasswordLDAPEntry for update member password. */ "use strict"; @@ -257,7 +260,7 @@ exports.ldapHelper = function (api, next) { } return next(null, true); }, - + /** * Main function of removeMemberProfileLDAPEntry * @@ -282,7 +285,7 @@ exports.ldapHelper = function (api, next) { removeClient(api, client, userId, callback); } ], function (err, result) { - + if (err) { api.log('removeMemberProfileLDAPEntry: error occurred: ' + err + " " + (err.stack || ''), "error"); return next(err, null); @@ -293,7 +296,7 @@ exports.ldapHelper = function (api, next) { }); } catch (err) { console.log('CAUGHT: ' + err); - return next(error, null); + return next(err, null); } return next(null, true); }, @@ -340,7 +343,56 @@ exports.ldapHelper = function (api, next) { api.log('Leave activateMemberProfileLDAPEntry', 'debug'); }); return next(null, true); + }, + + /** + * Main function of updateMemberPasswordLDAPEntry + * + * @param {Object} params require fields: userId, handle, newPassword, oldPassword + * @param {Function} next - callback function + * @since 1.1 + */ + updateMemberPasswordLDAPEntry: function (params, next) { + api.log('Enter updateMemberPasswordLDAPEntry', 'debug'); + + var client, error, index, requiredParams = ['userId', 'handle', 'newPassword', 'oldPassword']; + + for (index = 0; index < requiredParams.length; index += 1) { + error = api.helper.checkDefined(params[requiredParams[index]], requiredParams[index]); + if (error) { + api.log('updateMemberPasswordLDAPEntry: error occurred: ' + error + " " + (error.stack || ''), "error"); + next(error, null); + return; + } + } + try { + async.series([ + function (callback) { + client = createClient(); + callback(null, 'create client'); + }, + function (callback) { + bindClient(api, client, callback); + }, + function (callback) { + passwordModify(api, client, params, callback); + } + ], function (err, result) { + if (err) { + error = result.pop(); + api.log('updateMemberPasswordLDAPEntry: error occurred: ' + err + " " + (err.stack || ''), "error"); + next(error, null); + } else { + client.unbind(); + api.log('Leave updateMemberPasswordLDAPEntry', 'debug'); + next(); + } + }); + } catch (err) { + console.log('CAUGHT: ' + err); + next(error, null); + } } }; next(); -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index ffffd535d..25e6764d1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "moment": "~2.5.1", "mime": "~1.2.11", "xtend": "2.1.2", - "validator": "~3.5.0" + "validator": "~3.5.0", + "redis": "0.10.1" }, "devDependencies": { "supertest": "0.8.1", diff --git a/queries/get_user_information b/queries/get_user_information new file mode 100644 index 000000000..a56b0183c --- /dev/null +++ b/queries/get_user_information @@ -0,0 +1,7 @@ +SELECT + u.user_id +, u.handle +, su.password AS old_password +FROM user u +INNER JOIN security_user su ON su.user_id = u.handle +WHERE handle_lower = LOWER('@handle@') diff --git a/queries/get_user_information.json b/queries/get_user_information.json new file mode 100644 index 000000000..1552ca16e --- /dev/null +++ b/queries/get_user_information.json @@ -0,0 +1,5 @@ +{ + "name" : "get_user_information", + "db" : "common_oltp", + "sqlfile" : "get_user_information" +} diff --git a/queries/update_password b/queries/update_password new file mode 100644 index 000000000..68963e985 --- /dev/null +++ b/queries/update_password @@ -0,0 +1 @@ +UPDATE security_user SET password = '@password@' WHERE user_id = '@handle@' diff --git a/queries/update_password.json b/queries/update_password.json new file mode 100644 index 000000000..b75812766 --- /dev/null +++ b/queries/update_password.json @@ -0,0 +1,5 @@ +{ + "name" : "update_password", + "db" : "common_oltp", + "sqlfile" : "update_password" +} diff --git a/routes.js b/routes.js index d95da6c1f..5c40932b2 100755 --- a/routes.js +++ b/routes.js @@ -200,8 +200,9 @@ exports.routes = { ].concat(testMethods.get), post: [ // Stub API - { path: "/:apiVersion/users/resetPassword/:handle", action: "resetPassword" }, + + { path: "/:apiVersion/users/resetPassword/:handle", action: "resetPassword" }, { path: "/:apiVersion/terms/:termsOfUseId/agree", action: "agreeTermsOfUse" }, { path: "/:apiVersion/users", action: "memberRegister" }, { path: "/:apiVersion/develop/challenges/:challengeId/submit", action: "submitForDevelopChallenge" }, diff --git a/test/helpers/testHelper.js b/test/helpers/testHelper.js index 8a05944e7..2e91fabdb 100644 --- a/test/helpers/testHelper.js +++ b/test/helpers/testHelper.js @@ -1,7 +1,7 @@ /* * Copyright (C) 2013 - 2014 TopCoder Inc., All Rights Reserved. * - * @version 1.4 + * @version 1.5 * @author Sky_, muzehyun, Ghost_141, OlinaRuan * changes in 1.1: * - add getTrimmedData method @@ -11,6 +11,10 @@ * - add getAdminJwt and getMemberJwt * changes in 1.4 * - add updateTextColumn method. + * Changes in 1.5: + * - add deleteCachedKey, addCacheValue and getCachedValue method. + * - add PASSWORD_HASH_KEY + * - remove unused dependency. */ "use strict"; /*jslint node: true, stupid: true, unparam: true */ @@ -18,11 +22,11 @@ var async = require('async'); var fs = require('fs'); -var util = require('util'); var _ = require('underscore'); var assert = require('chai').assert; var crypto = require("crypto"); var jwt = require('jsonwebtoken'); +var redis = require('redis'); /** * The test helper @@ -51,6 +55,12 @@ var DEFAULT_TIMEOUT = 30000; // 30s var CLIENT_ID = configs.config.general.oauthClientId; var SECRET = configs.config.general.oauthClientSecret; +/** + * The password hash key. + * @since 1.5 + */ +helper.PASSWORD_HASH_KEY = process.env.PASSWORD_HASH_KEY || "default"; + /** * create connection for given database * @param {String} databaseName - the database name @@ -400,4 +410,49 @@ helper.getMemberJwt = function (userId) { return jwt.sign({sub: "ad|" + (userId || "132458")}, SECRET, {expiresInMinutes: 1000, audience: CLIENT_ID}); }; +/** + * Get cached value from redis server. + * @param {String} key - the key value. + * @param {Function} cb - the callback function. + * @since 1.5 + */ +helper.getCachedValue = function (key, cb) { + var client = redis.createClient(); + client.get(key, function (err, value) { + cb(err, JSON.parse(value)); + }); + // Quit the client. + client.quit(); +}; + +/** + * Delete the key from redis server. + * + * @param {String} key - the key value. + * @param {Function} cb - the callback function. + * @since 1.5 + */ +helper.deleteCachedKey = function (key, cb) { + var client = redis.createClient(); + client.del(key, function (err) { + cb(err); + client.quit(); + }); +}; + +/** + * Add cache to redis server. + * @param {String} key - the key for cache value. + * @param {Object} value - the value to cache. + * @param {Function} cb - the callback function. + * @since 1.5 + */ +helper.addCacheValue = function (key, value, cb) { + var client = redis.createClient(); + client.set(key, JSON.stringify(value), function (err) { + cb(err); + client.quit(); + }); +}; + module.exports = helper; diff --git a/test/test.resetPassword.js b/test/test.resetPassword.js new file mode 100644 index 000000000..585206024 --- /dev/null +++ b/test/test.resetPassword.js @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2014 TopCoder Inc., All Rights Reserved. + * + * @version 1.0 + * @author Ghost_141 + */ +'use strict'; +/*global describe, it, before, beforeEach, after, afterEach */ +/*jslint node: true, stupid: true, unparam: true */ + +/** + * Module dependencies. + */ +var fs = require('fs'); +var request = require('supertest'); +var assert = require('chai').assert; +var async = require('async'); + +var testHelper = require('./helpers/testHelper'); +var configs = require('../config'); +var API_ENDPOINT = process.env.API_ENDPOINT || 'http://localhost:8080'; + +describe('Reset Password API', function () { + this.timeout(120000); // The api with testing remote db could be quit slow + + var errorObject = require('../test/test_files/expected_reset_password_error_message'), + configGeneral = configs.config.general, + heffan = configGeneral.cachePrefix + 'heffan' + '-' + configGeneral.resetTokenSuffix, + user = configGeneral.cachePrefix + 'user' + '-' + configGeneral.resetTokenSuffix, + superUser = configGeneral.cachePrefix + 'super' + '-' + configGeneral.resetTokenSuffix; + + /** + * Clear database + * @param {Function} done the callback + */ + function clearDb(done) { + async.parallel({ + heffan: function (cbx) { + testHelper.deleteCachedKey(heffan, cbx); + }, + user: function (cbx) { + testHelper.deleteCachedKey(user, cbx); + }, + superUser: function (cbx) { + testHelper.deleteCachedKey(superUser, cbx); + } + }, function (err) { + done(err); + }); + } + + /** + * This function is run before all tests. + * Generate tests data. + * @param {Function} done the callback + */ + before(function (done) { + async.waterfall([ + clearDb, + function (cb) { + async.parallel({ + heffan: function (cbx) { + testHelper.addCacheValue(heffan, + { + value: 'abcde', + expireTimestamp: new Date('2015-1-1').getTime(), + createdAt: new Date().getTime(), + readAt: null + }, cbx); + }, + user: function (cbx) { + testHelper.addCacheValue(user, + { + value: 'abcde', + expireTimestamp: new Date('2014-1-1').getTime(), + createdAt: new Date().getTime(), + readAt: null + }, cbx); + } + }, function (err) { + cb(err); + }); + } + ], done); + }); + + /** + * This function is run after all tests. + * Clean up all data. + * @param {Function} done the callback + */ + after(function (done) { + clearDb(done); + }); + + /** + * Create a http request and test it. + * @param {String} handle - the request handle. + * @param {Number} expectStatus - the expected request response status. + * @param {Object} postData - the data that will be post to api. + * @param {Function} cb - the call back function. + */ + function createRequest(handle, expectStatus, postData, cb) { + request(API_ENDPOINT) + .post('/v2/users/resetPassword/' + handle) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(expectStatus) + .send(postData) + .end(cb); + } + + /** + * assert the bad response. + * @param {String} handle - the request handle + * @param {Number} expectStatus - the expect status. + * @param {String} errorMessage - the expected error message. + * @param {Object} postData - the data post to api. + * @param {Function} cb - the callback function. + */ + function assertBadResponse(handle, expectStatus, errorMessage, postData, cb) { + createRequest(handle, expectStatus, postData, function (err, result) { + if (!err) { + assert.equal(result.body.error.details, errorMessage, 'invalid error message'); + } else { + cb(err); + return; + } + cb(); + }); + } + + /** + * Test when password is too short. + */ + it('should return bad Request. The password is too short.', function (done) { + assertBadResponse('heffan', 400, errorObject.password.tooShort, { token: 'abcde', password: '123' }, done); + }); + + /** + * Test when password is too long. + */ + it('should return bad Request. The password is too long.', function (done) { + assertBadResponse('heffan', 400, errorObject.password.tooLong, { token: 'abcde', password: '1234567890abcdefghijklmnopqrstuvwxyz'}, done); + }); + + /** + * Test when password is just spaces. + */ + it('should return bad Request. The password is just spaces.', function (done) { + assertBadResponse('heffan', 400, errorObject.password.empty, { token: 'abcde', password: ' ' }, done); + }); + + /** + * Test when password contains the invalid characters. + */ + it('should return bad Request. The password contains invalid characters.', function (done) { + assertBadResponse('heffan', 400, errorObject.password.invalidCharacters, { token: 'abcde', password: '+*&^%$$#@' }, done); + }); + + /** + * Test when token is not existed in cache system. + */ + it('should return bad Request. The token is not existed in cache system.', function (done) { + assertBadResponse('super', 400, errorObject.token.notExistedOrExpired, { token: 'djoisdfj', password: 'password' }, done); + }); + + /** + * Test when token is in system but expired. + */ + it('should return bad Request. The token is in system but expired.', function (done) { + assertBadResponse('user', 400, errorObject.token.notExistedOrExpired, { token: 'djoisdfj', password: 'password' }, done); + }); + + /** + * Test when token is incorrect. + */ + it('should return bad Request. The token is incorrect.', function (done) { + assertBadResponse('heffan', 400, errorObject.token.inCorrect, { token: 'ajdoijfiodsfj', password: 'password' }, done); + }); + + /** + * Test when user in not exist. + */ + it('should return not Found Error. The user is not existed', function (done) { + assertBadResponse('notExist', 404, errorObject.notExist, { token: 'abcde', password: 'password' }, done); + }); + + /** + * Test success results. + */ + it('should return success results. The password has been saved.', function (done) { + var newPassword = 'abcdefghijk'; + async.waterfall([ + function (cb) { + createRequest('heffan', 200, { token: 'abcde', password: newPassword }, function (err) { + cb(err); + }); + }, + function (cb) { + testHelper.runSqlSelectQuery('password FROM security_user WHERE user_id = \'heffan\'', 'common_oltp', cb); + }, + function (value, cb) { + assert.equal(testHelper.decodePassword(value[0].password, testHelper.PASSWORD_HASH_KEY), newPassword, 'invalid password'); + cb(); + } + ], done); + }); + + /** + * Test success results. The user handle is in upper case. + */ + it('should return success results. The user handle is in upper case.', function (done) { + var newPassword = 'abcdefghijk'; + async.waterfall([ + function (cb) { + createRequest('HEFFAN', 200, { token: 'abcde', password: newPassword }, function (err) { + cb(err); + }); + }, + function (cb) { + testHelper.runSqlSelectQuery('password FROM security_user WHERE user_id = \'heffan\'', 'common_oltp', cb); + }, + function (value, cb) { + assert.equal(testHelper.decodePassword(value[0].password, testHelper.PASSWORD_HASH_KEY), newPassword, 'invalid password'); + cb(); + } + ], done); + }); +}); diff --git a/test/test_files/expected_reset_password_error_message.json b/test/test_files/expected_reset_password_error_message.json new file mode 100644 index 000000000..a7eda4dda --- /dev/null +++ b/test/test_files/expected_reset_password_error_message.json @@ -0,0 +1,13 @@ +{ + "password": { + "empty": "password should be non-null and non-empty string.", + "tooShort": "password must be at least 8 characters in length.", + "tooLong": "password may contain at most 30 characters.", + "invalidCharacters": "Your password may contain only letters, numbers and -_.{}[]()" + }, + "token": { + "notExistedOrExpired": "The token is expired or not existed. Please apply a new one.", + "inCorrect": "The token is expired, not existed or incorrect. Please apply a new one." + }, + "notExist": "The user is not exist." +} From edbe07db51fc2b6971e90f3b5bbf6c5a2ee28890 Mon Sep 17 00:00:00 2001 From: Ghost141 Date: Fri, 11 Apr 2014 23:35:04 +0800 Subject: [PATCH 2/2] Fix version error in helper.js --- initializers/helper.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/initializers/helper.js b/initializers/helper.js index a145f09e0..ed0202905 100644 --- a/initializers/helper.js +++ b/initializers/helper.js @@ -128,7 +128,7 @@ helper.MAX_INT = 2147483647; /** * HASH KEY For Password * - * @since 1.16 + * @since 1.23 */ helper.PASSWORD_HASH_KEY = process.env.PASSWORD_HASH_KEY || 'default'; @@ -1213,7 +1213,7 @@ helper.checkUserExists = function (handle, api, dbConnectionMap, callback) { * Validate the given password value. * @param {String} password - the password value. * @returns {Object} - Return error if the given password is invalid. - * @since 1.16 + * @since 1.23 */ helper.validatePassword = function (password) { var value = password.trim(), @@ -1244,7 +1244,7 @@ helper.validatePassword = function (password) { * * @param {Array} terms - The terms. * @returns {Boolean} true if all terms agreed otherwise false. - * @since 1.16 + * @since 1.22 */ helper.allTermsAgreed = function (terms) { return _.every(terms, function (term) {