diff --git a/README.md b/README.md index f6575f9b..575f01ed 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ $ npm install -g ask-cli-x Before you can start using the ASK CLI, configure your ASK (and AWS) credentials: ``` -$ askx init +$ askx configure ``` You’ll be prompted to sign into your Amazon developer account. If you choose to have your skill hosted by AWS, you’ll have the option of linking your AWS account as well. diff --git a/bin/askx.js b/bin/askx.js index 063cd17d..60e29d31 100755 --- a/bin/askx.js +++ b/bin/askx.js @@ -10,7 +10,7 @@ if (!require('semver').gte(process.version, '8.3.0')) { require('module-alias/register'); const commander = require('commander'); -require('@src/commands/init').createCommand(commander); +require('@src/commands/configure').createCommand(commander); require('@src/commands/deploy').createCommand(commander); require('@src/commands/v2new').createCommand(commander); @@ -21,7 +21,7 @@ commander .version(require('../package.json').version) .parse(process.argv); -const ALLOWED_ASK_ARGV_2 = ['api', 'init', 'deploy', 'new', 'util', 'help', '-v', '--version', '-h', '--help']; +const ALLOWED_ASK_ARGV_2 = ['api', 'configure', 'deploy', 'new', 'util', 'help', '-v', '--version', '-h', '--help']; if (process.argv[2] && ALLOWED_ASK_ARGV_2.indexOf(process.argv[2]) === -1) { console.log('Command not recognized. Please run "askx" to check the user instructions.'); } diff --git a/lib/api/api-wrapper.js b/lib/api/api-wrapper.js deleted file mode 100644 index c31e7698..00000000 --- a/lib/api/api-wrapper.js +++ /dev/null @@ -1,21 +0,0 @@ -const requestWrapper = require('./request-wrapper'); -const CONSTANTS = require('../utils/constants'); - -/* - * List of Alexa Skill Management API Service Calls. - * These functions are thin wrappers for Alexa Skill Management APIs. - */ - -module.exports = { - callListVendor -}; - -function callListVendor(profile, doDebug, callback) { - const url = '/vendors'; - const general = { - url, - method: 'GET' - }; - const headers = {}; - requestWrapper.request('list-vendors', CONSTANTS.SMAPI.VERSION.V0, general, headers, null, profile, doDebug, callback); -} diff --git a/lib/api/request-wrapper.js b/lib/api/request-wrapper.js deleted file mode 100644 index 8b856fc0..00000000 --- a/lib/api/request-wrapper.js +++ /dev/null @@ -1,126 +0,0 @@ -const os = require('os'); -const request = require('request'); - -const stringUtils = require('@src/utils/string-utils'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); -const logger = require('@src/utils/logger-utility'); -const tools = require('@src/utils/tools'); -const CONSTANTS = require('@src/utils/constants'); - -/** - * Wrapper for SMAPI request. - * Use oauthWrapper to refresh token when necessary before each request. - * @param clientMeta name for the API, or with command name - * @param version version for the API, enum values from CONSTANTS.SMAPI.VERSION - * @param general general contains url and method - * @param headers headers for the request - * @param payload payload for the request - * @param profile user profile - * @param doDebug ASK CLI debug mode - * @param callback callback with response when request is successful; kill the process if it fails - */ -module.exports.request = (clientMeta, version, general, headers, payload, profile, doDebug, callback) => { - let requestUrl = `${CONSTANTS.SMAPI.ENDPOINT}/${version}${general.url}`; - // check environmental variable setting for SMAPI endpoint - const envVarSmapiBaseUrl = process.env.ASK_SMAPI_SERVER_BASE_URL; - if (stringUtils.isNonBlankString(envVarSmapiBaseUrl)) { - requestUrl = `${envVarSmapiBaseUrl}/${version}${general.url}`; - } - module.exports.requestWithUrl(clientMeta, requestUrl, general.method, headers, payload, profile, doDebug, callback); -}; - -// request with URL directly -module.exports.requestWithUrl = (clientMeta, url, method, headers, payload, profile, doDebug, callback) => { - let cliUserAgentStr = `ask-cli-x/${require('../../package.json').version} Node/${process.version} ${os.type()}/${os.release()}`; - let apiName; - - if (stringUtils.isNonBlankString(process.env.ASK_DOWNSTREAM_CLIENT)) { - cliUserAgentStr = `${process.env.ASK_DOWNSTREAM_CLIENT} (ask-cli downstream client) ${cliUserAgentStr}`; - } - - if (typeof clientMeta === 'object') { - apiName = clientMeta.apiName; - headers['User-Agent'] = `command/${clientMeta.callerCommand} ${cliUserAgentStr}`; - } else { - apiName = clientMeta; - headers['User-Agent'] = cliUserAgentStr; - } - - const params = { - url, - method, - headers, - body: payload, - json: payload ? true : false - }; - - oauthWrapper.tokenRefreshAndRead(params, profile, () => { - request(params, (error, response) => { - if (error || response === null || response.statusCode === null) { - console.error('[Error]: Failed to make request to the Alexa Skill Management API service.'); - if (doDebug) { - console.error(); - console.error(error); - } - process.exit(1); - } else if (response.statusCode >= 300) { - if (doDebug) { - logger.getInstance().debug(debugContentForResponse(apiName, response)); - } - if ((apiName === 'head-model' && response.statusCode === 404) || - (apiName === 'get-skill-status' && response.statusCode === 404) || - (apiName === 'get-skill' && response.statusCode === 303) || - (response.statusCode === 412)) { - callback(response); - } else if (apiName === 'simulate-skill' || - (apiName === 'get-simulation' && response.statusCode === 429)) { - callback(response); - } else { - // no callback - console.error('Call ' + apiName + ' error.'); - console.error('Error code: ' + response.statusCode); - if (response.body && tools.convertDataToJsonObject(response.body)) { - console.error(JSON.stringify(tools.convertDataToJsonObject(response.body), null, 2)); - } - process.exit(1); - } - } else { - if (doDebug) { - logger.getInstance().debug(debugContentForResponse(apiName, response)); - } - callback(response); - } - }); - }); -}; - -/* - * Form the debug info object according to the response from each request - * Based on the best practice of what should be logged for each request, - * returned object which includes following field: - * - request id - * - request headers - * - response headers - * - * @params apiName - * @params dataBody - * @return debug content for response - */ -function debugContentForResponse(apiName, response) { - return { - activity: apiName, - 'request-id': response.headers['x-amzn-requestid'], - request: { - method: response.request.method, - url: response.request.href, - headers: response.request.headers, - body: response.request.body - }, - response: { - statusCode: response.statusCode, - statusMessage: response.statusMessage, - headers: response.headers - }, - body: response.body - }; -} diff --git a/lib/builtins/deploy-delegates/cfn-deployer/index.js b/lib/builtins/deploy-delegates/cfn-deployer/index.js index c267e4a0..f62af525 100644 --- a/lib/builtins/deploy-delegates/cfn-deployer/index.js +++ b/lib/builtins/deploy-delegates/cfn-deployer/index.js @@ -45,7 +45,7 @@ function invoke(reporter, options, callback) { const { profile, alexaRegion, skillId, skillName, code, userConfig, deployState } = options; const awsProfile = awsUtil.getAWSProfile(profile); if (!awsProfile) { - return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask init" to re-configure your porfile.`); + return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask configure" to re-configure your porfile.`); } let currentRegionDeployState = deployState[alexaRegion]; diff --git a/lib/builtins/deploy-delegates/lambda-deployer/index.js b/lib/builtins/deploy-delegates/lambda-deployer/index.js index 7be99cd1..45d981cc 100644 --- a/lib/builtins/deploy-delegates/lambda-deployer/index.js +++ b/lib/builtins/deploy-delegates/lambda-deployer/index.js @@ -39,7 +39,7 @@ function invoke(reporter, options, callback) { const { profile, alexaRegion, skillId, skillName, code, userConfig, deployState } = options; const awsProfile = awsUtil.getAWSProfile(profile); if (!stringUtils.isNonBlankString(awsProfile)) { - return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask init" to re-configure your profile.`); + return callback(`Profile [${profile}] doesn't have AWS profile linked to it. Please run "ask configure" to re-configure your porfile.`); } let currentRegionDeployState = deployState[alexaRegion]; if (!currentRegionDeployState) { diff --git a/lib/clients/http-client.js b/lib/clients/http-client.js index b8e7a3a4..32e62f6f 100644 --- a/lib/clients/http-client.js +++ b/lib/clients/http-client.js @@ -37,6 +37,11 @@ function request(options, operation, doDebug, callback) { return; } + const proxyUrl = process.env.ASK_CLI_PROXY; + if (stringUtils.isNonBlankString(proxyUrl)) { + requestOptions.proxy = proxyUrl; + } + // Set user-agent for each CLI request if (!requestOptions.headers) { requestOptions.headers = {}; diff --git a/lib/clients/lwa-auth-code-client/index.js b/lib/clients/lwa-auth-code-client/index.js new file mode 100644 index 00000000..87451598 --- /dev/null +++ b/lib/clients/lwa-auth-code-client/index.js @@ -0,0 +1,118 @@ +const { URL } = require('url'); +const queryString = require('querystring'); +const isAfter = require('date-fns/isAfter'); +const parseISO = require('date-fns/parseISO'); +const stringUtils = require('@src/utils/string-utils'); +const CONSTANTS = require('@src/utils/constants'); +const httpClient = require('@src/clients/http-client'); +const providerChainUtils = require('@src/utils/provider-chain-utils'); + +module.exports = class LWAAuthCodeClient { + constructor(config) { + this.config = this._handleDefaultLwaAuthCodeConfiguration(config); + } + + /** + * @param {String} authCode | used for fetching accessTokens + * @param {Function} callback (err, accessToken) + * @returns accessToken | Used for request validation in skill development process. + */ + getAccessTokenUsingAuthCode(authCode, callback) { + const url = new URL(this.config.tokenPath, this.config.tokenHost); + const body = { + grant_type: 'authorization_code', + redirect_uri: this.config.redirectUri, + client_id: this.config.clientId, + client_secret: this.config.clientConfirmation, + code: authCode + }; + const options = { + url: `${url}`, + method: 'POST', + body, + json: !!body + }; + return httpClient.request(options, 'GET_ACCESS_TOKEN', this.config.doDebug, (err, response) => { + if (err) { + return callback(err); + } + callback(null, response.body); + }); + } + + /** + * @param {Object} token | accessToken of the profile being used currently. + * @param {Function} callback (err, token) + * @returns accessToken | a new access token. + */ + refreshToken(token, callback) { + const url = new URL(this.config.tokenPath, this.config.tokenHost); + const body = { + grant_type: 'refresh_token', + refresh_token: token.refresh_token, + client_id: this.config.clientId, + client_secret: this.config.clientConfirmation + }; + const options = { + url: `${url}`, + method: 'POST', + body, + json: !!body + }; + return httpClient.request(options, 'GET_ACCESS_TOKEN_USING_REFRESH_TOKEN', this.config.doDebug, (err, response) => { + if (err) { + return callback(err); + } + callback(null, response.body); + }); + } + + /** + * @param {Object} token + * @returns boolean | checks validity of a given token + */ + isValidToken(token) { + return !isAfter(new Date(), parseISO(token.expires_at)); + } + + /** + * @returns {String} authorization code URL + */ + generateAuthorizeUrl() { + const queryParams = { + response_type: 'code', + client_id: this.config.clientId, + state: this.config.state + }; + if (stringUtils.isNonBlankString(this.config.scope)) { + queryParams.scope = this.config.scope; + } + if (stringUtils.isNonBlankString(this.config.redirectUri)) { + queryParams.redirect_uri = this.config.redirectUri; + } + const baseUrl = new URL(this.config.authorizePath, this.config.authorizeHost); + return `${baseUrl}?${queryString.stringify(queryParams)}`; + } + + /** + * @param {Object} authConfig + * @returns {Object} config | sets default values if some of the values are missing. + * @private + */ + _handleDefaultLwaAuthCodeConfiguration(authConfig) { + const { doDebug, redirectUri } = authConfig; + const authorizePath = CONSTANTS.LWA.DEFAULT_AUTHORIZE_PATH; + const tokenPath = CONSTANTS.LWA.DEFAULT_TOKEN_PATH; + + // Overwrite LWA options from Environmental Variable + const state = authConfig.state || Date.now(); + const scope = providerChainUtils.resolveProviderChain([authConfig.scope, CONSTANTS.LWA.DEFAULT_SCOPES]); + const clientId = providerChainUtils.resolveProviderChain([authConfig.clientId, process.env.ASK_LWA_CLIENT_ID, + CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_ID]); + const clientConfirmation = providerChainUtils.resolveProviderChain([authConfig.clientConfirmation, process.env.ASK_LWA_CLIENT_CONFIRMATION, + CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_CONFIRMATION]); + const authorizeHost = providerChainUtils.resolveProviderChain([process.env.ASK_LWA_AUTHORIZE_HOST, CONSTANTS.LWA.DEFAULT_AUTHORIZE_HOST]); + const tokenHost = providerChainUtils.resolveProviderChain([process.env.ASK_LWA_TOKEN_HOST, CONSTANTS.LWA.DEFAULT_TOKEN_HOST]); + return { clientId, clientConfirmation, authorizeHost, tokenHost, authorizePath, tokenPath, scope, state, redirectUri, doDebug }; + } +}; diff --git a/lib/clients/smapi-client/index.js b/lib/clients/smapi-client/index.js index 793d07d5..9548021f 100644 --- a/lib/clients/smapi-client/index.js +++ b/lib/clients/smapi-client/index.js @@ -1,8 +1,8 @@ const R = require('ramda'); const async = require('async'); const querystring = require('querystring'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const httpClient = require('@src/clients/http-client'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const CONSTANTS = require('@src/utils/constants'); const accountLinkingApi = require('./resources/account-linking'); @@ -73,7 +73,14 @@ module.exports = class SmapiClient { headers: EMPTY_HEADERS, body: NULL_PAYLOAD, }; - oauthWrapper.tokenRefreshAndRead(requestOptions, this.profile, () => { + const authorizationController = new AuthorizationController({ + auth_client_type: 'LWA' + }); + authorizationController.tokenRefreshAndRead(this.profile, (tokenErr, token) => { + if (tokenErr) { + return callback(tokenErr); + } + requestOptions.headers.authorization = token; httpClient.request(requestOptions, 'REDIRECT_URL', this.doDebug, (reqErr, reqResponse) => { if (reqErr) { return callback(reqErr); @@ -142,8 +149,14 @@ module.exports = class SmapiClient { body: payload, json: !!payload }; - // TODO: Improve oauthWrapper module since current tokenRefreshAndRead is short circuit style. - oauthWrapper.tokenRefreshAndRead(requestOptions, this.profile, () => { + const authorizationController = new AuthorizationController({ + auth_client_type: 'LWA' + }); + authorizationController.tokenRefreshAndRead(this.profile, (tokenErr, token) => { + if (tokenErr) { + return callback(tokenErr); + } + requestOptions.headers.authorization = token; httpClient.request(requestOptions, apiName, this.doDebug, (reqErr, reqResponse) => { if (reqErr) { return callback(reqErr); diff --git a/lib/commands/abstract-command.js b/lib/commands/abstract-command.js index 4cbcccca..cd542386 100644 --- a/lib/commands/abstract-command.js +++ b/lib/commands/abstract-command.js @@ -1,8 +1,13 @@ +const os = require('os'); +const path = require('path'); + const { validateRequiredOption, validateOptionString, validateOptionRules, } = require('@src/commands/option-validator'); +const AppConfig = require('@src/model/app-config'); +const CONSTANTS = require('@src/utils/constants'); const Messenger = require('@src/view/messenger'); /** @@ -66,6 +71,14 @@ class AbstractCommand { // validate options try { this._validateOptions(args[0]); + + /** + * Since this code is ran for every command, we'll just be initiating appConfig here (no files created). + * Only `ask configure` command should have the eligibility to create the ASK config file (which is handled in the configure workflow). + */ + if (args[0]._name !== 'configure') { + this._initiateAppConfig(); + } } catch (err) { Messenger.getInstance().error(err); this.exit(1); @@ -147,6 +160,11 @@ class AbstractCommand { } } + _initiateAppConfig() { + const configFilePath = path.join(os.homedir(), CONSTANTS.FILE_PATH.ASK.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.ASK.PROFILE_FILE); + new AppConfig(configFilePath); + } + /** * Build the usage string for an option * @param {Object} optionModel diff --git a/lib/commands/configure/ask-profile-setup-helper.js b/lib/commands/configure/ask-profile-setup-helper.js new file mode 100644 index 00000000..61fb412d --- /dev/null +++ b/lib/commands/configure/ask-profile-setup-helper.js @@ -0,0 +1,135 @@ +const SmapiClient = require('@src/clients/smapi-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); +const jsonView = require('@src/view/json-view'); +const Messenger = require('@src/view/messenger'); +const CONSTANTS = require('@src/utils/constants'); + +const messages = require('./messages'); +const ui = require('./ui'); + +module.exports = { + setupAskToken, + setupVendorId +}; + +/** + * Initiates access token setup to be used in future requests. + * @param {Object} config + * @param {Function} callback + */ +function setupAskToken(config, callback) { + _getAccessToken(config, (accessTokenError, accessToken) => { + if (accessTokenError) { + return callback(accessTokenError); + } + callback(null, accessToken); + }); +} + +/** + * Retrieves access tokens to be used in future requests. + * @param {Object} config + * @param {Function} callback + * @private + */ +function _getAccessToken(config, callback) { + const authorizationController = new AuthorizationController(_buildAuthorizationConfiguration(config)); + if (!config.needBrowser) { + const authorizeUrl = authorizationController.getAuthorizeUrl(); + Messenger.getInstance().info(`Paste the following url to your browser:\n ${authorizeUrl}`); + ui.getAuthCode((error, authCode) => { + if (error) { + return callback(error); + } + authorizationController.getAccessTokenUsingAuthCode(authCode, (err, accessToken) => callback(err, err == null ? accessToken : err)); + }); + } else { + authorizationController.getTokensByListeningOnPort((err, accessToken) => callback(err, err == null ? accessToken : err)); + } +} + +/** + * Retrieves vendor info for a specific profile. + * @param {Object} config + * @param {Function} callback + * @private + */ +function setupVendorId(config, callback) { + _getVendorInfo(config, (err, vendorInfo) => { + if (err) { + return callback(err); + } + _selectVendorId(vendorInfo, (vendorIdError, vendorId) => { + if (vendorIdError) { + return callback(vendorIdError); + } + callback(null, vendorId); + }); + }); +} + +/** + * Gets vendor info. + * @param {Object} config contains profile name and debug information to create smapi client. + * @param {Function} callback + * @private + */ +function _getVendorInfo(config, callback) { + const smapiClient = new SmapiClient({ + profile: config.askProfile, + doDebug: config.debug + }); + + smapiClient.vendor.listVendors((err, response) => { + if (err) { + return callback(err); + } + if (response.statusCode >= 300) { + const error = jsonView.toString(response.body); + return callback(error); + } + callback(null, response.body.vendors); + }); +} + +/** + * Helper function to select a vendor ID if multiple vendor IDs are available. + * @param {Array} vendorInfo + * @param {Function} callback + */ +function _selectVendorId(vendorInfo, callback) { + if (!vendorInfo) { + return callback(messages.VENDOR_INFO_FETCH_ERROR); + } + const numberOfVendors = vendorInfo.length; + const LIST_PAGE_SIZE = 50; + switch (numberOfVendors) { + case 0: + process.nextTick(() => { + callback(messages.VENDOR_ID_CREATE_INSTRUCTIONS); + }); + break; + case 1: + process.nextTick(() => { + callback(null, vendorInfo[0].id); + }); + break; + default: + ui.chooseVendorId(LIST_PAGE_SIZE, vendorInfo, (error, vendorId) => callback(error, error === null ? vendorId : error)); + } +} + +/** + * Builds authorization configuration to be injected into Authorization Controller. + * @param {Object} config + * @private + */ +function _buildAuthorizationConfiguration(config) { + return { + config, + scopes: CONSTANTS.LWA.DEFAULT_SCOPES, + state: CONSTANTS.LWA.DEFAULT_STATE, + auth_client_type: 'LWA', + redirectUri: CONSTANTS.LWA.S3_RESPONSE_PARSER_URL + }; +} diff --git a/lib/commands/configure/aws-profile-setup-helper.js b/lib/commands/configure/aws-profile-setup-helper.js new file mode 100644 index 00000000..5de29949 --- /dev/null +++ b/lib/commands/configure/aws-profile-setup-helper.js @@ -0,0 +1,214 @@ +const awsProfileHandler = require('aws-profile-handler'); +const fs = require('fs-extra'); +const opn = require('opn'); +const os = require('os'); +const path = require('path'); +const querystring = require('querystring'); + +const CONSTANTS = require('@src/utils/constants'); +const Messenger = require('@src/view/messenger'); +const profileHelper = require('@src/utils/profile-helper'); +const stringUtils = require('@src/utils/string-utils'); + +const messages = require('./messages'); +const ui = require('./ui'); + +module.exports = { + setupAwsProfile, +}; + +/** + * Initiates Aws profile setup. + * @param {Object} config + * @param {Function} callback + */ +function setupAwsProfile(config, callback) { + let awsProfileList; + try { + const awsPath = path.join(os.homedir(), CONSTANTS.FILE_PATH.AWS.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.AWS.CREDENTIAL_FILE); + awsProfileList = fs.existsSync(awsPath) ? awsProfileHandler.listProfiles(awsPath) : []; + } catch (error) { + return process.nextTick(() => { + callback(error); + }); + } + _initiateAwsProfileSetup(config, awsProfileList, (err, awsProfile) => callback(err, err === null ? awsProfile : err)); +} + +/** + * Initiate aws profile setup. + * @param {Object} config + * @param {Array} awsProfileList + * @param {Function} callback + * @private + */ +function _initiateAwsProfileSetup(config, awsProfileList, callback) { + // 1.confirm if using AWS profile from prompt + ui.confirmSettingAws((setUpError, setupChoice) => { + if (setUpError) { + return callback(setUpError); + } + if (!setupChoice) { + Messenger.getInstance().info(messages.SKIP_AWS_CONFIGURATION); + return callback(); + } + // 2.check if using environment variable AWS profile + _handleEnvironmentVariableAwsSetup(config, (envVarErr, isAwsProfile) => { + if (envVarErr) { + return callback(envVarErr); + } + if (isAwsProfile) { + return callback(null, isAwsProfile); + } + // 3.create new AWS profile or select and update the existing one + if (awsProfileList.length === 0) { + _createAwsProfileFlow(config, awsProfileList, (error, awsProfile) => callback(error, error === null ? awsProfile : error)); + } else { + ui.createNewOrSelectAWSProfile(awsProfileList, (err, awsProfile) => { + if (err) { + return callback(err); + } + if (awsProfile === 'Create new profile') { + _createAwsProfileFlow(config, awsProfileList, (error, newAwsProfile) => { + if (error) { + return callback(error); + } + callback(null, newAwsProfile); + }); + } else { + profileHelper.setupProfile(awsProfile, config.askProfile); + callback(null, awsProfile); + } + }); + } + }); + }); +} + +/** + * Creates config folders, IAM roles based on input credentials. + * @param {Object} config + * @param {Array} awsProfileList + * @param {Function} callback + * @private + */ +function _createAwsProfileFlow(config, awsProfileList, callback) { + // 1.create .aws folders if necessary + try { + const awsCredentialsFile = path.join(os.homedir(), CONSTANTS.FILE_PATH.AWS.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.AWS.CREDENTIAL_FILE); + fs.ensureFileSync(awsCredentialsFile); + } catch (error) { + return callback(error); + } + // 2.profile name settle down + _decideAwsProfileName(awsProfileList, (error, awsProfileName) => { + if (error) { + return callback(error); + } + // 3.create IAM user based on profile name + const iamUserName = `ask-cli-${stringUtils.filterNonAlphanumeric(awsProfileName)}`; + _openIamCreateUserPage(config.needBrowser, iamUserName, () => { + // 4.let users input their AWS user credentials + ui.addNewCredentials((err, credentialObject) => { + if (err) { + return callback(error); + } + awsProfileHandler.addProfile(awsProfileName, credentialObject); + profileHelper.setupProfile(awsProfileName, config.askProfile); + Messenger.getInstance().info(`\nAWS profile "${awsProfileName}" was successfully created. \ +The details are recorded in aws credentials file (.aws/credentials) located at your **HOME** folder.`); + callback(null, awsProfileName); + }); + }); + }); +} + +/** + * requests selection of profile setup using environment variables. + * @param {Function} callback + * @private + */ +function _handleEnvironmentVariableAwsSetup(config, callback) { + if (!_hasEnvironmentVariables()) { + return process.nextTick(() => { + callback(null, false); + }); + } + ui.selectEnvironmentVariables((err, selectEnvVarChoice) => { + if (err) { + return callback(err); + } + if (selectEnvVarChoice === 'No') { + callback(null, false); + } else { + profileHelper.setupProfile(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS, config.askProfile); + callback(null, CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); + } + }); +} + +/** + * Checks existence of environment variables. + * @private + */ +function _hasEnvironmentVariables() { + const accessKeyID = process.env.AWS_ACCESS_KEY_ID; + const accessSecretKey = process.env.AWS_SECRET_ACCESS_KEY; + return stringUtils.isNonBlankString(accessKeyID) && stringUtils.isNonBlankString(accessSecretKey); +} + +/** + * Opens IAM create user page for confirmation of permissions. + * @param {Boolean} isBrowser + * @param {String} userName + * @param {Function} callback + * @private + */ +function _openIamCreateUserPage(isBrowser, userName, callback) { + const params = { + accessKey: true, + step: 'review', + userNames: userName, + permissionType: 'policies', + policies: [ + CONSTANTS.AWS.IAM.USER.POLICY_ARN.IAM_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.CFN_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.S3_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.LAMBDA_FULL + ] + }; + const awsIamUrl = `${CONSTANTS.AWS.IAM.USER.NEW_USER_BASE_URL}${querystring.stringify(params)}`; + Messenger.getInstance().info(messages.AWS_CREATE_PROFILE_TITLE); + if (isBrowser) { + setTimeout(() => { + opn(awsIamUrl); + callback(); + }, CONSTANTS.CONFIGURATION.OPN_BROWSER_DELAY); + } else { + Messenger.getInstance().info(messages.AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER); + Messenger.getInstance().info(` ${awsIamUrl}`); + process.nextTick(() => { + callback(); + }); + } +} + +/** + * Requests for confirmation of AWS profle name. + * @param {Array} awsProfileList + * @param {Function} callback + * @private + */ +function _decideAwsProfileName(awsProfileList, callback) { + if (awsProfileList.length === 0) { + // if the credential file is empty, CLI will name the first profile as 'ask_cli_default' + process.nextTick(() => callback(null, CONSTANTS.COMMAND.CONFIGURE.AWS_DEFAULT_PROFILE_NAME)); + } else { + ui.requestAwsProfileName(awsProfileList, (error, userInputName) => { + if (error) { + return callback(error); + } + callback(null, userInputName); + }); + } +} diff --git a/lib/commands/configure/helper.js b/lib/commands/configure/helper.js new file mode 100644 index 00000000..63f59f9d --- /dev/null +++ b/lib/commands/configure/helper.js @@ -0,0 +1,48 @@ +const AppConfig = require('@src/model/app-config'); +const Messenger = require('@src/view/messenger'); + +const askProfileSetupHelper = require('./ask-profile-setup-helper'); +const awsProfileSetupHelper = require('./aws-profile-setup-helper'); +const messages = require('./messages'); + + +module.exports = { + initiateAskProfileSetup +}; + +/** + * Initialization process for a new profile. + * @param {Object} config + * @param {Function} callback + */ +function initiateAskProfileSetup(config, callback) { + askProfileSetupHelper.setupAskToken(config, (accessTokenError, accessToken) => { + if (accessTokenError) { + return callback(accessTokenError); + } + AppConfig.getInstance().setToken(config.askProfile, accessToken); + Messenger.getInstance().info(`ASK Profile "${config.askProfile}" was successfully created. The details are recorded in ask-cli config file (.ask/cli_config) located at your **HOME** folder.`); + AppConfig.getInstance().write(); + + askProfileSetupHelper.setupVendorId(config, (vendorIdError, vendorId) => { + if (vendorIdError) { + return callback(vendorIdError); + } + AppConfig.getInstance().setVendorId(config.askProfile, vendorId); + Messenger.getInstance().info(`Vendor ID set as ${vendorId}.\n`); + AppConfig.getInstance().write(); + + Messenger.getInstance().info(messages.AWS_CONFIGURATION_MESSAGE); + awsProfileSetupHelper.setupAwsProfile(config, (awsProfileError, awsProfile) => { + if (awsProfileError) { + return callback(awsProfileError); + } + if (awsProfile) { + Messenger.getInstance().info(`AWS profile "${awsProfile}" was successfully associated with your ASK profile "${config.askProfile}".\n`); + } + AppConfig.getInstance().setAwsProfile(config.askProfile, awsProfile); + callback(null, config.askProfile); + }); + }); + }); +} diff --git a/lib/commands/configure/index.js b/lib/commands/configure/index.js new file mode 100644 index 00000000..ec68ee73 --- /dev/null +++ b/lib/commands/configure/index.js @@ -0,0 +1,119 @@ +const fs = require('fs-extra'); +const jsonfile = require('jsonfile'); +const os = require('os'); +const path = require('path'); + +const { AbstractCommand } = require('@src/commands/abstract-command'); +const optionModel = require('@src/commands/option-model'); +const AppConfig = require('@src/model/app-config'); +const CONSTANTS = require('@src/utils/constants'); +const stringUtils = require('@src/utils/string-utils'); +const Messenger = require('@src/view/messenger'); + +const helper = require('./helper'); +const messages = require('./messages'); +const ui = require('./ui'); + +class ConfigureCommand extends AbstractCommand { + name() { + return 'configure'; + } + + description() { + return 'helps to configure the credentials that ask-cli uses to authenticate the user to Amazon developer services'; + } + + requiredOptions() { + return []; + } + + optionalOptions() { + return ['noBrowser', 'profile', 'debug']; + } + + handle(cmd, cb) { + Messenger.getInstance().info(messages.ASK_CLI_CONFIGURATION_MESSAGE); + let askProfileConfig; + try { + askProfileConfig = _ensureAppConfigInitiated(cmd); + } catch (appConfigError) { + Messenger.getInstance().error(appConfigError); + return cb(appConfigError); + } + + _initiateAskProfileSetup(askProfileConfig, (setupError, askProfile) => { + if (setupError) { + Messenger.getInstance().error(setupError); + return cb(setupError); + } + const appConfig = AppConfig.getInstance(); + Messenger.getInstance().info(messages.CONFIGURE_SETUP_SUCCESS_MESSAGE); + Messenger.getInstance().info(`ASK Profile: ${askProfile}`); + Messenger.getInstance().info(`AWS Profile: ${appConfig.getAwsProfile(askProfile)}`); + Messenger.getInstance().info(`Vendor ID: ${appConfig.getVendorId(askProfile)}`); + AppConfig.dispose(); + cb(); + }); + } +} + +/** + * Initiates ASK profile setup. + * @param {Object} askProfileConfig + * @param {Function} callback + * @private + */ +function _initiateAskProfileSetup(askProfileConfig, callback) { + if (askProfileConfig.isFirstTimeProfileCreation || askProfileConfig.askProfile) { + const profile = (askProfileConfig.askProfile || CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME).trim(); + if (!stringUtils.validateSyntax('PROFILE_NAME', profile)) { + return callback(messages.PROFILE_NAME_VALIDATION_ERROR); + } + askProfileConfig.askProfile = profile; + return helper.initiateAskProfileSetup(askProfileConfig, (err, askProfile) => callback(err, askProfile)); + } + ui.createOrUpdateProfile(AppConfig.getInstance().getProfilesList(), (error, profile) => { + if (error) { + return callback(error); + } + askProfileConfig.askProfile = profile; + helper.initiateAskProfileSetup(askProfileConfig, (err, askProfile) => callback(err, askProfile)); + }); +} + +/** + * Ensures AppConfig is initiated properly before proceeding to further steps. + * @param {Object} cmd commander object. + * @private + */ +function _ensureAppConfigInitiated(cmd) { + const askFolderPath = path.join(os.homedir(), CONSTANTS.FILE_PATH.ASK.HIDDEN_FOLDER); + const configFilePath = path.join(askFolderPath, CONSTANTS.FILE_PATH.ASK.PROFILE_FILE); + if (!fs.existsSync(configFilePath)) { + fs.ensureDirSync(askFolderPath); + jsonfile.writeFileSync(configFilePath, { profiles: {} }, { spaces: CONSTANTS.CONFIGURATION.JSON_DISPLAY_INDENT }); + } + new AppConfig(configFilePath); + return _buildAskConfig(cmd, askFolderPath, configFilePath); +} + +/** + * Build configuration from input CLI arguments. + * @param {Object} cmd commander object. + * @param {String} askFolderPath ask folder path. + * @param {String} configFilePath cli_config file path. + * @private + */ +function _buildAskConfig(cmd, askFolderPath, configFilePath) { + return { + askFolderPath, + configFilePath, + isFirstTimeProfileCreation: AppConfig.getInstance().getProfilesList().length < 1, + askProfile: cmd.profile, + needBrowser: cmd.browser, + debug: cmd.debug + }; +} + +module.exports = ConfigureCommand; +module.exports.createCommand = new ConfigureCommand(optionModel).createCommand(); diff --git a/lib/commands/configure/messages.js b/lib/commands/configure/messages.js new file mode 100644 index 00000000..c838a13c --- /dev/null +++ b/lib/commands/configure/messages.js @@ -0,0 +1,25 @@ +module.exports.ASK_SIGN_IN_FAILURE_MESSAGE = 'Access not granted. Please verify your account credentials are correct.\n' ++ 'If this is your first time getting the error, please retry "ask configure" to ensure any browser-cached tokens are refreshed.'; + +module.exports.ASK_CLI_CONFIGURATION_MESSAGE = `This command will configure the ASK CLI with a profile associated with your Amazon developer credentials. +------------------------- Step 1 of 2 : ASK CLI Configuration -------------------------`; + +module.exports.PROFILE_NAME_VALIDATION_ERROR = 'Invalid profile name. A profile name can contain upper and lowercase letters, numbers, hyphens, and underscores.'; + +module.exports.CONFIGURE_SETUP_SUCCESS_MESSAGE = `------------------------- Configuration Complete ------------------------- +Here is the summary for the profile setup: `; + +module.exports.VENDOR_INFO_FETCH_ERROR = 'Could not retrieve vendor data.'; + +module.exports.VENDOR_ID_CREATE_INSTRUCTIONS = 'There is no Vendor ID associated with your account. To setup Vendor ID, please follow the instructions here: https://developer.amazon.com/en-US/docs/alexa/smapi/manage-credentials-with-ask-cli.html#vendor-id'; + +module.exports.AWS_CONFIGURATION_MESSAGE = '------------------------- Step 2 of 2 : Associate an AWS Profile with ASK CLI -------------------------'; + +module.exports.AWS_CREATE_PROFILE_TITLE = '\nComplete the IAM user creation with required permissions from the AWS console, then come back to the terminal.'; + +module.exports.AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER = 'Please open the following url in your browser:'; + +module.exports.ACCESS_SECRET_KEY_AND_ID_SETUP = '\nPlease fill in the "Access Key ID" and "Secret Access Key" from the IAM user creation final page.'; + +module.exports.SKIP_AWS_CONFIGURATION = `------------------------- Skipping the AWS profile association ------------------------- +You will only be able to deploy your Alexa skill. To set up AWS credentials later, use the "ask configure" command towards the same profile again.`; diff --git a/lib/commands/init/questions.js b/lib/commands/configure/questions.js similarity index 78% rename from lib/commands/init/questions.js rename to lib/commands/configure/questions.js index 06b28079..d41d32ac 100644 --- a/lib/commands/init/questions.js +++ b/lib/commands/configure/questions.js @@ -3,10 +3,10 @@ const stringUtils = require('@src/utils/string-utils'); module.exports = { REQUEST_ASK_PROFILE_NAME: [{ - message: `Please provide a profile name or press enter to use ${CONSTANTS.COMMAND.INIT.ASK_DEFAULT_PROFILE_NAME} as the profile name: `, + message: `Please provide a profile name or press enter to use ${CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME} as the profile name: `, type: 'input', name: 'profile', - default: CONSTANTS.COMMAND.INIT.ASK_DEFAULT_PROFILE_NAME + default: CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME }], SELECT_VENDOR_ID: [{ type: 'rawlist', @@ -22,7 +22,7 @@ module.exports = { type: 'input', name: 'awsProfileName', message: 'Please provide your AWS profile name: ', - default: existingProfiles.includes(CONSTANTS.COMMAND.INIT.AWS_DEFAULT_PROFILE_NAME) ? null : CONSTANTS.COMMAND.INIT.AWS_DEFAULT_PROFILE_NAME, + default: existingProfiles.includes(CONSTANTS.COMMAND.CONFIGURE.AWS_DEFAULT_PROFILE_NAME) ? null : CONSTANTS.COMMAND.CONFIGURE.AWS_DEFAULT_PROFILE_NAME, validate: (input) => { if (!stringUtils.isNonBlankString(input)) { return 'Profile name can not be blank string.'; @@ -78,5 +78,18 @@ module.exports = { return true; } } + ], + REQUEST_AUTH_CODE: [ + { + type: 'input', + name: 'authCode', + message: 'Please enter the Authorization Code: ', + validate: (value) => { + if (!stringUtils.isNonBlankString(value.trim())) { + return 'Please enter a valid Authorization Code.'; + } + return true; + } + } ] }; diff --git a/lib/commands/configure/ui.js b/lib/commands/configure/ui.js new file mode 100644 index 00000000..0c5edd58 --- /dev/null +++ b/lib/commands/configure/ui.js @@ -0,0 +1,215 @@ +const inquirer = require('inquirer'); +const Messenger = require('@src/view/messenger'); +const stringUtils = require('@src/utils/string-utils'); + +const messages = require('./messages'); +const questions = require('./questions'); + +module.exports = { + createNewProfile, + chooseVendorId, + requestAwsProfileName, + confirmSettingAws, + selectEnvironmentVariables, + addNewCredentials, + createOrUpdateProfile, + createNewOrSelectAWSProfile, + getAuthCode, + profileFormatter +}; + +/** +* Ask the auth code from the user. +* @param callback Authorization code +* @private +*/ +function getAuthCode(callback) { + inquirer.prompt(questions.REQUEST_AUTH_CODE).then((answer) => { + callback(null, answer.authCode); + }).catch((error) => { + callback(error); + }); +} + +// ASK profile setup flow + +/** +* Requests for a new profile name. +* @param {Function} callback +*/ +function createNewProfile(callback) { + inquirer.prompt(questions.REQUEST_ASK_PROFILE_NAME).then((answer) => { + const newProfileName = answer.profile.trim(); + if (!stringUtils.validateSyntax('PROFILE_NAME', newProfileName)) { + return callback(messages.PROFILE_NAME_VALIDATION_ERROR); + } + callback(null, newProfileName); + }).catch((error) => { + callback(error); + }); +} + +/** + * Requests selection of a vendor ID (if multiple exist). + * @param {Number} VENDOR_PAGE_SIZE | vendor page size. + * @param {Object} vendorInfo | object encapsulating vendor name and ID. + * @param {Function} callback + */ +function chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, callback) { + const question = questions.SELECT_VENDOR_ID; + question[0].pageSize = VENDOR_PAGE_SIZE; + question[0].choices = _buildVendorJSONString(vendorInfo); + inquirer.prompt(question).then((answers) => { + const vendorId = answers.selectedVendor.substr(answers.selectedVendor.lastIndexOf(':') + 2); + callback(null, vendorId); + }).catch((error) => { + callback(error); + }); +} + +/** + * Helper function to map vendor info object to a string. + * @param {Object} vendorInfo | object encapsulating vendor name and ID. + */ +function _buildVendorJSONString(vendorInfo) { + return vendorInfo.map(vendor => `${vendor.name}: ${vendor.id}`); +} + +/** + * Requests confirmation for creation of new profile or update existing profile. + * @param {Function} callback + */ +function createOrUpdateProfile(profiles, callback) { + const profileList = profileFormatter(profiles); + const NUMBER_OF_PROFILES_PER_PAGE = 50; + const HEADER = 'Profile Associated AWS Profile'; + const CREATE_PROFILE_MESSAGE = 'Create new profile'; + profileList.splice(0, 0, new inquirer.Separator()); + profileList.splice(1, 0, CREATE_PROFILE_MESSAGE); + profileList.splice(2, 0, new inquirer.Separator()); + profileList.splice(3, 0, new inquirer.Separator(HEADER)); + const question = questions.CREATE_NEW_OR_OVERRIDE; + question[0].pageSize = NUMBER_OF_PROFILES_PER_PAGE; + question[0].choices = profileList; + inquirer.prompt(question).then((answers) => { + if (answers.profile === CREATE_PROFILE_MESSAGE) { + createNewProfile((error, profileName) => { + if (error) { + return callback(error); + } + callback(null, profileName); + }); + } else { + const profile = answers.profile.split(' ')[0].trim().replace(/\[/, '').replace(/\]/, ''); + if (!stringUtils.validateSyntax('PROFILE_NAME', profile)) { + callback(messages.PROFILE_NAME_VALIDATION_ERROR); + return; + } + callback(null, profile); + } + }).catch((error) => { + callback(error); + }); +} + +// AWS Profile setup flow + +/** + * Request AWS profile name. + * @param {Array} awsProfileList | list of aws profile names. + * @param {Function} callback + */ +function requestAwsProfileName(awsProfileList, callback) { + inquirer.prompt(questions.REQUEST_AWS_PROFILE_NAME(awsProfileList)).then((answer) => { + callback(null, answer.awsProfileName.trim()); + }).catch((error) => { + callback(error); + }); +} + +/** + * Requests confirmation of setting up AWS profile. + * @param {Function} callback + */ +function confirmSettingAws(callback) { + inquirer.prompt(questions.SET_UP_AWS_PROFILE).then((answer) => { + callback(null, answer.choice); + }).catch((error) => { + callback(error); + }); +} + +/** + * Requests selection of environment variables. + * @param {Function} callback + */ +function selectEnvironmentVariables(callback) { + inquirer.prompt(questions.USE_ENVIRONMENT_VARIABLES).then((answer) => { + callback(null, answer.choice); + }).catch((error) => { + callback(error); + }); +} + +/** + * Requests access key and secret key. + * @param {Function} callback + */ +function addNewCredentials(callback) { + Messenger.getInstance().info(messages.ACCESS_SECRET_KEY_AND_ID_SETUP); + inquirer.prompt(questions.REQUEST_ACCESS_SECRET_KEY_AND_ID).then((answer) => { + const credentialObject = { + aws_access_key_id: answer.accessKeyId.trim(), + aws_secret_access_key: answer.secretAccessKey.trim() + }; + callback(null, credentialObject); + }).catch((error) => { + callback(error); + }); +} + +/** + * Requests confirmation for creation of new profile or selection of existing profile. + * @param {Function} callback + */ +function createNewOrSelectAWSProfile(awsProfileList, callback) { + const AWS_DISPLAY_PAGE_SIZE = 25; + const CREATE_NEW_PROFILE = 'Create new profile'; + awsProfileList.push(new inquirer.Separator()); + awsProfileList.push(CREATE_NEW_PROFILE); + awsProfileList.push(new inquirer.Separator()); + const question = questions.SELECT_OR_CREATE_AWS_PROFILE; + question[0].pageSize = AWS_DISPLAY_PAGE_SIZE; + question[0].choices = awsProfileList; + inquirer.prompt(question).then((answer) => { + callback(null, answer.chosenProfile); + }).catch((error) => { + callback(error); + }); +} + +/** + * Helper function to format input profiles' list into inquirer input format. + * @param {Array} profileList array of ASK profiles. + * @private + */ +function profileFormatter(profileList) { + const FORMATTER_SPACING = 26; + if (!profileList || profileList.length === 0) { + return null; + } + const formattedProfileList = []; + for (const profileObj of profileList) { + const formattedASKProfile = `[${profileObj.askProfile}]`; + let fillingSpace = ' '; + if (formattedASKProfile.length <= FORMATTER_SPACING) { + fillingSpace = ' '.repeat(FORMATTER_SPACING - formattedASKProfile.length); + } + if (!profileObj.awsProfile) { + formattedProfileList.push(`${formattedASKProfile}${fillingSpace}** NULL **`); + continue; + } + formattedProfileList.push(`${formattedASKProfile}${fillingSpace}"${profileObj.awsProfile}"`); + } + return formattedProfileList; +} diff --git a/lib/commands/init/ask-setup-helper.js b/lib/commands/init/ask-setup-helper.js deleted file mode 100644 index e7b731f8..00000000 --- a/lib/commands/init/ask-setup-helper.js +++ /dev/null @@ -1,89 +0,0 @@ -const jsonfile = require('jsonfile'); -const path = require('path'); -const os = require('os'); -const fs = require('fs'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); -const apiWrapper = require('@src/api/api-wrapper'); -const jsonRead = require('@src/utils/json-read'); -const tools = require('@src/utils/tools'); -const jsonUtility = require('@src/utils/json-utility'); -const profileHelper = require('@src/utils/profile-helper'); -const ui = require('./ui'); -const messages = require('./messages'); - -const VENDOR_PAGE_SIZE = 50; - -module.exports = { - setupAskConfig, - setVendorId, - isFirstTimeCreation -}; - -function setupAskConfig(tokens, askProfile, callback) { - try { - oauthWrapper.writeToken(tokens, askProfile); - callback(); - } catch (error) { - callback(`Failed to update the cli_config file with the retrieved token. ${error}`); - } -} - -function setVendorId(profile, doDebug, callback) { - apiWrapper.callListVendor(profile, doDebug, (data) => { - const homeConfigFile = path.join(os.homedir(), '.ask', 'cli_config'); - const propertyPathArray = ['profiles', profile, 'vendor_id']; - const homeConfig = jsonRead.readFile(homeConfigFile); - if (!homeConfig) { - callback(messages.ASK_CONFIG_NOT_FOUND_ERROR); - return; - } - const vendorInfo = tools.convertDataToJsonObject(data.body).vendors; - if (!vendorInfo) { - callback(messages.VENDOR_INFO_FETCH_ERROR); - return; - } - handleVendors(vendorInfo, homeConfigFile, propertyPathArray, callback); - }); -} - -function handleVendors(vendorInfo, homeConfigFile, propertyPathArray, callback) { - const numberOfVendors = vendorInfo.length; - switch (numberOfVendors) { - case 0: - callback('There is no vendor ID for your account.'); - break; - case 1: - jsonUtility.writeToProperty(homeConfigFile, propertyPathArray, vendorInfo[0].id); - callback(null, vendorInfo[0].id); - break; - default: - ui.chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, (vendorId) => { - jsonUtility.writeToProperty(homeConfigFile, propertyPathArray, vendorId); - callback(null, vendorId); - }); - } -} - -function createConfigFile(askFolder, cliConfig, newConfig, writeFileOptions) { - if (!fs.existsSync(askFolder)) { - fs.mkdirSync(askFolder); - } - jsonfile.writeFileSync(cliConfig, newConfig, writeFileOptions); -} - -function isFirstTimeCreation() { - const askFolder = path.join(os.homedir(), '.ask'); - const cliConfig = path.join(askFolder, 'cli_config'); - if (!fs.existsSync(cliConfig)) { - createConfigFile(askFolder, cliConfig, { profiles: {} }, { spaces: 2 }); - return true; - } - - const rawProfileList = profileHelper.getListProfile(); - if (!rawProfileList || rawProfileList.length === 0) { - createConfigFile(askFolder, cliConfig, { profiles: {} }, { spaces: 2 }); - return true; - } - - return false; -} diff --git a/lib/commands/init/aws-setup-helper.js b/lib/commands/init/aws-setup-helper.js deleted file mode 100644 index 5b9ee886..00000000 --- a/lib/commands/init/aws-setup-helper.js +++ /dev/null @@ -1,76 +0,0 @@ -const querystring = require('querystring'); -const opn = require('opn'); - -const profileHelper = require('@src/utils/profile-helper'); -const stringUtils = require('@src/utils/string-utils'); -const CONSTANTS = require('@src/utils/constants'); -const ui = require('./ui'); -const messages = require('./messages'); - -module.exports = { - handleEnvironmentVariableAwsSetup, - decideAwsProfileName, - openIamCreateUserPage -}; - -function handleEnvironmentVariableAwsSetup(askProfile, callback) { - if (!_hasEnvironmentVariables()) { - return process.nextTick(() => { - callback(null, false); - }); - } - ui.selectEnvironmentVariables((selectEnvVarChoice) => { - if (selectEnvVarChoice === 'No') { - callback(null, false); - } else { - profileHelper.setupProfile(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS, askProfile); - callback(null, CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); - } - }); -} - -function _hasEnvironmentVariables() { - const accessKeyID = process.env.AWS_ACCESS_KEY_ID; - const accessSecretKey = process.env.AWS_SECRET_ACCESS_KEY; - return stringUtils.isNonBlankString(accessKeyID) && stringUtils.isNonBlankString(accessSecretKey); -} - -function decideAwsProfileName(awsProfileList, callback) { - if (awsProfileList.length === 0) { - // if the credential file is empty, CLI will name the first profile as 'ask_cli_default' - process.nextTick(() => callback(CONSTANTS.COMMAND.INIT.AWS_DEFAULT_PROFILE_NAME)); - } else { - ui.requestAwsProfileName(awsProfileList, (userInputName) => { - callback(userInputName); - }); - } -} - -function openIamCreateUserPage(isBrowser, userName, callback) { - const params = { - accessKey: true, - step: 'review', - userNames: userName, - permissionType: 'policies', - policies: [ - CONSTANTS.AWS.IAM.USER.POLICY_ARN.IAM_FULL, - CONSTANTS.AWS.IAM.USER.POLICY_ARN.CFN_FULL, - CONSTANTS.AWS.IAM.USER.POLICY_ARN.S3_FULL, - CONSTANTS.AWS.IAM.USER.POLICY_ARN.LAMBDA_FULL - ] - }; - const awsIamUrl = `${CONSTANTS.AWS.IAM.USER.NEW_USER_BASE_URL}${querystring.stringify(params)}`; - console.log(messages.AWS_CREATE_PROFILE_TITLE); - if (isBrowser) { - setTimeout(() => { - opn(awsIamUrl); - callback(); - }, CONSTANTS.CONFIGURATION.OPN_BROWSER_DELAY); - } else { - console.log(messages.AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER); - console.log(` ${awsIamUrl}`); - process.nextTick(() => { - callback(); - }); - } -} diff --git a/lib/commands/init/aws-setup-wizard.js b/lib/commands/init/aws-setup-wizard.js deleted file mode 100644 index 97b9b63d..00000000 --- a/lib/commands/init/aws-setup-wizard.js +++ /dev/null @@ -1,84 +0,0 @@ -const os = require('os'); -const path = require('path'); -const fs = require('fs-extra'); -const awsProfileHandler = require('aws-profile-handler'); - -const profileHelper = require('@src/utils/profile-helper'); -const stringUtils = require('@src/utils/string-utils'); -const CONSTANTS = require('@src/utils/constants'); -const helper = require('./aws-setup-helper'); -const messages = require('./messages'); -const ui = require('./ui'); - - -module.exports = { - startFlow, - createAwsProfileFlow -}; - -function startFlow(isBrowser, askProfile, callback) { - let awsProfileList; - try { - const awsPath = path.join(os.homedir(), CONSTANTS.FILE_PATH.AWS.HIDDEN_FOLDER, CONSTANTS.FILE_PATH.AWS.CREDENTIAL_FILE); - awsProfileList = fs.existsSync(awsPath) ? awsProfileHandler.listProfiles(awsPath) : []; - } catch (error) { - return process.nextTick(() => { - callback(error); - }); - } - // 1.confirm if using AWS profile from prompt - ui.confirmSettingAws((setupChoice) => { - if (!setupChoice) { - console.log(messages.SKIP_AWS_INITIALIZATION); - return callback(); - } - // 2.check if using environment variable AWS profile - helper.handleEnvironmentVariableAwsSetup(askProfile, (envVarErr, isAwsProfile) => { - if (envVarErr) { - return callback(envVarErr); - } - if (isAwsProfile) { - return callback(null, isAwsProfile); - } - // 3.create new AWS profile or select and update the existing one - if (awsProfileList.length === 0) { - module.exports.createAwsProfileFlow(isBrowser, askProfile, awsProfileList, callback); - } else { - ui.createNewOrSelectAWSProfile(awsProfileList, (awsProfile) => { - if (awsProfile === 'Create new profile') { - module.exports.createAwsProfileFlow(isBrowser, askProfile, awsProfileList, callback); - } else { - profileHelper.setupProfile(awsProfile, askProfile); - callback(null, awsProfile); - } - }); - } - }); - }); -} - - -function createAwsProfileFlow(isBrowser, askProfile, awsProfileList, callback) { - // 1.create .aws folders if necessary - try { - const awsCredentialsFile = path.join(os.homedir(), '.aws', 'credentials'); - fs.ensureFileSync(awsCredentialsFile); - } catch (error) { - return callback(error); - } - // 2.profile name settle down - helper.decideAwsProfileName(awsProfileList, (awsProfileName) => { - // 3.create IAM user based on profile name - const iamUserName = `ask-cli-${stringUtils.filterNonAlphanumeric(awsProfileName)}`; - helper.openIamCreateUserPage(isBrowser, iamUserName, () => { - // 4.let users input their AWS user credentials - ui.addNewCredentials((credentialObject) => { - awsProfileHandler.addProfile(awsProfileName, credentialObject); - profileHelper.setupProfile(awsProfileName, askProfile); - console.log(`\nAWS profile "${awsProfileName}" was successfully created. \ -The details are recorded in aws credentials file ($HOME/.aws/credentials).`); - callback(null, awsProfileName); - }); - }); - }); -} diff --git a/lib/commands/init/handler.js b/lib/commands/init/handler.js deleted file mode 100644 index aee8d2a6..00000000 --- a/lib/commands/init/handler.js +++ /dev/null @@ -1,108 +0,0 @@ -const profileHelper = require('@src/utils/profile-helper'); -const CONSTANTS = require('@src/utils/constants'); -const lwaUtil = require('@src/utils/lwa'); -const stringUtils = require('@src/utils/string-utils'); -const awsSetupWizard = require('./aws-setup-wizard'); -const askSetupHelper = require('./ask-setup-helper'); -const messages = require('./messages'); -const ui = require('./ui'); - -const LIST_PAGE_SIZE = 50; - -module.exports = { - handleOptions -}; - -function handleOptions(options) { - if (options && typeof options === 'string') { - console.error(`[Error]: ${messages.INVALID_COMMAND_ERROR}`); - process.exit(1); - } else if (options.listProfiles) { - profileHelper.displayProfile(); - } else { - initProcessHandler(options); - } -} - -function initProcessHandler(options) { - console.log(messages.ASK_CLI_INITIALIZATION_MESSAGE); - // if first time creation (create the config file) or explicit profile setting, direct go to the init setup - if (askSetupHelper.isFirstTimeCreation() || options.profile) { - const askProfile = (options.profile || CONSTANTS.COMMAND.INIT.ASK_DEFAULT_PROFILE_NAME).trim(); - if (!profileHelper.askProfileSyntaxValidation(askProfile)) { - console.error(`[Error]: ${messages.PROFILE_NAME_VALIDATION_ERROR}`); - process.exit(1); - return; - } - directInitProcess(options.browser, askProfile, options.debug); - } else { - listInitProcess(options.browser, options.debug); - } -} - -function directInitProcess(isBrowser, askProfile, doDebug) { - // Step 1 ASK profile setup and VendorId retrieval - lwaUtil.accessTokenGenerator(isBrowser, {}, (tokenGeneratorError, token) => { - if (tokenGeneratorError) { - console.error(`[Error]: ${tokenGeneratorError}`); - process.exit(1); - return; - } - askSetupHelper.setupAskConfig(token, askProfile, (configSetupError) => { - if (configSetupError) { - console.error(`[Error]: ${configSetupError}`); - process.exit(1); - return; - } - console.log(`ASK Profile "${askProfile}" was successfully created. The details are recorded in ask-cli config ($HOME/.ask/cli_config).`); - askSetupHelper.setVendorId(askProfile, doDebug, (vendorIdError, vendorId) => { - if (vendorIdError) { - console.error(`[Error]: ${vendorIdError}`); - process.exit(1); - return; - } - console.log(`Vendor ID set as ${vendorId}.`); - console.log(); - // Step 2 AWS profile setup - console.log(messages.AWS_INITIALIZATION_MESSAGE); - awsSetupWizard.startFlow(isBrowser, askProfile, (resolveAwsProfileError, awsProfile) => { - if (resolveAwsProfileError) { - console.error(`[Error]: ${resolveAwsProfileError}`); - process.exit(1); - return; - } - if (awsProfile) { - console.log(`AWS profile "${awsProfile}" was successfully associated with your ASK profile "${askProfile}".`); - } - console.log(); - console.log('------------------------- Initialization Complete -------------------------'); - displayInitResult(askProfile, vendorId, awsProfile); - process.exit(); - }); - }); - }); - }); -} - -function listInitProcess(isBrowser, doDebug) { - const profileList = profileHelper.stringFormatter(profileHelper.getListProfile()); - ui.createOrUpdateProfile(LIST_PAGE_SIZE, profileList, (error, askProfile) => { - if (error) { - console.error(`[Error]: ${messages.PROFILE_NAME_VALIDATION_ERROR}`); - process.exit(1); - return; - } - directInitProcess(isBrowser, askProfile, doDebug); - }); -} - -function displayInitResult(askProfile, vendorId, awsProfile) { - console.log('Here is the summary for the profile setup: '); - console.log(` ASK Profile: ${askProfile}`); - if (stringUtils.isNonBlankString(awsProfile)) { - console.log(` AWS Profile: ${awsProfile}`); - } else { - console.log(` No AWS profile linked to profile "${askProfile}"`); - } - console.log(` Vendor ID: ${vendorId}`); -} diff --git a/lib/commands/init/index.js b/lib/commands/init/index.js deleted file mode 100644 index 0cdbfce0..00000000 --- a/lib/commands/init/index.js +++ /dev/null @@ -1,19 +0,0 @@ -const CONSTANTS = require('@src/utils/constants'); -const handler = require('./handler'); - -module.exports = { - createCommand: (commander) => { - commander - .command('init') - .description('initialize the ask-cli with your Amazon developer account credentials') - .option('-l, --list-profiles', 'list all the profiles for ask-cli') - .option('--no-browser', 'display authorization URL instead of opening browser') - .option('-p, --profile ', 'name for the profile to be created/updated') - .option('--debug', 'ask cli debug mode') - .option('-h, --help', 'output usage information', () => { - console.log(CONSTANTS.COMMAND.INIT.HELP_DESCRIPTION); - process.exit(0); - }) - .action(handler.handleOptions); - } -}; diff --git a/lib/commands/init/messages.js b/lib/commands/init/messages.js deleted file mode 100644 index 4937e181..00000000 --- a/lib/commands/init/messages.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - ASK_CLI_INITIALIZATION_MESSAGE: `This command will initialize the ASK CLI with a profile associated with your Amazon developer credentials. -------------------------- Step 1 of 2 : ASK CLI Initialization -------------------------`, - AWS_INITIALIZATION_MESSAGE: '------------------------- Step 2 of 2 : Associate an AWS Profile with ASK CLI -------------------------', - SKIP_AWS_INITIALIZATION: `------------------------- Skipping the AWS profile association ------------------------- -You will only be able to deploy your Alexa skill. To set up AWS credentials later, use the "ask init" command towards the same profile again.`, - AWS_CREATE_PROFILE_TITLE: '\nComplete the IAM user creation with required permissions from the AWS console, then come back to the terminal.', - AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER: 'Please open the following url in your browser:', - ACCESS_SECRET_KEY_AND_ID_SETUP: '\nPlease fill in the "Access Key ID" and "Secret Access Key" from the IAM user creation final page.', - INVALID_COMMAND_ERROR: 'Invalid command. Please run "ask init -h" for help.', - PROFILE_NAME_VALIDATION_ERROR: 'Invalid profile name. A profile name can contain upper and lowercase letters, numbers, hyphens, and underscores.', - VENDOR_INFO_FETCH_ERROR: 'Could not retrieve vendor data.', - ASK_CONFIG_NOT_FOUND_ERROR: 'Config file not found at $HOME/.ask/cli_config.' -}; diff --git a/lib/commands/init/ui.js b/lib/commands/init/ui.js deleted file mode 100644 index 818e2eb7..00000000 --- a/lib/commands/init/ui.js +++ /dev/null @@ -1,115 +0,0 @@ -const profileHelper = require('@src/utils/profile-helper'); -const inquirer = require('inquirer'); -const questions = require('./questions'); -const messages = require('./messages'); - -module.exports = { - createNewProfile, - chooseVendorId, - createOrUpdateProfile, - createNewOrSelectAWSProfile, - confirmSettingAws, - addNewCredentials, - confirmOverwritingProfile, - selectEnvironmentVariables, - requestAwsProfileName -}; - -function confirmSettingAws(callback) { - inquirer.prompt(questions.SET_UP_AWS_PROFILE).then((answer) => { - callback(answer.choice); - }); -} - -function selectEnvironmentVariables(callback) { - inquirer.prompt(questions.USE_ENVIRONMENT_VARIABLES).then((answer) => { - callback(answer.choice); - }); -} - -function addNewCredentials(callback) { - console.log(messages.ACCESS_SECRET_KEY_AND_ID_SETUP); - inquirer.prompt(questions.REQUEST_ACCESS_SECRET_KEY_AND_ID).then((answer) => { - const credentialObject = { - aws_access_key_id: answer.accessKeyId.trim(), - aws_secret_access_key: answer.secretAccessKey.trim() - }; - callback(credentialObject); - }); -} - -function confirmOverwritingProfile(profileName, callback) { - const question = questions.CONFIRM_OVERRIDING_PROFILE; - question[0].message = `The profile ${profileName} already exists. Do you want to overwrite the existing credentials?`; - inquirer.prompt(question).then((answer) => { - callback(answer.overwrite); - }); -} - -function createNewOrSelectAWSProfile(awsProfileList, callback) { - const AWS_DISPLAY_PAGE_SIZE = 25; - const CREATE_NEW_PROFILE = 'Create new profile'; - awsProfileList.push(new inquirer.Separator()); - awsProfileList.push(CREATE_NEW_PROFILE); - awsProfileList.push(new inquirer.Separator()); - const question = questions.SELECT_OR_CREATE_AWS_PROFILE; - question[0].pageSize = AWS_DISPLAY_PAGE_SIZE; - question[0].choices = awsProfileList; - inquirer.prompt(question).then((answer) => { - callback(answer.chosenProfile); - }); -} - -function createNewProfile(callback) { - inquirer.prompt(questions.REQUEST_ASK_PROFILE_NAME).then((answer) => { - const newProfileName = answer.profile.trim(); - if (!profileHelper.askProfileSyntaxValidation(newProfileName)) { - return callback(messages.PROFILE_NAME_VALIDATION_ERROR); - } - callback(null, newProfileName); - }); -} - -function buildVendorJSONString(vendorInfo) { - return vendorInfo.map(vendor => `${vendor.name}: ${vendor.id}`); -} - -function chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, callback) { - const question = questions.SELECT_VENDOR_ID; - question[0].pageSize = VENDOR_PAGE_SIZE; - question[0].choices = buildVendorJSONString(vendorInfo); - inquirer.prompt(question).then((answers) => { - const vendorId = answers.selectedVendor.substr(answers.selectedVendor.lastIndexOf(':') + 2); - callback(vendorId); - }); -} - -function createOrUpdateProfile(LIST_PAGE_SIZE, profileList, callback) { - const HEADER = 'Profile Associated AWS Profile'; - const CREATE_PROFILE_MESSAGE = 'Create new profile'; - profileList.splice(0, 0, new inquirer.Separator()); - profileList.splice(1, 0, CREATE_PROFILE_MESSAGE); - profileList.splice(2, 0, new inquirer.Separator()); - profileList.splice(3, 0, new inquirer.Separator(HEADER)); - const question = questions.CREATE_NEW_OR_OVERRIDE; - question[0].pageSize = LIST_PAGE_SIZE; - question[0].choices = profileList; - inquirer.prompt(question).then((answers) => { - if (answers.profile === CREATE_PROFILE_MESSAGE) { - createNewProfile(callback); - } else { - const profile = answers.profile.split(' ')[0].trim().replace(/\[/, '').replace(/\]/, ''); - if (!profileHelper.askProfileSyntaxValidation(profile)) { - callback(messages.PROFILE_NAME_VALIDATION_ERROR); - return; - } - callback(null, profile); - } - }); -} - -function requestAwsProfileName(awsProfileList, callback) { - inquirer.prompt(questions.REQUEST_AWS_PROFILE_NAME(awsProfileList)).then((answer) => { - callback(answer.awsProfileName.trim()); - }); -} diff --git a/lib/commands/option-model.json b/lib/commands/option-model.json index fd793a22..a0fe89c8 100644 --- a/lib/commands/option-model.json +++ b/lib/commands/option-model.json @@ -11,6 +11,12 @@ "alias": null, "stringInput": "NONE" }, + "noBrowser": { + "name": "no-browser", + "description": "ask cli displays a URL that you can use to sign in with your Amazon developer account from anywhere", + "alias": null, + "stringInput": "NONE" + }, "summary": { "name": "summary", "description": "summary for in-skill product", @@ -225,7 +231,7 @@ "name": "multiturn-token", "description": "optional multiturn token for dialog", "alias": null, - "stringInput": "REQUIRED" + "stringInput": "REQUIRED" }, "filters": { "name": "filters", diff --git a/lib/controllers/authorization-controller/index.js b/lib/controllers/authorization-controller/index.js new file mode 100644 index 00000000..f289a2dc --- /dev/null +++ b/lib/controllers/authorization-controller/index.js @@ -0,0 +1,164 @@ +const opn = require('opn'); +const portscanner = require('portscanner'); + +const LWAAuthCodeClient = require('@src/clients/lwa-auth-code-client'); +const AppConfig = require('@src/model/app-config'); +const CONSTANTS = require('@src/utils/constants'); +const stringUtils = require('@src/utils/string-utils'); +const Messenger = require('@src/view/messenger'); +const SpinnerView = require('@src/view/spinner-view'); + +const LocalHostServer = require('./server'); + +const messages = require('./messages'); + +module.exports = class AuthorizationController { + /** + * Constructor for AuthorizationController. + * @param {Object} config | contains default scopes, default state, auth_client_type (LWA), + * redirectUri, askProfile config file path, needBrowser, debug information. For more details, + * see @src/commands/configure/helper.js + */ + constructor(config) { + this.authConfig = config; + this.oauthClient = this._getAuthClientInstance(); + } + + /** + * Generates Authroization URL. + */ + getAuthorizeUrl() { + return this.oauthClient.generateAuthorizeUrl(); + } + + /** + * Retrieves access token. + * @param {String} authCode | authorization code. + * @param {Function} callback + */ + getAccessTokenUsingAuthCode(authCode, callback) { + return this.oauthClient.getAccessTokenUsingAuthCode(authCode, (err, accessToken) => callback(err, err == null ? accessToken : err)); + } + + /** + * Refreshes token and sets up authorization param. + * @param {String} profile | current profile in use. + * @param {Function} callback + */ + tokenRefreshAndRead(profile, callback) { + if (profile === CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.PROFILE_NAME) { + const askRefreshToken = process.env.ASK_REFRESH_TOKEN; + const askAccessToken = process.env.ASK_ACCESS_TOKEN; + + // '!== undefined' check is required because environment variables return values as strings instead of objects. + const isNonBlankRefreshToken = stringUtils.isNonBlankString(askRefreshToken) && askRefreshToken !== 'undefined'; + const isNonBlankAccessToken = stringUtils.isNonBlankString(askAccessToken) && askAccessToken !== 'undefined'; + if (!isNonBlankRefreshToken && !isNonBlankAccessToken) { + return callback(messages.ASK_ENV_VARIABLES_ERROR_MESSAGE); + } + if (isNonBlankRefreshToken) { + this._getRefreshTokenAndUpdateConfig(profile, (err, token) => callback(err, err === null ? token : err)); + } else if (isNonBlankAccessToken) { + return callback(null, askAccessToken); + } + } else if (this.oauthClient.isValidToken(AppConfig.getInstance().getToken(profile))) { + callback(null, AppConfig.getInstance().getToken(profile).access_token); + } else { + this._getRefreshTokenAndUpdateConfig(profile, (err, token) => callback(err, err === null ? token : err)); + } + } + + /** + * @param {String} profile | profile in use currently. + * @param {String} configFilePath | path for the config file. + * @param {Function} callback + * @private + */ + _getRefreshTokenAndUpdateConfig(profile, callback) { + this._getRefreshToken(profile, (err, refreshedAccessToken) => { + if (err) { + return callback(err); + } + AppConfig.getInstance().setToken(profile, refreshedAccessToken); + AppConfig.getInstance().write(); + callback(null, refreshedAccessToken.access_token); + }); + } + + /** + * Helper method to call refreshToken. + * @param {String} profile | current profile. + * @param {Function} callback + * @private + */ + _getRefreshToken(profile, callback) { + const token = AppConfig.getInstance().getToken(profile); + this.oauthClient.refreshToken(token, (err, refreshedAccessToken) => callback(err, err == null ? refreshedAccessToken : err)); + } + + /** + * Helper method to keep listening to LWA response. + * @param {Function} callback + */ + getTokensByListeningOnPort(callback) { + const PORT = CONSTANTS.LWA.LOCAL_PORT; + this.oauthClient.config.redirectUri = `http://127.0.0.1:${PORT}/cb`; + portscanner.checkPortStatus(PORT, (err, currentStatus) => { + if (err) { + return callback(err); + } + if (currentStatus !== 'closed') { + return callback(messages.PORT_OCCUPIED_WARN_MESSAGE); + } + Messenger.getInstance().info(messages.AUTH_MESSAGE); + + setTimeout(() => { + opn(this.oauthClient.generateAuthorizeUrl()); + }, CONSTANTS.CONFIGURATION.OPN_BROWSER_DELAY); + + this._listenResponseFromLWA(PORT, (error, authCode) => { + if (error) { + return callback(error); + } + this.oauthClient.getAccessTokenUsingAuthCode(authCode, + (accessTokenError, accessToken) => callback(accessTokenError, accessTokenError == null ? accessToken : accessTokenError)); + }); + }); + } + + /** + * Helper method to facilitate spinner view and handle clean up process. + * @param {String} PORT | port to be listened on. + * @param {Function} callback + * @private + */ + _listenResponseFromLWA(PORT, callback) { + const listenSpinner = new SpinnerView(); + const server = new LocalHostServer(PORT); + server.create((err, authCode) => { + listenSpinner.terminate(); + if (err) { + return callback(err); + } + return callback(null, authCode); + }); + server.listen(() => { + listenSpinner.start(` Listening on http://localhost:${PORT}...`); + }); + server.registerEvent('connection', (socket) => { + socket.unref(); + }); + } + + /** + * Factory method to facilitate substitution of other OAuth2 clients. + * @returns LWA client object. + * @private + */ + _getAuthClientInstance() { + if (this.authConfig.auth_client_type === 'LWA') { + const { clientId, clientConfirmation, authorizeHost, tokenHost, scope, state, doDebug, redirectUri } = this.authConfig; + return new LWAAuthCodeClient({ clientId, clientConfirmation, authorizeHost, tokenHost, scope, state, doDebug, redirectUri }); + } + } +}; diff --git a/lib/controllers/authorization-controller/messages.js b/lib/controllers/authorization-controller/messages.js new file mode 100644 index 00000000..c58e3beb --- /dev/null +++ b/lib/controllers/authorization-controller/messages.js @@ -0,0 +1,10 @@ +module.exports.AUTH_MESSAGE = 'Switch to "Login with Amazon" page and sign-in with your Amazon developer credentials.\n' ++ 'If your browser did not open the page, run the configuration process again with command "ask configure --no-browser".\n'; + +module.exports.PORT_OCCUPIED_WARN_MESSAGE = '[Warn]: 9090 port on localhost has been occupied, ' ++ 'ask-cli cannot start a local server for receiving authorization code.\nPlease either abort any processes running on port 9090\n' ++ 'or add `--no-browser` flag to the command as an alternative approach.'; + +module.exports.ASK_SIGN_IN_SUCCESS_MESSAGE = 'Sign in was successful. Close browser and return to the command line interface.'; + +module.exports.ASK_ENV_VARIABLES_ERROR_MESSAGE = 'Could not find either of the environment variables: ASK_ACCESS_TOKEN, ASK_REFRESH_TOKEN'; diff --git a/lib/controllers/authorization-controller/server.js b/lib/controllers/authorization-controller/server.js new file mode 100644 index 00000000..c5ac7988 --- /dev/null +++ b/lib/controllers/authorization-controller/server.js @@ -0,0 +1,47 @@ +const http = require('http'); +const url = require('url'); +const messages = require('./messages'); + +module.exports = class LocalHostServer { + constructor(PORT) { + this.port = PORT; + this.server = null; + } + + create(callback) { + this.server = http.createServer(this._defaultServerCallback(callback)); + } + + listen(callback) { + this.server.listen(this.port, callback); + } + + registerEvent(eventName, callback) { + this.server.on(eventName, callback); + } + + destroy() { + this.server.close(); + this.server.unref(); + } + + _defaultServerCallback(callback) { + return (request, response) => { + response.on('close', () => { + request.socket.destroy(); + }); + const requestUrl = request.url; + const requestQuery = url.parse(requestUrl, true).query; + this.destroy(); + if (requestUrl.startsWith('/cb?code')) { + response.end(messages.ASK_SIGN_IN_SUCCESS_MESSAGE); + callback(null, requestQuery.code); + } + if (requestUrl.startsWith('/cb?error')) { + response.statusCode = 403; + response.end(`Error: ${requestQuery.error}\nError description: ${requestQuery.error_description}`); + callback(messages.ASK_SIGN_IN_FAILURE_MESSAGE); + } + }; + } +}; diff --git a/lib/model/abstract-config-file.js b/lib/model/abstract-config-file.js index af000a6a..a267de33 100644 --- a/lib/model/abstract-config-file.js +++ b/lib/model/abstract-config-file.js @@ -107,13 +107,19 @@ module.exports = class ConfigFile { } catch (error) { throw new Error(`No access to read/write file ${this.path}.`); } - // check file extension - if (path.extname(this.path).toLowerCase() === '.json') { + // check file name + if (path.basename(this.path) === 'cli_config') { this.fileType = FILE_TYPE_JSON; - } else if (path.extname(this.path).toLowerCase() === '.yaml' || path.extname(this.path).toLowerCase() === '.yml') { - this.fileType = FILE_TYPE_YAML; } else { - throw new Error(`File type for ${this.path} is not supported. Only JSON and YAML files are supported.`); + // check file extension + const fileExtension = path.extname(this.path).toLowerCase(); + if (fileExtension === '.json') { + this.fileType = FILE_TYPE_JSON; + } else if (fileExtension === '.yaml' || fileExtension === '.yml') { + this.fileType = FILE_TYPE_YAML; + } else { + throw new Error(`File type for ${this.path} is not supported. Only JSON and YAML files are supported.`); + } } } }; diff --git a/lib/model/app-config.js b/lib/model/app-config.js index a5d415e9..1611dee6 100644 --- a/lib/model/app-config.js +++ b/lib/model/app-config.js @@ -51,4 +51,20 @@ module.exports = class AppConfig extends ConfigFile { setVendorId(profile, vendorId) { this.setProperty(['profiles', profile, 'vendor_id'], vendorId); } + + /** + * Returns all profile names and their associated aws profile names (if any) as list of objects. + * return profilesList. Eg: [{ askProfile: 'askProfile1', awsProfile: 'awsProfile1'}, { askProfile: 'askProfile2', awsProfile: 'awsProfile2'}]. + */ + getProfilesList() { + const profilesList = []; + const profilesObj = this.getProperty(['profiles']); + for (const profile of Object.getOwnPropertyNames(profilesObj)) { + profilesList.push({ + askProfile: profile, + awsProfile: profilesObj[profile].aws_profile + }); + } + return profilesList; + } }; diff --git a/lib/utils/constants.js b/lib/utils/constants.js index ab6090a5..f0f0ca03 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -96,6 +96,10 @@ module.exports.FILE_PATH = { AWS: { HIDDEN_FOLDER: '.aws', CREDENTIAL_FILE: 'credentials' + }, + ASK: { + HIDDEN_FOLDER: '.ask', + PROFILE_FILE: 'cli_config' } }; @@ -302,7 +306,16 @@ module.exports.LWA = { CLI_INTERNAL_ONLY_LWA_CLIENT: { CLIENT_ID: 'amzn1.application-oa2-client.aad322b5faab44b980c8f87f94fbac56', CLIENT_CONFIRMATION: '1642d8869b829dda3311d6c6539f3ead55192e3fc767b9071c888e60ef151cf9' - } + }, + DEFAULT_AUTHORIZE_HOST: 'https://www.amazon.com', + DEFAULT_AUTHORIZE_PATH: '/ap/oa', + DEFAULT_TOKEN_HOST: 'https://api.amazon.com', + DEFAULT_TOKEN_PATH: '/auth/o2/token', + LOCAL_PORT: 9090 +}; + +module.exports.REGEX_VALIDATIONS = { + PROFILE_NAME: /^[a-zA-Z0-9-_]+$/g }; module.exports.COMMAND = { @@ -310,14 +323,13 @@ module.exports.COMMAND = { DIALOG: 'dialog', SIMULATE: 'simulate', }, - INIT: Object.freeze({ - HELP_DESCRIPTION: '\n Usage: ask-init\n\n' + - ' $ ask init [--no-browser] [-l|--list-profiles] [-p|--profile] [--debug]\n\n' + + CONFIGURE: Object.freeze({ + HELP_DESCRIPTION: '\n Usage: ask-configure\n\n' + + ' $ ask configure [--no-browser] [-p|--profile] [--debug]\n\n' + ' To establish authorization for the CLI to create and modify skills ' + - 'associated with your Amazon developer account, you must run the init command.\n\n\n' + + 'associated with your Amazon developer account, you must run the configure command.\n\n\n' + ' Options:\n\n' + ' --no-browser display authorization url instead of opening browser\n' + - ' -l, --list-profiles list all the profile(s) for ask-cli\n' + ' -p, --profile name for the profile to be created/updated\n' + ' --aws-setup setup AWS profile with "Access Key ID" and "Secret Access Key"\n' + ' --debug ask cli debug mode\n' + diff --git a/lib/utils/profile-helper.js b/lib/utils/profile-helper.js index 8ddfc9a5..d30cfb0c 100644 --- a/lib/utils/profile-helper.js +++ b/lib/utils/profile-helper.js @@ -117,7 +117,7 @@ function resolveVendorId(profile) { const configFile = path.join(os.homedir(), '.ask', 'cli_config'); if (!fs.existsSync(configFile)) { - throw new Error('The app config for ask-cli does not exist. Please run "ask init" to complete the CLI initialization.'); + throw new Error('The app config for ask-cli does not exist. Please run "ask configure" to complete the CLI initialization.'); } else { const cliConfig = JSON.parse(fs.readFileSync(configFile, 'utf-8')); return R.view(R.lensPath(['profiles', profile, 'vendor_id']), cliConfig); diff --git a/lib/utils/provider-chain-utils.js b/lib/utils/provider-chain-utils.js new file mode 100644 index 00000000..d846db91 --- /dev/null +++ b/lib/utils/provider-chain-utils.js @@ -0,0 +1,23 @@ +const stringUtils = require('@src/utils/string-utils'); + +module.exports = { + resolveProviderChain +}; + +/** + * Utility function to return first non-blank value. + * @param {Array} array of values with top priority first. + * @returns first non-blank string. + * + * The check 'item !== undefined' is put in place to handle environment variables. + * When environment variables are accessed, if they aren't set, the resultant value is + * returned as 'undefined' (as a string) instead of undefined (as an object). + */ +function resolveProviderChain(chain) { + for (const item of chain) { + if (stringUtils.isNonBlankString(item) && item !== 'undefined') { + return item; + } + } + return null; +} diff --git a/lib/utils/string-utils.js b/lib/utils/string-utils.js index 483791e4..528883e9 100644 --- a/lib/utils/string-utils.js +++ b/lib/utils/string-utils.js @@ -1,11 +1,13 @@ const R = require('ramda'); +const CONSTANTS = require('@src/utils/constants'); module.exports = { isNonEmptyString, isNonBlankString, isLambdaFunctionName, filterNonAlphanumeric, - splitStringFilterAndMapTo + splitStringFilterAndMapTo, + validateSyntax }; function isNonEmptyString(str) { @@ -57,3 +59,13 @@ function splitStringFilterAndMapTo(string, delimiter, filterBy, mapper) { } return arr; } + +/** + * + * @param {String} name | type of regex to be matched against. + * @param {String} value | value to be tested. + * returns true if the value adheres with the regex pattern. + */ +function validateSyntax(name, value) { + return isNonBlankString(value) ? CONSTANTS.REGEX_VALIDATIONS[name].test(value) : false; +} diff --git a/package.json b/package.json index d758b43a..22443c69 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "bunyan": "^1.8.12", "chalk": "2.4.2", "commander": "^2.9.0", + "date-fns": "^2.7.0", "folder-hash": "^3.0.0", "fs-extra": "^2.1.0", "inquirer": "^6.2.0", diff --git a/test/functional/commands/api/index.js b/test/functional/commands/api/index.js index 5abd8416..a776f767 100644 --- a/test/functional/commands/api/index.js +++ b/test/functional/commands/api/index.js @@ -4,9 +4,9 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const Messenger = require('@src/view/messenger'); const { commander } = require('@src/commands/api/api-commander'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const httpClient = require('@src/clients/http-client'); const CONSTANTS = require('@src/utils/constants'); @@ -164,19 +164,11 @@ class ApiCommandBasicTest { // Mock the real behaviour of tokenRefreshAndRead // Set the headers property of requestOption object // to correspoding token of the profile - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsFake((requestOptions, profile, cb) => { + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsFake((profile, cb) => { if (profile === CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.PROFILE_NAME) { - Object.assign(requestOptions.headers, { - authorization: testDataProvider.VALID_TOKEN.ENV_PROFILE_TOKEN - }); - cb(); - } else { - if (this._TEST_DATA.ASK_CONFIG.profiles[profile]) { - Object.assign(requestOptions.headers, { - authorization: this._TEST_DATA.ASK_CONFIG.profiles[profile].token - }); - cb(); - } + cb(null, testDataProvider.VALID_TOKEN.ENV_PROFILE_TOKEN); + } else if (this._TEST_DATA.ASK_CONFIG.profiles[profile]) { + cb(null, this._TEST_DATA.ASK_CONFIG.profiles[profile].token); } }); diff --git a/test/functional/commands/api/manifest/get-manifest-test.js b/test/functional/commands/api/manifest/get-manifest-test.js index 75c762c4..cf305962 100644 --- a/test/functional/commands/api/manifest/get-manifest-test.js +++ b/test/functional/commands/api/manifest/get-manifest-test.js @@ -108,7 +108,7 @@ describe('Functional test - ask api get-manifest', () => { ASK_REFRESH_TOKEN: 3, ASK_VENDOR_ID: 4 }; - const httpMockConfig = [{ + const httpMockConfig = [{ input: [requestOptions, operation], output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] }]; @@ -131,7 +131,7 @@ describe('Functional test - ask api get-manifest', () => { json: false }; const envVar = {}; - const httpMockConfig = [{ + const httpMockConfig = [{ input: [requestOptions, operation], output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] }]; @@ -163,7 +163,7 @@ describe('Functional test - ask api get-manifest', () => { it('| getManifest function can handle http response with status code < 300', (done) => { const cmd = `ask api get-manifest -s ${TEST_SKILL_ID}`; const envVar = {}; - const httpMockConfig = [{ + const httpMockConfig = [{ input: [getManifestRequestOptionsWithDevelopmentStage, operation], output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] }]; @@ -196,7 +196,7 @@ describe('Functional test - ask api get-manifest', () => { const cmd = `ask api get-manifest -s ${TEST_SKILL_ID}`; const envVar = {}; const httpMockConfig = [ - { + { input: [getManifestRequestOptionsWithDevelopmentStage, operation], output: [null, { statusCode: 303, body: { location: TEST_LOCATION }}] }, @@ -218,7 +218,7 @@ describe('Functional test - ask api get-manifest', () => { const cmd = `ask api get-manifest -s ${TEST_SKILL_ID} -g ${TEST_LIVE_STAGE}`; const envVar = {}; const httpMockConfig = [ - { + { input: [getManifestRequestOptionsWithLiveStage, operation], output: [null, { statusCode: 303, body: { location: TEST_LOCATION }}] }, @@ -240,7 +240,7 @@ describe('Functional test - ask api get-manifest', () => { const cmd = `ask api get-manifest -s ${TEST_SKILL_ID}`; const envVar = {}; const httpMockConfig = [ - { + { input: [getManifestRequestOptionsWithDevelopmentStage, operation], output: [null, { statusCode: 303, body: { location: TEST_LOCATION }}] }, @@ -262,7 +262,7 @@ describe('Functional test - ask api get-manifest', () => { const cmd = `ask api get-manifest -s ${TEST_SKILL_ID}`; const envVar = {}; const httpMockConfig = [ - { + { input: [getManifestRequestOptionsWithDevelopmentStage, operation], output: [null, { statusCode: 303, body: {} }] } diff --git a/test/functional/commands/api/vendor/list-vendors-test.js b/test/functional/commands/api/vendor/list-vendors-test.js index cb5166be..b5e53b1a 100644 --- a/test/functional/commands/api/vendor/list-vendors-test.js +++ b/test/functional/commands/api/vendor/list-vendors-test.js @@ -1,5 +1,4 @@ const { expect } = require('chai'); -const chalk = require('chalk'); const jsonView = require('@src/view/json-view'); const CONSTANTS = require('@src/utils/constants'); const ApiCommandBasicTest = require('@test/functional/commands/api'); @@ -38,7 +37,7 @@ describe('Functional test - ask api list-vendors', () => { }; it('| Can get correct http response when profile is set to default ', (done) => { - const cmd = `ask api list-vendors`; + const cmd = 'ask api list-vendors'; const envVar = {}; const httpMockConfig = [{ input: [defaultRequestOption, operation], @@ -58,7 +57,7 @@ describe('Functional test - ask api list-vendors', () => { const envVar = {}; const httpMockConfig = [{ input: [requestOptionWithValidProfile, operation], - output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] + output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY }] }]; const expectationHandler = (msgCatcher) => { expect(msgCatcher.error).equal(''); @@ -77,9 +76,9 @@ describe('Functional test - ask api list-vendors', () => { ASK_REFRESH_TOKEN: 3, ASK_VENDOR_ID: TEST_ENV_VENDOR_ID } - const httpMockConfig = [{ + const httpMockConfig = [{ input: [requestOptionWithEnvVarProfile, operation], - output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] + output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY }] }]; const expectationHandler = (msgCatcher) => { expect(msgCatcher.error).equal(''); @@ -108,7 +107,7 @@ describe('Functional test - ask api list-vendors', () => { const envVar = {}; const httpMockConfig = [{ input: [defaultRequestOption, operation], - output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY}] + output: [null, { statusCode: 200, body: TEST_HTTP_RESPONSE_BODY }] }]; const expectationHandler = (msgCatcher) => { expect(msgCatcher.error).equal(''); @@ -140,7 +139,7 @@ describe('Functional test - ask api list-vendors', () => { const envVar = {}; const httpMockConfig = [{ input: [defaultRequestOption, operation], - output: [null, { statusCode : 300, body: TEST_HTTP_RESPONSE_BODY}] + output: [null, { statusCode: 300, body: TEST_HTTP_RESPONSE_BODY }] }]; const expectationHandler = (msgCatcher) => { expect(msgCatcher.info).equal(''); diff --git a/test/unit/builtins/lambda-deployer/index-test.js b/test/unit/builtins/lambda-deployer/index-test.js index 327269c8..079fe8c9 100644 --- a/test/unit/builtins/lambda-deployer/index-test.js +++ b/test/unit/builtins/lambda-deployer/index-test.js @@ -13,6 +13,7 @@ describe('Builtins test - lambda-deployer index.js test', () => { const TEST_AWS_REGION_DEFAULT = 'us-east-1'; const TEST_SKILL_NAME = 'skill_name'; const TEST_IAM_ROLE_ARN = 'IAM role arn'; + const NULL_PROFILE = 'null'; describe('# test class method: bootstrap', () => { afterEach(() => { @@ -65,8 +66,8 @@ describe('Builtins test - lambda-deployer index.js test', () => { const TEST_OPTIONS_WITHOUT_PROFILE = { profile: null, }; - const TEST_ERROR = `Profile [${null}] doesn't have AWS profile linked to it. Please run "ask init" to re-configure your profile.`; - sinon.stub(awsUtil, 'getAWSProfile').withArgs(null).returns(null); + const TEST_ERROR = `Profile [${NULL_PROFILE}] doesn't have AWS profile linked to it. Please run "ask configure" to re-configure your porfile.`; + sinon.stub(awsUtil, 'getAWSProfile').withArgs(NULL_PROFILE).returns(NULL_PROFILE); // call lambdaDeployer.invoke(REPORTER, TEST_OPTIONS_WITHOUT_PROFILE, (err) => { // verify diff --git a/test/unit/clients/http-client-test.js b/test/unit/clients/http-client-test.js index 9614404b..245718e8 100644 --- a/test/unit/clients/http-client-test.js +++ b/test/unit/clients/http-client-test.js @@ -100,6 +100,36 @@ describe('Clients test - cli http request client', () => { }); }); + describe('# embedding proxyUrl', () => { + let stubRequest; + let proxyhttpClient; + let initialProxyUrl; + + beforeEach(() => { + initialProxyUrl = process.env.ASK_CLI_PROXY; + stubRequest = sinon.stub(); + proxyhttpClient = proxyquire('@src/clients/http-client', { request: stubRequest }); + }); + + it('| proxyUrl is added to request options', (done) => { + // setup + process.env.ASK_CLI_PROXY = 'proxyUrl'; + stubRequest.callsFake(() => {}); + // call + proxyhttpClient.request(VALID_OPTIONS, VALID_OPERATION, true, () => {}); + // verfiy + expect(stubRequest.args[0][0]).to.have.property('proxy'); + expect(stubRequest.args[0][0].proxy).equal('proxyUrl'); + done(); + // reset + }); + + afterEach(() => { + stubRequest.reset(); + process.env.ASK_DOWNSTREAM_CLIENT = initialProxyUrl; + }); + }); + describe('# call request correctly', () => { let stubRequest; let proxyhttpClient; diff --git a/test/unit/clients/lwa-auth-code-client-test/index.js b/test/unit/clients/lwa-auth-code-client-test/index.js new file mode 100644 index 00000000..2fb2599f --- /dev/null +++ b/test/unit/clients/lwa-auth-code-client-test/index.js @@ -0,0 +1,281 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { URL } = require('url'); +const queryString = require('querystring'); + +const httpClient = require('@src/clients/http-client'); +const LWAClient = require('@src/clients/lwa-auth-code-client'); +const CONSTANTS = require('@src/utils/constants'); + +describe('# Clients test - LWA OAuth2 client test', () => { + const TEST_BASIC_CONFIGURATION = { + doDebug: false, + state: 'state', + scope: 'scope', + redirectUri: 'redirectUri' + }; + const EMPTY_CONFIG = {}; + const authorizePath = CONSTANTS.LWA.DEFAULT_AUTHORIZE_PATH; + const tokenPath = CONSTANTS.LWA.DEFAULT_TOKEN_PATH; + const authorizeHost = CONSTANTS.LWA.DEFAULT_AUTHORIZE_HOST; + const tokenHost = CONSTANTS.LWA.DEFAULT_TOKEN_HOST; + const DEFAULT_CLIENT_ID = CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_ID; + const DEFAULT_CLIENT_CONFIRMATION = CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_CONFIRMATION; + const DEFAULT_SCOPE = CONSTANTS.LWA.DEFAULT_SCOPES; + const currentDatePlusHalfAnHour = new Date(new Date(Date.now()).getTime() + (0.5 * 60 * 60 * 1000)).toISOString(); + const POST_REQUEST_METHOD = 'POST'; + const REFRESH_TOKEN_GRANT_TYPE = 'refresh_token'; + const AUTHORIZATION_CODE_GRANT_TYPE = 'authorization_code'; + const TEST_REQUEST_RESPONSE_ACCESS_TOKEN = { + statusCode: 200, + body: { token: 'BODY' }, + headers: {} + }; + const VALID_AUTH_CODE = 'authCode'; + + const VALID_ACCESS_TOKEN = { + access_token: 'accessToken', + refresh_token: 'refreshToken', + token_type: 'bearer', + expires_in: 3600, + expires_at: currentDatePlusHalfAnHour + }; + const INVALID_ACCESS_TOKEN = { + expires_at: new Date('05 October 2011 14:48 UTC').toISOString() + }; + + describe('# inspect correctness for constructor', () => { + it('| initiate as a LWAClient class', () => { + // call + const lwaClient = new LWAClient(TEST_BASIC_CONFIGURATION); + // verify + expect(lwaClient).to.be.instanceOf(LWAClient); + expect(lwaClient.config.doDebug).equal(false); + expect(lwaClient.config.authorizePath).equal(authorizePath); + expect(lwaClient.config.tokenPath).equal(tokenPath); + }); + }); + + describe('# test generateAuthorizeUrl', () => { + beforeEach(() => { + sinon.useFakeTimers(Date.UTC(2016, 2, 15)); + }); + + it('| test proper authorization code uri generation', () => { + // setup + const CONFIG = { + doDebug: false, + scope: 'scope', + redirectUri: 'redirectUri' + }; + const queryParamsUri = `response_type=code&client_id=${DEFAULT_CLIENT_ID}&state=1458000000000&scope=scope&redirect_uri=redirectUri`; + + const uri = `${authorizeHost}${authorizePath}?${queryParamsUri}`; + const lwaClient = new LWAClient(CONFIG); + // call & verify + expect(lwaClient.generateAuthorizeUrl()).equal(uri); + }); + + it('| test authorization code uri generation with blank scope and redirectUri', () => { + // setup + sinon.stub(Date, 'now'); + Date.now.returns('date'); + const CONFIG = { + doDebug: false, + scope: '', + redirectUri: '', + state: '' + }; + const outQueryParams = { + response_type: 'code', + client_id: DEFAULT_CLIENT_ID, + state: 'date', + scope: DEFAULT_SCOPE + }; + + const uri = `${authorizeHost}${authorizePath}?${queryString.stringify(outQueryParams)}`; + const lwaClient = new LWAClient(CONFIG); + // call & verify + expect(lwaClient.generateAuthorizeUrl()).equal(uri); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test isValidToken', () => { + it('| access token is valid', () => { + // setup + const lwaClient = new LWAClient(EMPTY_CONFIG); + // call & verify + expect(lwaClient.isValidToken(VALID_ACCESS_TOKEN)).equal(true); + }); + + it('| access token is invalid', () => { + // setup + const lwaClient = new LWAClient(EMPTY_CONFIG); + // call & verify + expect(lwaClient.isValidToken(INVALID_ACCESS_TOKEN)).equal(false); + }); + }); + + describe('# test refreshToken', () => { + beforeEach(() => { + sinon.stub(httpClient, 'request'); + }); + + it('| refreshing access token successful', (done) => { + // setup + const lwaClient = new LWAClient(TEST_BASIC_CONFIGURATION); + httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE_ACCESS_TOKEN); + // call + lwaClient.refreshToken(VALID_ACCESS_TOKEN, (err, res) => { + const expectedOptions = { + url: `${new URL(tokenPath, tokenHost)}`, + method: POST_REQUEST_METHOD, + body: { + grant_type: REFRESH_TOKEN_GRANT_TYPE, + refresh_token: 'refreshToken', + client_id: DEFAULT_CLIENT_ID, + client_secret: DEFAULT_CLIENT_CONFIRMATION + }, + json: true + }; + // verify + expect(httpClient.request.args[0][0]).deep.equal(expectedOptions); + expect(httpClient.request.args[0][2]).equal(false); + expect(err).equal(null); + expect(res).deep.equal({ token: 'BODY' }); + done(); + }); + }); + + it('| Failure while refreshing access token', (done) => { + // setup + const TEST_ERROR = 'error'; + const lwaClient = new LWAClient(TEST_BASIC_CONFIGURATION); + httpClient.request.callsArgWith(3, TEST_ERROR); + // call + lwaClient.refreshToken(VALID_ACCESS_TOKEN, (err, res) => { + // verify + expect(err).equal(TEST_ERROR); + expect(res).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test getAccessTokenUsingAuthCode', () => { + beforeEach(() => { + sinon.stub(httpClient, 'request'); + }); + + it('| fetch access token successful using auth code', (done) => { + // setup + const lwaClient = new LWAClient(TEST_BASIC_CONFIGURATION); + httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE_ACCESS_TOKEN); + // call + lwaClient.getAccessTokenUsingAuthCode(VALID_AUTH_CODE, (err, res) => { + const expectedOptions = { + url: `${new URL(tokenPath, tokenHost)}`, + method: POST_REQUEST_METHOD, + body: { + code: 'authCode', + grant_type: AUTHORIZATION_CODE_GRANT_TYPE, + redirect_uri: 'redirectUri', + client_id: DEFAULT_CLIENT_ID, + client_secret: DEFAULT_CLIENT_CONFIRMATION + }, + json: true + }; + // verify + expect(httpClient.request.args[0][0]).deep.equal(expectedOptions); + expect(httpClient.request.args[0][2]).equal(false); + expect(err).equal(null); + expect(res).deep.equal({ token: 'BODY' }); + done(); + }); + }); + + it('| Failure while fetching access token using auth code', (done) => { + // setup + const TEST_ERROR = 'error'; + const lwaClient = new LWAClient(TEST_BASIC_CONFIGURATION); + httpClient.request.callsArgWith(3, TEST_ERROR); + // call + lwaClient.getAccessTokenUsingAuthCode(VALID_AUTH_CODE, (err, res) => { + // verify + expect(err).equal(TEST_ERROR); + expect(res).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test _handleDefaultLwaAuthCodeConfiguration', () => { + let initialClientId; + let initialClientConfirmation; + + beforeEach(() => { + initialClientId = process.env.ASK_LWA_CLIENT_ID; + initialClientConfirmation = process.env.ASK_LWA_CLIENT_CONFIRMATION; + }); + + it('| test default value setting in case of missing config values', () => { + // call + const lwaClient = new LWAClient(EMPTY_CONFIG); + // verify + expect(lwaClient.config.clientId).equal(DEFAULT_CLIENT_ID); + expect(lwaClient.config.clientConfirmation).equal(DEFAULT_CLIENT_CONFIRMATION); + expect(lwaClient.config.authorizeHost).equal(authorizeHost); + expect(lwaClient.config.tokenHost).equal(tokenHost); + }); + + it('| test default value setting in case of non-empty config values', () => { + // setup + const NON_EMPTY_CONFIG = { + clientId: 'clientId', + clientConfirmation: 'clientConfirmation', + scope: 'scope', + state: 'state' + }; + // call + const lwaClient = new LWAClient(NON_EMPTY_CONFIG); + // verify + expect(lwaClient.config.clientId).equal('clientId'); + expect(lwaClient.config.clientConfirmation).equal('clientConfirmation'); + expect(lwaClient.config.scope).equal('scope'); + expect(lwaClient.config.state).equal('state'); + }); + + it('| test default value setting in case of invalid config values', () => { + // setup + const INVALID_CONFIG = { + clientId: ' ', + clientConfirmation: ' ' + }; + process.env.ASK_LWA_CLIENT_ID = 'envClientId'; + process.env.ASK_LWA_CLIENT_CONFIRMATION = 'envClientConfirmation'; + // call + const lwaClient = new LWAClient(INVALID_CONFIG); + // verify + expect(lwaClient.config.clientId).equal(process.env.ASK_LWA_CLIENT_ID); + expect(lwaClient.config.clientConfirmation).equal(process.env.ASK_LWA_CLIENT_CONFIRMATION); + }); + + afterEach(() => { + process.env.ASK_LWA_CLIENT_ID = initialClientId; + process.env.ASK_LWA_CLIENT_CONFIRMATION = initialClientConfirmation; + sinon.restore(); + }); + }); +}); diff --git a/test/unit/clients/smapi-client-test/index.js b/test/unit/clients/smapi-client-test/index.js index 0be585ef..ead2a651 100644 --- a/test/unit/clients/smapi-client-test/index.js +++ b/test/unit/clients/smapi-client-test/index.js @@ -2,8 +2,8 @@ const { expect } = require('chai'); const sinon = require('sinon'); const SmapiClient = require('@src/clients/smapi-client'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const triggerAccountLinking = require('./resources/account-linking'); @@ -26,6 +26,13 @@ const triggerPublishingTests = require('./resources/publishing'); describe('Clients test - smapi client test', () => { const TEST_PROFILE = 'testProfile'; const TEST_DO_DEBUG = false; + const TEST_ACCESS_TOKEN = { + access_token: 'access_token', + refresh_token: 'refresh_token', + token_type: 'bearer', + expires_in: 3600, + expires_at: 'expires_at' + }; const smapiClient = new SmapiClient({ profile: TEST_PROFILE, doDebug: TEST_DO_DEBUG @@ -53,13 +60,13 @@ describe('Clients test - smapi client test', () => { const TEST_ERROR = 'error'; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); sinon.stub(httpClient, 'request'); }); it('| input request options correctly to _smapiRequest', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE); // call smapiClient._smapiRequest(TEST_API_NAME, TEST_METHOD, TEST_VERSION, TEST_URL_PATH, {}, {}, null, (err, res) => { @@ -67,12 +74,12 @@ describe('Clients test - smapi client test', () => { const expectedOptions = { url: `${CONSTANTS.SMAPI.ENDPOINT}/${TEST_VERSION}/${TEST_URL_PATH}`, method: TEST_METHOD, - headers: {}, + headers: { authorization: 'access_token' }, body: null, json: false }; - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); - expect(oauthWrapper.tokenRefreshAndRead.args[0][1]).equal(TEST_PROFILE); + + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); expect(httpClient.request.args[0][0]).deep.equal(expectedOptions); expect(httpClient.request.args[0][1]).equal(TEST_API_NAME); expect(httpClient.request.args[0][2]).equal(false); @@ -88,7 +95,7 @@ describe('Clients test - smapi client test', () => { it('| input request options without headers input to _smapiRequest', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE); // call smapiClient._smapiRequest(TEST_API_NAME, TEST_METHOD, TEST_VERSION, TEST_URL_PATH, {}, null, null, (err, res) => { @@ -96,12 +103,11 @@ describe('Clients test - smapi client test', () => { const expectedOptions = { url: `${CONSTANTS.SMAPI.ENDPOINT}/${TEST_VERSION}/${TEST_URL_PATH}`, method: TEST_METHOD, - headers: {}, + headers: { authorization: 'access_token' }, body: null, json: false }; - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); - expect(oauthWrapper.tokenRefreshAndRead.args[0][1]).equal(TEST_PROFILE); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).deep.equal(TEST_PROFILE); expect(httpClient.request.args[0][0]).deep.equal(expectedOptions); expect(httpClient.request.args[0][1]).equal(TEST_API_NAME); expect(httpClient.request.args[0][2]).equal(false); @@ -117,7 +123,7 @@ describe('Clients test - smapi client test', () => { it('| input request options but http request fails', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, TEST_ERROR); // call smapiClient._smapiRequest(TEST_API_NAME, TEST_METHOD, TEST_VERSION, TEST_URL_PATH, {}, {}, null, (err, res) => { @@ -130,7 +136,7 @@ describe('Clients test - smapi client test', () => { it('| input request options but the response is not parsable', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE_NOT_PARSABLE); // call smapiClient._smapiRequest(TEST_API_NAME, TEST_METHOD, TEST_VERSION, TEST_URL_PATH, {}, {}, null, (err, res) => { @@ -144,7 +150,7 @@ describe('Clients test - smapi client test', () => { it('| input request options and the SMAPI returns error status code but without response object', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE_ERROR_STATUS_CODE_WITHOUT_BODY); // call smapiClient._smapiRequest(TEST_API_NAME, TEST_METHOD, TEST_VERSION, TEST_URL_PATH, {}, {}, null, (err, res) => { @@ -159,7 +165,7 @@ No response body from the service request.`; afterEach(() => { httpClient.request.restore(); - oauthWrapper.tokenRefreshAndRead.restore(); + AuthorizationController.prototype.tokenRefreshAndRead.restore(); }); }); @@ -168,12 +174,12 @@ No response body from the service request.`; beforeEach(() => { sinon.stub(httpClient, 'request'); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); it('| pass the resquest option and make http client request correctly', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE); // call smapiClient.smapiRedirectRequestWithUrl(TEST_URL, (err, res) => { @@ -181,11 +187,10 @@ No response body from the service request.`; const expectedRequestOption = { url: TEST_URL, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { authorization: 'access_token' }, body: null }; - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedRequestOption); - expect(oauthWrapper.tokenRefreshAndRead.args[0][1]).equal(TEST_PROFILE); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).deep.equal(TEST_PROFILE); expect(httpClient.request.args[0][0]).deep.equal(expectedRequestOption); expect(httpClient.request.args[0][1]).equal('REDIRECT_URL'); expect(httpClient.request.args[0][2]).equal(false); @@ -201,7 +206,7 @@ No response body from the service request.`; it('| pass the resquest option correctly but SMAPI response error status code without response body', (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsArgWith(2); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN.access_token); httpClient.request.callsArgWith(3, null, TEST_REQUEST_RESPONSE_ERROR_STATUS_CODE_WITHOUT_BODY); // call smapiClient.smapiRedirectRequestWithUrl(TEST_URL, (err, res) => { diff --git a/test/unit/clients/smapi-client-test/resources/account-linking.js b/test/unit/clients/smapi-client-test/resources/account-linking.js index 353c071d..fd2fdff0 100644 --- a/test/unit/clients/smapi-client-test/resources/account-linking.js +++ b/test/unit/clients/smapi-client-test/resources/account-linking.js @@ -1,22 +1,25 @@ -'use strict'; - -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill accountLinking APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; const TEST_SKILL_STAGE = 'skillStage'; const TEST_ACCOUNT_LINKING_INFO = 'accountLinkingInfo'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -26,7 +29,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/accountLinkingClient`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -38,7 +43,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/accountLinkingClient`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { accountLinkingRequest: TEST_ACCOUNT_LINKING_INFO }, json: true } @@ -50,26 +57,29 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/accountLinkingClient`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } } ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, (done) => { + it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/alexa-hosted.js b/test/unit/clients/smapi-client-test/resources/alexa-hosted.js index 185e6381..fce45610 100644 --- a/test/unit/clients/smapi-client-test/resources/alexa-hosted.js +++ b/test/unit/clients/smapi-client-test/resources/alexa-hosted.js @@ -1,8 +1,9 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const noop = () => {}; @@ -12,8 +13,13 @@ module.exports = (smapiClient) => { const TEST_REPO_URL = 'RepoUrl'; const TEST_VENDOR_ID = 'vendorId'; const TEST_PERMISSION_TYPE = 'permissionType'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; + let httpClientStub; + beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); [ @@ -24,7 +30,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/alexaHosted`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -36,7 +44,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/alexaHosted/repository/credentials/generate`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { repository: { type: 'GIT', @@ -53,7 +63,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/vendors/${TEST_VENDOR_ID}/alexaHosted/permissions/${TEST_PERMISSION_TYPE}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -61,18 +73,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/beta-test.js b/test/unit/clients/smapi-client-test/resources/beta-test.js index d24807a4..91098ded 100644 --- a/test/unit/clients/smapi-client-test/resources/beta-test.js +++ b/test/unit/clients/smapi-client-test/resources/beta-test.js @@ -1,7 +1,8 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const MAX_RESULTS = CONSTANTS.DEFAULT_LIST_MAX_RESULT; @@ -10,13 +11,17 @@ const noop = () => {}; module.exports = (smapiClient) => { describe('# Beta Test APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; const TEST_FEEDBACK_EMAIL = 'feedback@amazon.com'; const TESTERS = [{ emailId: 'test@amazon.com' }]; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -26,7 +31,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { feedbackEmail: TEST_FEEDBACK_EMAIL }, @@ -40,7 +47,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { feedbackEmail: TEST_FEEDBACK_EMAIL }, @@ -54,7 +63,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: {}, json: true } @@ -66,7 +77,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/start`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: {}, json: true } @@ -78,7 +91,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/end`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: {}, json: true } @@ -90,7 +105,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/testers`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -102,7 +119,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/testers/add`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { testers: TESTERS }, json: true } @@ -114,7 +133,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/testers/remove`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { testers: TESTERS }, json: true } @@ -126,7 +147,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/testers/sendReminder`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { testers: TESTERS }, json: true } @@ -138,7 +161,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}/skills/${TEST_SKILL_ID}/betaTest/testers/requestFeedback`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { testers: TESTERS }, json: true } @@ -146,18 +171,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/catalog.js b/test/unit/clients/smapi-client-test/resources/catalog.js index 8d0ac830..78138bfe 100644 --- a/test/unit/clients/smapi-client-test/resources/catalog.js +++ b/test/unit/clients/smapi-client-test/resources/catalog.js @@ -1,23 +1,24 @@ -'use strict'; - -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# catalog CRUD related APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; const TEST_CATALOG_ID = 'catalogId'; const TEST_VENDOR_ID = 'vendorId'; - const TEST_UPLOAD_ID = 'uploadId' + const TEST_UPLOAD_ID = 'uploadId'; const TEST_NEXT_TOKEN = 'nextToken'; const TEST_MAX_RESULTS = 'maxResults'; const TEST_CATALOG_TYPE = 'type'; @@ -25,6 +26,8 @@ module.exports = (smapiClient) => { const TEST_CATALOG_TITLE = 'title'; const TEST_NUMBER_OF_PARTS = 'numberOfParts'; const TEST_PART_ETAG_LIST = ['list1', 'list2']; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -34,7 +37,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { title: TEST_CATALOG_TITLE, type: TEST_CATALOG_TYPE, @@ -51,7 +56,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -59,11 +66,13 @@ module.exports = (smapiClient) => { { testCase: 'list-catalogs', apiFunc: smapiClient.catalog.listCatalogs, - parameters: [TEST_VENDOR_ID, { nextToken:TEST_NEXT_TOKEN, maxResults: TEST_MAX_RESULTS }, noop], + parameters: [TEST_VENDOR_ID, { nextToken: TEST_NEXT_TOKEN, maxResults: TEST_MAX_RESULTS }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs?nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&vendorId=${TEST_VENDOR_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -75,19 +84,24 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs?maxResults=${TEST_MAX_RESULTS}&vendorId=${TEST_VENDOR_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } + }, { testCase: 'list-catalogs without max results', apiFunc: smapiClient.catalog.listCatalogs, - parameters: [TEST_VENDOR_ID, { nextToken:TEST_NEXT_TOKEN }, noop], + parameters: [TEST_VENDOR_ID, { nextToken: TEST_NEXT_TOKEN }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs?nextToken=${TEST_NEXT_TOKEN}&vendorId=${TEST_VENDOR_ID}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -99,19 +113,23 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs?vendorId=${TEST_VENDOR_ID}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } }, { - testCase: 'list-catalogs with null querry', + testCase: 'list-catalogs with null query', apiFunc: smapiClient.catalog.listCatalogs, parameters: [TEST_VENDOR_ID, null, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs?vendorId=${TEST_VENDOR_ID}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -123,7 +141,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { numberOfParts: TEST_NUMBER_OF_PARTS }, @@ -137,7 +157,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads/${TEST_UPLOAD_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -145,11 +167,13 @@ module.exports = (smapiClient) => { { testCase: 'list-catalog-uploads', apiFunc: smapiClient.catalog.listCatalogUploads, - parameters: [TEST_CATALOG_ID, { nextToken:TEST_NEXT_TOKEN, maxResults: TEST_MAX_RESULTS }, noop], + parameters: [TEST_CATALOG_ID, { nextToken: TEST_NEXT_TOKEN, maxResults: TEST_MAX_RESULTS }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads?nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -161,7 +185,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads?maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -169,11 +195,13 @@ module.exports = (smapiClient) => { { testCase: 'list-catalog-uploads without max results', apiFunc: smapiClient.catalog.listCatalogUploads, - parameters: [TEST_CATALOG_ID, { nextToken:TEST_NEXT_TOKEN }, noop], + parameters: [TEST_CATALOG_ID, { nextToken: TEST_NEXT_TOKEN }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads?nextToken=${TEST_NEXT_TOKEN}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -185,19 +213,23 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads?maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } }, { - testCase: 'list-catalog-uploads with null querry', + testCase: 'list-catalog-uploads with null query', apiFunc: smapiClient.catalog.listCatalogUploads, parameters: [TEST_CATALOG_ID, null, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads?maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -209,7 +241,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/skills/${TEST_SKILL_ID}/catalogs/${TEST_CATALOG_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -221,7 +255,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v0/catalogs/${TEST_CATALOG_ID}/uploads/${TEST_UPLOAD_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { partETags: TEST_PART_ETAG_LIST }, @@ -229,20 +265,21 @@ module.exports = (smapiClient) => { } }, ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, (done) => { + it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); - + afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); - }) -} + }); +}; diff --git a/test/unit/clients/smapi-client-test/resources/evaluations.js b/test/unit/clients/smapi-client-test/resources/evaluations.js index 3479a63b..ba27b51b 100644 --- a/test/unit/clients/smapi-client-test/resources/evaluations.js +++ b/test/unit/clients/smapi-client-test/resources/evaluations.js @@ -1,21 +1,26 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const httpClient = require('@src/clients/http-client'); const CONSTANTS = require('@src/utils/constants'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill evaluations APIs', () => { + let httpClientStub; const TEST_SKILL_ID = 'skillId'; const TEST_LOCALE = 'test'; const TEST_STAGE = 'live'; const TEST_UTTERANCE = 'utterance'; const TEST_MULTI_TURN_TOKEN = 'multiTurnToken'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); [ @@ -26,7 +31,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_STAGE}/interactionModel/locales/${TEST_LOCALE}/profileNlu`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { utterance: TEST_UTTERANCE, multiTurnToken: TEST_MULTI_TURN_TOKEN @@ -41,7 +48,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_STAGE}/interactionModel/locales/${TEST_LOCALE}/profileNlu`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { utterance: TEST_UTTERANCE }, @@ -51,18 +60,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); - + afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/history.js b/test/unit/clients/smapi-client-test/resources/history.js index 37103361..7fddcb6f 100644 --- a/test/unit/clients/smapi-client-test/resources/history.js +++ b/test/unit/clients/smapi-client-test/resources/history.js @@ -1,32 +1,39 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const httpClient = require('@src/clients/http-client'); const CONSTANTS = require('@src/utils/constants'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill intent request history API', () => { + let httpClientStub; const TEST_SKILL_ID = 'skillId'; const TEST_NEXT_TOKEN = 'nextToken'; const TEST_MAX_RESULTS = 'maxResults'; const TEST_SORT_DIRECTION = 'sortDirection'; const TEST_SORT_FIELD = 'sortField'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); [ { - testCase: 'intent-requests-history with null params ', + testCase: 'intent-requests-history with null params', apiFunc: smapiClient.skill.history.getIntentRequestsHistory, parameters: [TEST_SKILL_ID, {}, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/history/intentRequests`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -38,7 +45,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/history/intentRequests?maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -46,18 +55,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/interaction-model.js b/test/unit/clients/smapi-client-test/resources/interaction-model.js index d82af200..93171465 100644 --- a/test/unit/clients/smapi-client-test/resources/interaction-model.js +++ b/test/unit/clients/smapi-client-test/resources/interaction-model.js @@ -1,15 +1,18 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill interaction model APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -21,6 +24,8 @@ module.exports = (smapiClient) => { const TEST_MAX_RESULTS = 'maxResults'; const TEST_SORT_DIRECTION = 'sortDirection'; const TEST_SORT_FIELD = 'sortField'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -30,7 +35,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -42,7 +49,10 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: { 'If-Match': TEST_ETAG }, + headers: { + 'If-Match': TEST_ETAG, + authorization: TEST_ACCESS_TOKEN + }, body: { interactionModel: 'model' }, @@ -56,7 +66,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { interactionModel: 'model' }, @@ -70,7 +82,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}`, method: CONSTANTS.HTTP_REQUEST.VERB.HEAD, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -84,10 +98,12 @@ module.exports = (smapiClient) => { sortField: TEST_SORT_FIELD }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` - + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` + + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -100,10 +116,12 @@ module.exports = (smapiClient) => { sortField: TEST_SORT_FIELD }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` - + `nextToken=${TEST_NEXT_TOKEN}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` + + `nextToken=${TEST_NEXT_TOKEN}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -116,10 +134,12 @@ module.exports = (smapiClient) => { sortField: TEST_SORT_FIELD }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` - + `maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` + + `maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}&sortField=${TEST_SORT_FIELD}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -132,10 +152,12 @@ module.exports = (smapiClient) => { sortField: TEST_SORT_FIELD }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` - + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortField=${TEST_SORT_FIELD}`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` + + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortField=${TEST_SORT_FIELD}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -148,10 +170,12 @@ module.exports = (smapiClient) => { sortDirection: TEST_SORT_DIRECTION }, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` - + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions?` + + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}&sortDirection=${TEST_SORT_DIRECTION}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -162,9 +186,11 @@ module.exports = (smapiClient) => { parameters: [TEST_SKILL_ID, TEST_SKILL_STAGE, TEST_LOCALE, null, noop], expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/` - + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions`, + + `${TEST_SKILL_STAGE}/interactionModel/locales/${TEST_LOCALE}/versions`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -172,18 +198,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/isp.js b/test/unit/clients/smapi-client-test/resources/isp.js index 31cedd0b..43e14177 100644 --- a/test/unit/clients/smapi-client-test/resources/isp.js +++ b/test/unit/clients/smapi-client-test/resources/isp.js @@ -1,15 +1,18 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# smapi client isp APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -24,6 +27,8 @@ module.exports = (smapiClient) => { const TEST_ISP_STATUS = 'ispStatus'; const TEST_NEXT_TOKEN = 'nextToken'; const TEST_MAX_RESULTS = 'maxResults'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -33,7 +38,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { vendorId: TEST_VENDOR_ID, inSkillProductDefinition: TEST_ISP_DEFINITION @@ -48,7 +55,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -60,7 +69,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/summary`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -72,7 +83,10 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: { 'If-Match': TEST_ISP_ETAG }, + headers: { + 'If-Match': TEST_ISP_ETAG, + authorization: TEST_ACCESS_TOKEN + }, body: { inSkillProductDefinition: TEST_ISP_DEFINITION }, json: true } @@ -84,7 +98,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { inSkillProductDefinition: TEST_ISP_DEFINITION }, json: true } @@ -96,7 +112,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/skills/${TEST_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -109,7 +127,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_ISP_STAGE}/inSkillProducts?` + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -124,7 +144,9 @@ module.exports = (smapiClient) => { + `&productId=id1&productId=id2&vendorId=${TEST_VENDOR_ID}&referenceName=${TEST_ISP_REFERENCE_NAME}` + `&type=${TEST_ISP_TYPE}&stage=${TEST_ISP_STAGE}&status=${TEST_ISP_STATUS}&isAssociatedWithSkill=true`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -139,7 +161,9 @@ module.exports = (smapiClient) => { + `&vendorId=${TEST_VENDOR_ID}&referenceName=${TEST_ISP_REFERENCE_NAME}&type=${TEST_ISP_TYPE}` + `&stage=${TEST_ISP_STAGE}&status=${TEST_ISP_STATUS}&isAssociatedWithSkill=true`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -154,7 +178,9 @@ module.exports = (smapiClient) => { + `&referenceName=${TEST_ISP_REFERENCE_NAME}&type=${TEST_ISP_TYPE}&stage=${TEST_ISP_STAGE}` + `&status=${TEST_ISP_STATUS}&isAssociatedWithSkill=true`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -166,7 +192,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts?vendorId=${TEST_VENDOR_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -179,7 +207,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/skills?` + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -192,7 +222,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/skills?` + `maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -204,7 +236,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/skills?nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -216,7 +250,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/skills`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -228,7 +264,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/skills/${TEST_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -240,7 +278,10 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: { 'If-Match': TEST_ISP_ETAG }, + headers: { + 'If-Match': TEST_ISP_ETAG, + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -252,7 +293,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -264,7 +307,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/inSkillProducts/${TEST_ISP_ID}/stages/${TEST_ISP_STAGE}/entitlement`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -272,18 +317,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/manifest.js b/test/unit/clients/smapi-client-test/resources/manifest.js index ff156bf3..38b7bf1e 100644 --- a/test/unit/clients/smapi-client-test/resources/manifest.js +++ b/test/unit/clients/smapi-client-test/resources/manifest.js @@ -1,24 +1,26 @@ -'use strict'; - -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill manifest APIs', () => { - + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_MANIFEST = { manifest: 'manifest' }; const TEST_SKILL_ID = 'skillId'; const TEST_SKILL_STAGE = 'skillStage'; const TEST_ETAG = 'eTag'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -28,7 +30,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/manifest`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -40,7 +44,10 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/manifest`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: { "If-Match": TEST_ETAG }, + headers: { + 'If-Match': TEST_ETAG, + authorization: TEST_ACCESS_TOKEN + }, body: { manifest: 'manifest' }, json: true } @@ -52,26 +59,29 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/manifest`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { manifest: 'manifest' }, json: true } } ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, (done) => { + it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/private-skill.js b/test/unit/clients/smapi-client-test/resources/private-skill.js index cd29e2bb..93918e09 100644 --- a/test/unit/clients/smapi-client-test/resources/private-skill.js +++ b/test/unit/clients/smapi-client-test/resources/private-skill.js @@ -1,17 +1,18 @@ -'use strict'; - -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill privateSkill APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -19,6 +20,8 @@ module.exports = (smapiClient) => { const TEST_ACCOUNT_ID = 'accountId'; const TEST_NEXT_TOKEN = 'nextToken'; const TEST_MAX_RESULTS = 'maxResults'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -28,7 +31,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/privateDistributionAccounts/${TEST_ACCOUNT_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -40,7 +45,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/privateDistributionAccounts?nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -52,7 +59,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/privateDistributionAccounts?maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -64,7 +73,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/privateDistributionAccounts?nextToken=${TEST_NEXT_TOKEN}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -76,26 +87,29 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/privateDistributionAccounts/${TEST_ACCOUNT_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } } ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, (done) => { + it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/publishing.js b/test/unit/clients/smapi-client-test/resources/publishing.js index 7b50ce85..7c28c1b5 100644 --- a/test/unit/clients/smapi-client-test/resources/publishing.js +++ b/test/unit/clients/smapi-client-test/resources/publishing.js @@ -1,15 +1,18 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill publishing lifecycle related APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -19,6 +22,8 @@ module.exports = (smapiClient) => { const TEST_MAX_RESULTS = 'maxResults'; const TEST_CERTIFICATION_ID = 'certificationId'; const TEST_ACCEPT_LANGUAGE = 'acceptLanguage'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -28,7 +33,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/submit`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -40,7 +47,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/withdraw`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { reason: TEST_WITHDRAW_REASON, message: TEST_WITHDRAW_MESSAGE @@ -57,7 +66,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications?` + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -70,7 +81,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications?` + `nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -83,7 +96,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications?` + `maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -95,7 +110,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -107,7 +124,10 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications/${TEST_CERTIFICATION_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: { 'Accept-Language': TEST_ACCEPT_LANGUAGE }, + headers: { + 'Accept-Language': TEST_ACCEPT_LANGUAGE, + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -119,7 +139,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/certifications/${TEST_CERTIFICATION_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: { }, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -127,18 +149,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/skill-package.js b/test/unit/clients/smapi-client-test/resources/skill-package.js index a5f6a571..6048440e 100644 --- a/test/unit/clients/smapi-client-test/resources/skill-package.js +++ b/test/unit/clients/smapi-client-test/resources/skill-package.js @@ -1,15 +1,18 @@ -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# smapi client skill package APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -18,6 +21,8 @@ module.exports = (smapiClient) => { const TEST_LOCATION = 'packageLocation'; const TEST_IMPORT_ID = 'importId'; const TEST_EXPORT_ID = 'exportId'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -27,7 +32,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/uploads`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -39,7 +46,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/imports`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { location: TEST_LOCATION }, json: true } @@ -51,7 +60,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/imports`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { location: TEST_LOCATION, vendorId: TEST_VENDOR_ID }, json: true } @@ -63,7 +74,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/imports/${TEST_IMPORT_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -75,7 +88,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/exports`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -87,25 +102,28 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/exports/${TEST_EXPORT_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } }, ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, () => { + it(`| call ${testCase} successfully`, () => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/skill.js b/test/unit/clients/smapi-client-test/resources/skill.js index e95cd835..7fc0bd01 100644 --- a/test/unit/clients/smapi-client-test/resources/skill.js +++ b/test/unit/clients/smapi-client-test/resources/skill.js @@ -1,15 +1,20 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { + let httpClientStub; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; describe('# skill CRUD related APIs', () => { beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -27,7 +32,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { manifest: 'manifest', vendorId: TEST_VENDOR_ID @@ -42,7 +49,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -55,7 +64,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills?nextToken=${TEST_NEXT_TOKEN}&` + `maxResults=${TEST_MAX_RESULTS}&vendorId=${TEST_VENDOR_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -67,7 +78,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills?nextToken=${TEST_NEXT_TOKEN}&vendorId=${TEST_VENDOR_ID}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -79,7 +92,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills?maxResults=${TEST_MAX_RESULTS}&vendorId=${TEST_VENDOR_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -91,7 +106,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills?vendorId=${TEST_VENDOR_ID}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -103,7 +120,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/status`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -115,7 +134,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/status?resource=${CONSTANTS.SKILL.RESOURCES.MANIFEST}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -128,7 +149,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/status?resource=${CONSTANTS.SKILL.RESOURCES.MANIFEST}` + `&resource=${CONSTANTS.SKILL.RESOURCES.INTERACTION_MODEL}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -141,7 +164,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/inSkillProducts?` + `nextToken=${TEST_NEXT_TOKEN}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -154,7 +179,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/inSkillProducts?` + `maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -167,7 +194,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/inSkillProducts?` + `nextToken=${TEST_NEXT_TOKEN}&maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -179,7 +208,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/inSkillProducts?maxResults=50`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -187,24 +218,26 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); describe('# skill development lifecycle related APIs', () => { beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -230,7 +263,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/enablement`, method: CONSTANTS.HTTP_REQUEST.VERB.PUT, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -242,7 +277,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/enablement`, method: CONSTANTS.HTTP_REQUEST.VERB.DELETE, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -254,7 +291,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/enablement`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -266,7 +305,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/validations`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { locales: ['locale1', 'locale2'] }, json: true } @@ -278,7 +319,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/stages/${TEST_SKILL_STAGE}/validations/${TEST_VALIDATION_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -290,7 +333,9 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/skills/${TEST_SKILL_ID}/credentials`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -305,7 +350,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}` + `&locale=${TEST_LOCALE}&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -320,7 +367,9 @@ module.exports = (smapiClient) => { + `&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -335,7 +384,9 @@ module.exports = (smapiClient) => { + `&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -350,7 +401,9 @@ module.exports = (smapiClient) => { + `&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -365,7 +418,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -380,7 +435,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -395,7 +452,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&intent=${TEST_INTENT}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -410,7 +469,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&locale=${TEST_LOCALE}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -425,7 +486,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}` + `&maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -440,7 +503,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}` + `&locale=${TEST_LOCALE}&maxResults=${TEST_DEFAULT_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -455,7 +520,9 @@ module.exports = (smapiClient) => { + `&period=${TEST_PERIOD}&metric=${TEST_METRIC}&stage=${TEST_SKILL_STAGE}&skillType=${TEST_SKILL_TYPE}&intent=${TEST_INTENT}` + `&locale=${TEST_LOCALE}&maxResults=${TEST_MAX_RESULTS}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -463,18 +530,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/task.js b/test/unit/clients/smapi-client-test/resources/task.js index 402beb51..da12d5c8 100644 --- a/test/unit/clients/smapi-client-test/resources/task.js +++ b/test/unit/clients/smapi-client-test/resources/task.js @@ -1,14 +1,18 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); + +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# task get and search APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -22,6 +26,8 @@ module.exports = (smapiClient) => { const TEST_PROVIDER_SKILL_ID = 'providerSkillId'; const TEST_MAX_RESULTS = 'maxResults'; const TEST_NEXT_TOKEN = 'nextToken'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { @@ -32,7 +38,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V1}` + `/tasks/${TEST_TASK_NAME}/versions/${TEST_TASK_VERSION}/?skillId=${TEST_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -47,7 +55,9 @@ module.exports = (smapiClient) => { + `maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}&` + `skillId=${TEST_SKILL_ID}&keywords=${TEST_SINGLE_KEYWORD}&providerSkillId=${TEST_PROVIDER_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -62,7 +72,9 @@ module.exports = (smapiClient) => { + `maxResults=${TEST_MAX_RESULTS}&nextToken=${TEST_NEXT_TOKEN}&` + `skillId=${TEST_SKILL_ID}&keywords=${TEST_MULTIPLE_KEYWORDS_QUERY_PARAM}&providerSkillId=${TEST_PROVIDER_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -78,23 +90,31 @@ module.exports = (smapiClient) => { + `skillId=${TEST_SKILL_ID}&keywords=${TEST_MULTIPLE_KEYWORDS_INCLUDING_SPACES_QUERY_PARAM}&` + `providerSkillId=${TEST_PROVIDER_SKILL_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } } ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + // setup + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); + + // call apiFunc(...parameters); - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + + // verify + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/test.js b/test/unit/clients/smapi-client-test/resources/test.js index 2bde9ed5..acd03a1b 100644 --- a/test/unit/clients/smapi-client-test/resources/test.js +++ b/test/unit/clients/smapi-client-test/resources/test.js @@ -1,15 +1,18 @@ const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { describe('# skill test related APIs', () => { + let httpClientStub; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); const TEST_SKILL_ID = 'skillId'; @@ -19,6 +22,8 @@ module.exports = (smapiClient) => { const TEST_ENDPOINT_REGION = 'endpointRegion'; const TEST_INVOKE_PAYLOAD = 'invokePayload'; const FORCE_NEW_SESSION = 'FORCE_NEW_SESSION'; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; [ { testCase: 'simulate-skill without force new session', @@ -28,7 +33,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V2}/skills/${TEST_SKILL_ID}` + `/stages/${CONSTANTS.SKILL.STAGE.DEVELOPMENT}/simulations`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { input: { content: TEST_INPUT }, device: { locale: TEST_LOCALE } @@ -44,7 +51,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V2}/skills/${TEST_SKILL_ID}` + `/stages/${CONSTANTS.SKILL.STAGE.DEVELOPMENT}/simulations`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { input: { content: TEST_INPUT }, device: { locale: TEST_LOCALE }, @@ -61,7 +70,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V2}/skills/${TEST_SKILL_ID}` + `/stages/${CONSTANTS.SKILL.STAGE.DEVELOPMENT}/invocations`, method: CONSTANTS.HTTP_REQUEST.VERB.POST, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: { endpointRegion: TEST_ENDPOINT_REGION, skillRequest: TEST_INVOKE_PAYLOAD @@ -77,7 +88,9 @@ module.exports = (smapiClient) => { url: `${CONSTANTS.SMAPI.ENDPOINT}/${CONSTANTS.SMAPI.VERSION.V2}/skills/${TEST_SKILL_ID}/stages/` + `${CONSTANTS.SKILL.STAGE.DEVELOPMENT}/simulations/${TEST_SIMULATION_ID}`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } @@ -85,18 +98,19 @@ module.exports = (smapiClient) => { ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/clients/smapi-client-test/resources/vendor.js b/test/unit/clients/smapi-client-test/resources/vendor.js index d4c83667..6ecfaeaa 100644 --- a/test/unit/clients/smapi-client-test/resources/vendor.js +++ b/test/unit/clients/smapi-client-test/resources/vendor.js @@ -1,18 +1,21 @@ -'use strict'; - -const expect = require('chai').expect; +const { expect } = require('chai'); const sinon = require('sinon'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const httpClient = require('@src/clients/http-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const noop = () => {}; module.exports = (smapiClient) => { - describe('# smapi client vendor APIs', () => { + let httpClientStub; + const TEST_PROFILE = 'testProfile'; + const TEST_ACCESS_TOKEN = 'access_token'; + beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead'); + httpClientStub = sinon.stub(httpClient, 'request').callsFake(noop); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); }); [ @@ -23,26 +26,29 @@ module.exports = (smapiClient) => { expectedOptions: { url: `${CONSTANTS.SMAPI.ENDPOINT}/v1/vendors`, method: CONSTANTS.HTTP_REQUEST.VERB.GET, - headers: {}, + headers: { + authorization: TEST_ACCESS_TOKEN + }, body: null, json: false } } ].forEach(({ testCase, apiFunc, parameters, expectedOptions }) => { - it (`| call ${testCase} successfully`, (done) => { + it(`| call ${testCase} successfully`, (done) => { // setup - oauthWrapper.tokenRefreshAndRead.callsFake(noop); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, TEST_ACCESS_TOKEN); // call apiFunc(...parameters); // verify - expect(oauthWrapper.tokenRefreshAndRead.called).equal(true); - expect(oauthWrapper.tokenRefreshAndRead.args[0][0]).deep.equal(expectedOptions); + expect(AuthorizationController.prototype.tokenRefreshAndRead.called).equal(true); + expect(AuthorizationController.prototype.tokenRefreshAndRead.args[0][0]).equal(TEST_PROFILE); + expect(httpClientStub.args[0][0]).deep.equal(expectedOptions); done(); }); }); afterEach(() => { - oauthWrapper.tokenRefreshAndRead.restore(); + sinon.restore(); }); }); }; diff --git a/test/unit/commands/abstract-command-test.js b/test/unit/commands/abstract-command-test.js index 2af6c4b9..5818c86e 100644 --- a/test/unit/commands/abstract-command-test.js +++ b/test/unit/commands/abstract-command-test.js @@ -1,10 +1,16 @@ const { expect } = require('chai'); const sinon = require('sinon'); const commander = require('commander'); +const path = require('path'); + const { AbstractCommand } = require('@src/commands/abstract-command'); +const AppConfig = require('@src/model/app-config'); describe('Command test - AbstractCommand class', () => { + const FIXTURE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model'); + const APP_CONFIG_NO_PROFILES_PATH = path.join(FIXTURE_PATH, 'app-config-no-profiles.json'); + describe('# Command class constructor', () => { const mockOptionModel = { 'foo-option': { @@ -33,6 +39,10 @@ describe('Command test - AbstractCommand class', () => { } }; + beforeEach(() => { + sinon.stub(path, 'join').returns(APP_CONFIG_NO_PROFILES_PATH); + }); + it('| should be able to register command', (done) => { class MockCommand extends AbstractCommand { constructor(optionModel, handle) { @@ -213,6 +223,7 @@ describe('Command test - AbstractCommand class', () => { afterEach(() => { sinon.restore(); + AppConfig.dispose(); }); }); @@ -270,4 +281,98 @@ describe('Command test - AbstractCommand class', () => { expect(AbstractCommand.parseOptionKey('skill')).eq('skill'); }); }); + + describe('# check AppConfig object ', () => { + const mockOptionModel = { + 'foo-option': { + name: 'foo-option', + description: 'foo option', + alias: 'f', + stringInput: 'REQUIRED' + }, + 'bar-option': { + name: 'bar-option', + description: 'bar option', + alias: 'b', + stringInput: 'REQUIRED' + }, + 'another-bar-option': { + name: 'another-bar-option', + description: 'another bar option', + alias: 'a', + stringInput: 'OPTIONAL' + }, + 'baz-option': { + name: 'baz-option', + description: 'baz option', + alias: 'z', + stringInput: 'NONE' + } + }; + + beforeEach(() => { + sinon.stub(path, 'join').returns(APP_CONFIG_NO_PROFILES_PATH); + }); + + it('| should not be null for other commands', (done) => { + class NonConfigureCommand extends AbstractCommand { + constructor(optionModel, handle) { + super(optionModel); + this.handle = handle; + } + + name() { + return 'random'; + } + + description() { + return 'random description'; + } + } + + const mockCommand = new NonConfigureCommand(mockOptionModel, (options) => { + expect(options._name).eq('random'); + expect(options._description).eq('random description'); + expect(options.options).deep.eq([]); + expect(AppConfig.getInstance().getProfilesList().length).eq(0); + done(); + }); + + mockCommand.createCommand()(commander); + commander.parse(['node', 'mock', 'random']); + }); + + it('| should be null for configure command', (done) => { + class ConfigureCommand extends AbstractCommand { + constructor(optionModel, handle) { + super(optionModel); + this.handle = handle; + } + + name() { + return 'configure'; + } + + description() { + return 'configure description'; + } + } + + const mockCommand = new ConfigureCommand(mockOptionModel, (options) => { + expect(options._name).eq('configure'); + expect(options._description).eq('configure description'); + expect(options.options).deep.eq([]); + expect(AppConfig.getInstance()).eq(null); + done(); + }); + + mockCommand.createCommand()(commander); + commander.parse(['node', 'mock', 'configure']); + }); + + afterEach(() => { + sinon.restore(); + AppConfig.dispose(); + }); + }); }); diff --git a/test/unit/commands/api/skill-package/export-package/helper-test.js b/test/unit/commands/api/skill-package/export-package/helper-test.js index 7f3b3723..5badeec1 100644 --- a/test/unit/commands/api/skill-package/export-package/helper-test.js +++ b/test/unit/commands/api/skill-package/export-package/helper-test.js @@ -1,10 +1,10 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const helper = require('@src/commands/api/skill-package/export-package/helper'); const httpClient = require('@src/clients/http-client'); const jsonView = require('@src/view/json-view'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const SmapiClient = require('@src/clients/smapi-client/index.js'); describe('Commands export-package - helper test', () => { @@ -30,7 +30,7 @@ describe('Commands export-package - helper test', () => { body: {} }; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { sinon.restore(); diff --git a/test/unit/commands/api/skill-package/export-package/index-test.js b/test/unit/commands/api/skill-package/export-package/index-test.js index 1f05a923..1488f6a0 100644 --- a/test/unit/commands/api/skill-package/export-package/index-test.js +++ b/test/unit/commands/api/skill-package/export-package/index-test.js @@ -2,13 +2,13 @@ const { expect } = require('chai'); const fs = require('fs'); const sinon = require('sinon'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const CONSTANTS = require('@src/utils/constants'); const ExportPackageCommand = require('@src/commands/api/skill-package/export-package'); const helper = require('@src/commands/api/skill-package/export-package/helper'); const httpClient = require('@src/clients/http-client'); const jsonView = require('@src/view/json-view'); const Messenger = require('@src/view/messenger'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); const optionModel = require('@src/commands/option-model'); const profileHelper = require('@src/utils/profile-helper'); const zipUtils = require('@src/utils/zip-utils'); @@ -99,7 +99,7 @@ describe('Commands export-package test - command class test', () => { } }; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); it('| export skill package fails, expect throw error', (done) => { @@ -149,7 +149,7 @@ describe('Commands export-package test - command class test', () => { } }; beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); it('| poll skill package fails, expect throw error', (done) => { diff --git a/test/unit/commands/configure/ask-profile-setup-helper-test.js b/test/unit/commands/configure/ask-profile-setup-helper-test.js new file mode 100644 index 00000000..82ad44cb --- /dev/null +++ b/test/unit/commands/configure/ask-profile-setup-helper-test.js @@ -0,0 +1,283 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const askProfileSetupHelper = require('@src/commands/configure/ask-profile-setup-helper'); +const messages = require('@src/commands/configure/messages'); +const ui = require('@src/commands/configure/ui'); +const AuthorizationController = require('@src/controllers/authorization-controller'); +const httpClient = require('@src/clients/http-client'); +const SmapiClient = require('@src/clients/smapi-client'); +const jsonView = require('@src/view/json-view'); +const Messenger = require('@src/view/messenger'); + +describe('Command: Configure - ASK profile setup helper test', () => { + const TEST_PROFILE = 'testProfile'; + const TEST_DO_DEBUG = false; + const TEST_NEED_BROWSER = false; + const TEST_VENDOR_ID_1 = 'testVendorId_1'; + const TEST_VENDOR_ID_2 = 'testVendorId_2'; + const TEST_ERROR_MESSAGE = 'error thrown'; + const TEST_CONFIG = { + askProfile: TEST_PROFILE, + needBrowser: TEST_NEED_BROWSER, + debug: TEST_DO_DEBUG + }; + const TEST_VENDOR_HTTP_RESPONSE_BODY = { + vendors: [ + { id: TEST_VENDOR_ID_1 } + ] + }; + + const TEST_MULTIPLE_VENDORS_HTTP_RESPONSE_BODY = { + vendors: [ + { id: TEST_VENDOR_ID_1 }, + { id: TEST_VENDOR_ID_2 } + ] + }; + + const TEST_EMPTY_VENDOR_HTTP_RESPONSE_BODY = { + vendors: [] + }; + + const TEST_ACCESS_TOKEN = { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires_in: 3600, + expires_at: 'expires_at' + }; + + const TEST_AUTHORIZE_URL = 'authorizeUrl'; + const TEST_AUTH_CODE = 'authCode'; + + describe('# test setupAskToken', () => { + let infoStub; + beforeEach(() => { + sinon.stub(AuthorizationController.prototype, 'getTokensByListeningOnPort'); + sinon.stub(AuthorizationController.prototype, 'getAccessTokenUsingAuthCode'); + sinon.stub(AuthorizationController.prototype, 'getAuthorizeUrl'); + sinon.stub(ui, 'getAuthCode'); + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + }); + + it('| test valid access token retrieval, getTokensByListeningOnPort returns valid token', (done) => { + // setup + AuthorizationController.prototype.getTokensByListeningOnPort.callsArgWith(0, null, TEST_ACCESS_TOKEN); + + // call + askProfileSetupHelper.setupAskToken({ needBrowser: true, askProfile: TEST_PROFILE }, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_ACCESS_TOKEN); + done(); + }); + }); + + it('| test valid access token retrieval, getTokensByListeningOnPort returns error', (done) => { + // setup + AuthorizationController.prototype.getTokensByListeningOnPort.callsArgWith(0, TEST_ERROR_MESSAGE); + + // call + askProfileSetupHelper.setupAskToken({ needBrowser: true, askProfile: TEST_PROFILE }, (error, accessToken) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(accessToken).eq(undefined); + done(); + }); + }); + + it('| test valid access token retrieval, authcode retrieval returns error', (done) => { + // setup + AuthorizationController.prototype.getAuthorizeUrl.returns(TEST_AUTHORIZE_URL); + ui.getAuthCode.callsArgWith(0, TEST_ERROR_MESSAGE); + + // call + askProfileSetupHelper.setupAskToken(TEST_CONFIG, (error, accessToken) => { + // verify + expect(infoStub.args[0][0]).eq(`Paste the following url to your browser:\n ${TEST_AUTHORIZE_URL}`); + expect(error).eq(TEST_ERROR_MESSAGE); + expect(accessToken).eq(undefined); + done(); + }); + }); + + it('| test valid access token retrieval, getAccessTokenUsingAuthCode returns valid token', (done) => { + // setup + AuthorizationController.prototype.getAccessTokenUsingAuthCode.callsArgWith(1, null, TEST_ACCESS_TOKEN); + AuthorizationController.prototype.getAuthorizeUrl.returns('authorizeUrl'); + ui.getAuthCode.callsArgWith(0, null, TEST_AUTH_CODE); + + // call + askProfileSetupHelper.setupAskToken(TEST_CONFIG, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_ACCESS_TOKEN); + expect(infoStub.args[0][0]).eq(`Paste the following url to your browser:\n ${TEST_AUTHORIZE_URL}`); + done(); + }); + }); + + it('| test valid access token retrieval, getAccessTokenUsingAuthCode throws error', (done) => { + // setup + AuthorizationController.prototype.getAccessTokenUsingAuthCode.callsArgWith(1, TEST_ERROR_MESSAGE); + AuthorizationController.prototype.getAuthorizeUrl.returns('authorizeUrl'); + ui.getAuthCode.callsArgWith(0, null, TEST_AUTH_CODE); + + // call + askProfileSetupHelper.setupAskToken(TEST_CONFIG, (error, accessToken) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(accessToken).eq(undefined); + expect(infoStub.args[0][0]).eq(`Paste the following url to your browser:\n ${TEST_AUTHORIZE_URL}`); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test setupVendorId', () => { + const smapiClient = new SmapiClient({ + profile: TEST_PROFILE, + doDebug: TEST_DO_DEBUG + }); + const EMPTY_RESPONSE_BODY = '{}'; + + smapiClient.testFunc = () => {}; + let stubTestFunc; + + describe('# test _getVendorInfo', () => { + beforeEach(() => { + sinon.stub(httpClient, 'request'); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); + stubTestFunc = sinon.stub(smapiClient, 'testFunc'); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, 'refresh_token'); + }); + + it('| returns valid vendorID, one vendorID present', (done) => { + // setup + httpClient.request.callsArgWith(3, null, { statusCode: 200, body: TEST_VENDOR_HTTP_RESPONSE_BODY }); + stubTestFunc.callsArgWith(0, null, { statusCode: 200, body: TEST_VENDOR_HTTP_RESPONSE_BODY }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(null); + expect(vendorId).eq(TEST_VENDOR_ID_1); + done(); + }); + }); + + it('| throws error: smapi client returns error', (done) => { + // setup + httpClient.request.callsArgWith(3, TEST_ERROR_MESSAGE, {}); + stubTestFunc.callsArgWith(0, TEST_ERROR_MESSAGE, {}); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(vendorId).eq(undefined); + done(); + }); + }); + + it('| throws error: status code greater than or equal to 300', (done) => { + // setup + sinon.stub(jsonView, 'toString'); + jsonView.toString.withArgs(sinon.match.any).returns(TEST_ERROR_MESSAGE); + httpClient.request.callsArgWith(3, null, { statusCode: 300, body: EMPTY_RESPONSE_BODY }); + stubTestFunc.callsArgWith(0, null, { statusCode: 300, body: EMPTY_RESPONSE_BODY }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(vendorId).eq(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test _selectVendorId', () => { + beforeEach(() => { + sinon.stub(httpClient, 'request'); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead'); + stubTestFunc = sinon.stub(smapiClient, 'testFunc'); + AuthorizationController.prototype.tokenRefreshAndRead.callsArgWith(1, null, 'refresh_token'); + }); + + it('| throws error: no vendorInfo present', (done) => { + // setup + httpClient.request.callsArgWith(3, null, { body: {} }); + stubTestFunc.callsArgWith(0, TEST_ERROR_MESSAGE, { body: {} }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(messages.VENDOR_INFO_FETCH_ERROR); + expect(vendorId).eq(undefined); + done(); + }); + }); + + it('| throws error: no vendorIDs present', (done) => { + // setup + httpClient.request.callsArgWith(3, null, { body: TEST_EMPTY_VENDOR_HTTP_RESPONSE_BODY }); + stubTestFunc.callsArgWith(0, TEST_ERROR_MESSAGE, { body: TEST_EMPTY_VENDOR_HTTP_RESPONSE_BODY }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(messages.VENDOR_ID_CREATE_INSTRUCTIONS); + expect(vendorId).eq(undefined); + done(); + }); + }); + + it('| returns valid vendorID: multiple vendorIDs present', (done) => { + // setup + sinon.stub(ui, 'chooseVendorId'); + ui.chooseVendorId.callsArgWith(2, null, TEST_VENDOR_ID_2); + httpClient.request.callsArgWith(3, null, { body: TEST_MULTIPLE_VENDORS_HTTP_RESPONSE_BODY }); + stubTestFunc.callsArgWith(0, TEST_ERROR_MESSAGE, { body: TEST_MULTIPLE_VENDORS_HTTP_RESPONSE_BODY }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(null); + expect(vendorId).eq(TEST_VENDOR_ID_2); + done(); + }); + }); + + it('| throws error: vendorId selection component throws error', (done) => { + // setup + sinon.stub(ui, 'chooseVendorId'); + ui.chooseVendorId.callsArgWith(2, TEST_ERROR_MESSAGE, null); + httpClient.request.callsArgWith(3, null, { body: TEST_MULTIPLE_VENDORS_HTTP_RESPONSE_BODY }); + stubTestFunc.callsArgWith(0, TEST_ERROR_MESSAGE, { body: TEST_MULTIPLE_VENDORS_HTTP_RESPONSE_BODY }); + + // call + askProfileSetupHelper.setupVendorId(TEST_CONFIG, (err, vendorId) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(vendorId).eq(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + }); +}); diff --git a/test/unit/commands/configure/aws-profile-setup-helper-test.js b/test/unit/commands/configure/aws-profile-setup-helper-test.js new file mode 100644 index 00000000..d0c1782a --- /dev/null +++ b/test/unit/commands/configure/aws-profile-setup-helper-test.js @@ -0,0 +1,314 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const awsProfileHandler = require('aws-profile-handler'); +const fs = require('fs-extra'); +const path = require('path'); +const querystring = require('querystring'); +const proxyquire = require('proxyquire'); + +const awsProfileSetupHelper = require('@src/commands/configure/aws-profile-setup-helper'); +const messages = require('@src/commands/configure/messages'); +const ui = require('@src/commands/configure/ui'); +const CONSTANTS = require('@src/utils/constants'); +const profileHelper = require('@src/utils/profile-helper'); +const stringUtils = require('@src/utils/string-utils'); +const Messenger = require('@src/view/messenger'); + +describe('Command: Configure - AWS profile setup helper test', () => { + const TEST_PROFILE = 'testAwsProfile'; + const TEST_DO_DEBUG = false; + const TEST_NEED_BROWSER = false; + const TEST_CONFIG_PATH = '~/.aws/credentials'; + const TEST_CONFIG = { + askProfile: TEST_PROFILE, + needBrowser: TEST_NEED_BROWSER, + debug: TEST_DO_DEBUG + }; + const TEST_ERROR_MESSAGE = 'errorThrown'; + const TEST_NEW_AWS_PROFILE_NAME = 'newProfileName'; + const TEST_PARAMS = { + accessKey: true, + step: 'review', + userNames: `ask-cli-${TEST_NEW_AWS_PROFILE_NAME}`, + permissionType: 'policies', + policies: [ + CONSTANTS.AWS.IAM.USER.POLICY_ARN.IAM_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.CFN_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.S3_FULL, + CONSTANTS.AWS.IAM.USER.POLICY_ARN.LAMBDA_FULL + ] + }; + const TEST_CREDENTIALS = { aws_access_key_id: 'accessKeyId', aws_secret_access_key: 'secretAccessKey' }; + + describe('# test setupAwsProfile', () => { + beforeEach(() => { + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(path, 'join').returns(TEST_CONFIG_PATH); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| returns error, awsProfileHandler listProfiles function throws error, expect error called back ', (done) => { + // setup + sinon.stub(awsProfileHandler, 'listProfiles').throws(new Error(TEST_ERROR_MESSAGE)); + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(err.message).eq(TEST_ERROR_MESSAGE); + expect(awsProfile).eq(undefined); + done(); + }); + }); + + describe('# test _initiateAwsProfileSetup', () => { + it('| returns error, ui confirmSettingAws fails', (done) => { + // setup + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, TEST_ERROR_MESSAGE); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(awsProfile).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns error due to invalid setup choice', (done) => { + // setup + const infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, null); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.SKIP_AWS_CONFIGURATION); + expect(err).eq(undefined); + expect(awsProfile).eq(undefined); + done(); + }); + }); + + describe('# test _handleEnvironmentVariableAwsSetup', () => { + it('| returns error, ui selectEnvironmentVariables fails', (done) => { + // setup + sinon.stub(stringUtils, 'isNonBlankString').returns(true); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, TEST_ERROR_MESSAGE); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(awsProfile).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns error, setup choice is Yes', (done) => { + // setup + sinon.stub(stringUtils, 'isNonBlankString').returns(true); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, null, 'Yes'); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + sinon.stub(profileHelper, 'setupProfile'); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(profileHelper.setupProfile.args[0][0]).eq(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); + expect(profileHelper.setupProfile.args[0][1]).eq(TEST_PROFILE); + expect(err).eq(null); + expect(awsProfile).eq(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); + done(); + }); + }); + + it('| returns error, ui createNewOrSelectAWSProfile fails ', (done) => { + // setup + sinon.stub(stringUtils, 'isNonBlankString').returns(false); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, TEST_ERROR_MESSAGE); + sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, TEST_ERROR_MESSAGE); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + expect(awsProfile).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns awsProfile, user chooses existing profile ', (done) => { + // setup + sinon.stub(stringUtils, 'isNonBlankString').returns(false); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, TEST_ERROR_MESSAGE); + sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, null, 'existing_profile'); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + sinon.stub(profileHelper, 'setupProfile'); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(profileHelper.setupProfile.args[0][0]).eq('existing_profile'); + expect(profileHelper.setupProfile.args[0][1]).eq(TEST_PROFILE); + expect(err).eq(null); + expect(awsProfile).eq('existing_profile'); + done(); + }); + }); + }); + }); + }); + + describe('# test _createAwsProfileFlow', () => { + afterEach(() => { + sinon.restore(); + }); + + it('| fs ensureFileSync throws error', (done) => { + // setup + sinon.stub(path, 'join').returns(TEST_CONFIG_PATH); + sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(fs, 'ensureFileSync').throws(new Error(TEST_ERROR_MESSAGE)); + sinon.stub(stringUtils, 'isNonBlankString').returns(false); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err) => { + // verify + expect(err.message).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns valid awsProfile ', (done) => { + // setup + const DEFAULT_AWS_PROFILE = 'ask_cli_default'; + const opnStub = sinon.stub(); + const proxyHelper = proxyquire('@src/commands/configure/aws-profile-setup-helper', { + opn: opnStub + }); + const infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + sinon.stub(fs, 'existsSync').returns(false); + sinon.stub(fs, 'ensureFileSync'); + sinon.stub(stringUtils, 'isNonBlankString').returns(false); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'addNewCredentials').callsArgWith(0, null, TEST_CREDENTIALS); + sinon.stub(awsProfileHandler, 'addProfile'); + sinon.stub(profileHelper, 'setupProfile'); + + // call + proxyHelper.setupAwsProfile({ askProfile: TEST_PROFILE, needBrowser: true }, (err, awsProfile) => { + // verify + expect(awsProfileHandler.addProfile.args[0][0]).eq(DEFAULT_AWS_PROFILE); + expect(awsProfileHandler.addProfile.args[0][1]).to.deep.eq(TEST_CREDENTIALS); + expect(profileHelper.setupProfile.args[0][0]).eq(DEFAULT_AWS_PROFILE); + expect(profileHelper.setupProfile.args[0][1]).eq(TEST_PROFILE); + expect(infoStub.args[0][0]).eq(messages.AWS_CREATE_PROFILE_TITLE); + expect(infoStub.args[1][0]).eq(`\nAWS profile "${DEFAULT_AWS_PROFILE}" was successfully created. The details are recorded in aws credentials file (.aws/credentials) located at your **HOME** folder.`); + expect(err).eq(null); + expect(awsProfile).eq(DEFAULT_AWS_PROFILE); + done(); + }); + }); + + it('| returns error, ui requestAwsProfileName fails ', (done) => { + // setup + sinon.stub(path, 'join').returns(TEST_CONFIG_PATH); + sinon.stub(stringUtils, 'isNonBlankString').returns(true); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, null, 'No'); + sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, null, 'Create new profile'); + sinon.stub(ui, 'requestAwsProfileName').callsArgWith(1, TEST_ERROR_MESSAGE); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + sinon.stub(fs, 'ensureFileSync'); + sinon.stub(fs, 'existsSync').returns(true); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err) => { + // verify + expect(err).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns error, ui addNewCredentials fails ', (done) => { + // setup + sinon.stub(path, 'join').returns(TEST_CONFIG_PATH); + const infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + sinon.stub(stringUtils, 'isNonBlankString').returns(true); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, null, 'No'); + sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, null, 'Create new profile'); + sinon.stub(ui, 'requestAwsProfileName').callsArgWith(1, null, TEST_NEW_AWS_PROFILE_NAME); + sinon.stub(ui, 'addNewCredentials').callsArgWith(0, TEST_ERROR_MESSAGE); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + sinon.stub(fs, 'ensureFileSync'); + sinon.stub(fs, 'existsSync').returns(true); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err) => { + // verify + expect(infoStub.args[0][0]).eq(messages.AWS_CREATE_PROFILE_TITLE); + expect(infoStub.args[1][0]).eq(messages.AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER); + expect(infoStub.args[2][0]).eq(` ${CONSTANTS.AWS.IAM.USER.NEW_USER_BASE_URL}${querystring.stringify(TEST_PARAMS)}`); + done(); + }); + }); + + it('| successfully added awsProfile and credentials ', (done) => { + // setup + sinon.stub(path, 'join').returns(TEST_CONFIG_PATH); + const infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + sinon.stub(stringUtils, 'isNonBlankString').returns(true); + sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, null, true); + sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, null, 'No'); + sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, null, 'Create new profile'); + sinon.stub(ui, 'requestAwsProfileName').callsArgWith(1, null, TEST_NEW_AWS_PROFILE_NAME); + sinon.stub(ui, 'addNewCredentials').callsArgWith(0, null, TEST_CREDENTIALS); + sinon.stub(awsProfileHandler, 'listProfiles').returns([TEST_PROFILE]); + sinon.stub(fs, 'ensureFileSync'); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(awsProfileHandler, 'addProfile'); + sinon.stub(profileHelper, 'setupProfile'); + + // call + awsProfileSetupHelper.setupAwsProfile(TEST_CONFIG, (err, awsProfile) => { + // verify + expect(awsProfileHandler.addProfile.args[0][0]).eq(TEST_NEW_AWS_PROFILE_NAME); + expect(awsProfileHandler.addProfile.args[0][1]).to.deep.eq(TEST_CREDENTIALS); + expect(profileHelper.setupProfile.args[0][0]).eq(TEST_NEW_AWS_PROFILE_NAME); + expect(profileHelper.setupProfile.args[0][1]).eq(TEST_PROFILE); + expect(infoStub.args[0][0]).eq(messages.AWS_CREATE_PROFILE_TITLE); + expect(infoStub.args[1][0]).eq(messages.AWS_CREATE_PROFILE_NO_BROWSER_OPEN_BROWSER); + expect(infoStub.args[2][0]).eq(` ${CONSTANTS.AWS.IAM.USER.NEW_USER_BASE_URL}${querystring.stringify(TEST_PARAMS)}`); + expect(infoStub.args[3][0]).eq(`\nAWS profile "${TEST_NEW_AWS_PROFILE_NAME}" was successfully created. The details are recorded in aws credentials file (.aws/credentials) located at your **HOME** folder.`); + expect(err).eq(null); + expect(awsProfile).eq(TEST_NEW_AWS_PROFILE_NAME); + + done(); + }); + }); + }); +}); diff --git a/test/unit/commands/configure/helper-test.js b/test/unit/commands/configure/helper-test.js new file mode 100644 index 00000000..433a64ba --- /dev/null +++ b/test/unit/commands/configure/helper-test.js @@ -0,0 +1,153 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const askProfileSetupHelper = require('@src/commands/configure/ask-profile-setup-helper'); +const awsProfileSetupHelper = require('@src/commands/configure/aws-profile-setup-helper'); +const messages = require('@src/commands/configure/messages'); +const helper = require('@src/commands/configure/helper'); +const AppConfig = require('@src/model/app-config'); +const Messenger = require('@src/view/messenger'); + +describe('Command: Configure - helper test', () => { + const TEST_PROFILE = 'testProfile'; + const TEST_AWS_PROFILE = 'testAwsProfile'; + const TEST_DO_DEBUG = false; + const TEST_NEED_BROWSER = false; + const TEST_VENDOR_ID = 'testVendorId'; + const TEST_ERROR_MESSAGE = 'errorMessage'; + const TEST_ACCESS_TOKEN = { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires_in: 3600, + expires_at: 'expires_at' + }; + const TEST_CONFIG = { + askProfile: TEST_PROFILE, + needBrowser: TEST_NEED_BROWSER, + debug: TEST_DO_DEBUG + }; + let setTokenStub; + let infoStub; + let setAwsProfileStub; + let writeStub; + let setVendorIdStub; + describe('# test initiateAskProfileSetup', () => { + beforeEach(() => { + infoStub = sinon.stub(); + setAwsProfileStub = sinon.stub(); + setVendorIdStub = sinon.stub(); + writeStub = sinon.stub(); + setTokenStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + sinon.stub(AppConfig, 'getInstance').returns({ + write: writeStub, + setAwsProfile: setAwsProfileStub, + setToken: setTokenStub, + setVendorId: setVendorIdStub + }); + sinon.stub(askProfileSetupHelper, 'setupAskToken'); + sinon.stub(askProfileSetupHelper, 'setupVendorId'); + sinon.stub(awsProfileSetupHelper, 'setupAwsProfile'); + }); + + it('| setupAskToken returns error', (done) => { + // setup + askProfileSetupHelper.setupAskToken.callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + helper.initiateAskProfileSetup(TEST_CONFIG, (error, askProfile) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| setupAskToken returns valid token and setupVendorId throws error ', (done) => { + // setup + askProfileSetupHelper.setupAskToken.callsArgWith(1, null, TEST_ACCESS_TOKEN); + askProfileSetupHelper.setupVendorId.callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + helper.initiateAskProfileSetup(TEST_CONFIG, (error, askProfile) => { + // verify + expect(setTokenStub.args[0][0]).eq(TEST_PROFILE); + expect(setTokenStub.args[0][1]).to.deep.eq(TEST_ACCESS_TOKEN); + expect(infoStub.args[0][0]).eq(`ASK Profile "${TEST_PROFILE}" was successfully created. The details are recorded in ask-cli config file (.ask/cli_config) located at your **HOME** folder.`); + expect(error).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| setupVendorId returns valid vendorId and setupAwsProfile throws error', (done) => { + // setup + askProfileSetupHelper.setupAskToken.callsArgWith(1, null, TEST_ACCESS_TOKEN); + askProfileSetupHelper.setupVendorId.callsArgWith(1, null, TEST_VENDOR_ID); + awsProfileSetupHelper.setupAwsProfile.callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + helper.initiateAskProfileSetup(TEST_CONFIG, (error, askProfile) => { + // verify + expect(setVendorIdStub.args[0][0]).eq(TEST_PROFILE); + expect(setVendorIdStub.args[0][1]).to.deep.eq(TEST_VENDOR_ID); + expect(infoStub.args[0][0]).eq(`ASK Profile "${TEST_PROFILE}" was successfully created. The details are recorded in ask-cli config file (.ask/cli_config) located at your **HOME** folder.`); + expect(infoStub.args[1][0]).eq(`Vendor ID set as ${TEST_VENDOR_ID}.\n`); + expect(error).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| successfully initiated askProfile but awsProfile is not set', (done) => { + // setup + askProfileSetupHelper.setupAskToken.callsArgWith(1, null, TEST_ACCESS_TOKEN); + askProfileSetupHelper.setupVendorId.callsArgWith(1, null, TEST_VENDOR_ID); + awsProfileSetupHelper.setupAwsProfile.callsArgWith(1, null); + + // call + helper.initiateAskProfileSetup(TEST_CONFIG, (error, askProfile) => { + // verify + expect(setVendorIdStub.args[0][0]).eq(TEST_PROFILE); + expect(setVendorIdStub.args[0][1]).to.deep.eq(TEST_VENDOR_ID); + expect(infoStub.args[0][0]).eq(`ASK Profile "${TEST_PROFILE}" was successfully created. The details are recorded in ask-cli config file (.ask/cli_config) located at your **HOME** folder.`); + expect(infoStub.args[1][0]).eq(`Vendor ID set as ${TEST_VENDOR_ID}.\n`); + expect(infoStub.args[2][0]).eq(messages.AWS_CONFIGURATION_MESSAGE); + expect(setAwsProfileStub.args[0][0]).eq(TEST_PROFILE); + expect(setAwsProfileStub.args[0][1]).to.deep.eq(undefined); + expect(error).eq(null); + expect(askProfile).eq(TEST_PROFILE); + done(); + }); + }); + + it('| successfully initiated askProfile and awsProfile', (done) => { + // setup + askProfileSetupHelper.setupAskToken.callsArgWith(1, null, TEST_ACCESS_TOKEN); + askProfileSetupHelper.setupVendorId.callsArgWith(1, null, TEST_VENDOR_ID); + awsProfileSetupHelper.setupAwsProfile.callsArgWith(1, null, TEST_AWS_PROFILE); + + // call + helper.initiateAskProfileSetup(TEST_CONFIG, (error, askProfile) => { + // verify + expect(setVendorIdStub.args[0][0]).eq(TEST_PROFILE); + expect(setVendorIdStub.args[0][1]).to.deep.eq(TEST_VENDOR_ID); + expect(infoStub.args[0][0]).eq(`ASK Profile "${TEST_PROFILE}" was successfully created. The details are recorded in ask-cli config file (.ask/cli_config) located at your **HOME** folder.`); + expect(infoStub.args[1][0]).eq(`Vendor ID set as ${TEST_VENDOR_ID}.\n`); + expect(infoStub.args[2][0]).eq(messages.AWS_CONFIGURATION_MESSAGE); + expect(infoStub.args[3][0]).eq(`AWS profile "${TEST_AWS_PROFILE}" was successfully associated with your ASK profile "${TEST_PROFILE}".\n`); + expect(setAwsProfileStub.args[0][0]).eq(TEST_PROFILE); + expect(setAwsProfileStub.args[0][1]).to.deep.eq(TEST_AWS_PROFILE); + expect(error).eq(null); + expect(askProfile).eq(TEST_PROFILE); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); +}); diff --git a/test/unit/commands/configure/index-test.js b/test/unit/commands/configure/index-test.js new file mode 100644 index 00000000..52b5cfbe --- /dev/null +++ b/test/unit/commands/configure/index-test.js @@ -0,0 +1,218 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const path = require('path'); +const fs = require('fs-extra'); +const jsonfile = require('jsonfile'); + +const ConfigureCommand = require('@src/commands/configure'); +const helper = require('@src/commands/configure/helper'); +const messages = require('@src/commands/configure/messages'); +const ui = require('@src/commands/configure/ui'); +const optionModel = require('@src/commands/option-model'); +const AppConfig = require('@src/model/app-config'); +const stringUtils = require('@src/utils/string-utils'); +const Messenger = require('@src/view/messenger'); + + +describe('Commands Configure test - command class test', () => { + const TEST_APP_CONFIG_FILE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model', 'cli_config'); + const TEST_PROFILE = 'default'; + const TEST_CMD = { + profile: TEST_PROFILE + }; + const TEST_INVALID_PROFILE = '&@%$&%@$^'; + const TEST_ERROR_MESSAGE = 'error'; + const TEST_AWS_PROFILE = 'awsProfile'; + const TEST_VENDOR_ID = 'vendorId'; + let infoStub; + let errorStub; + let warnStub; + let getProfileListStub; + + beforeEach(() => { + infoStub = sinon.stub(); + errorStub = sinon.stub(); + warnStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub, + error: errorStub, + warn: warnStub + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| validate command information is set correctly', () => { + const instance = new ConfigureCommand(optionModel); + expect(instance.name()).eq('configure'); + expect(instance.description()).eq('helps to configure the credentials that ask-cli uses to authenticate the user to Amazon developer services'); + expect(instance.requiredOptions()).deep.eq([]); + expect(instance.optionalOptions()).deep.eq(['noBrowser', 'profile', 'debug']); + }); + + describe('validate command handle - ensure AppConfig initiated', () => { + let instance; + const INVALID_FILE_PATH = '/invalid/path'; + + beforeEach(() => { + instance = new ConfigureCommand(optionModel); + }); + + afterEach(() => { + sinon.restore(); + AppConfig.dispose(); + }); + + it('| AppConfig creation, expect throw error', (done) => { + // setup + sinon.stub(path, 'join').returns(INVALID_FILE_PATH); + sinon.stub(fs, 'existsSync').returns(true); + + // call + instance.handle(TEST_CMD, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(err.message).eq(`File ${INVALID_FILE_PATH} not exists.`); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| returns error, invalid profile entered by user', (done) => { + // setup + sinon.stub(path, 'join').returns(TEST_APP_CONFIG_FILE_PATH); + const existsSyncStub = sinon.stub(fs, 'existsSync'); + existsSyncStub.onCall(0).returns(false); + existsSyncStub.onCall(1).returns(true); + sinon.stub(fs, 'ensureDirSync'); + sinon.stub(jsonfile, 'writeFileSync'); + sinon.stub(stringUtils, 'validateSyntax').returns(false); + getProfileListStub = sinon.stub().returns([]); + sinon.stub(AppConfig, 'getInstance').returns({ + getProfilesList: getProfileListStub, + }); + + // call + instance.handle({}, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(messages.PROFILE_NAME_VALIDATION_ERROR); + expect(askProfile).eq(undefined); + done(); + }); + }); + + describe(('# existing profiles'), () => { + beforeEach(() => { + instance = new ConfigureCommand(optionModel); + sinon.stub(path, 'join').returns(TEST_APP_CONFIG_FILE_PATH); + const existsSyncStub = sinon.stub(fs, 'existsSync'); + existsSyncStub.onCall(0).returns(false); + existsSyncStub.onCall(1).returns(true); + sinon.stub(fs, 'ensureDirSync'); + sinon.stub(jsonfile, 'writeFileSync'); + getProfileListStub = sinon.stub().returns([TEST_INVALID_PROFILE]); + const getAwsProfileStub = sinon.stub().returns(TEST_AWS_PROFILE); + const getVendorIdStub = sinon.stub().returns(TEST_VENDOR_ID); + sinon.stub(AppConfig, 'getInstance').returns({ + getProfilesList: getProfileListStub, + getAwsProfile: getAwsProfileStub, + getVendorId: getVendorIdStub + }); + }); + + it('| returns error, existing profiles but user enters invalid profile name', (done) => { + // setup + sinon.stub(stringUtils, 'validateSyntax').returns(false); + + // call + instance.handle({ profile: TEST_INVALID_PROFILE }, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(messages.PROFILE_NAME_VALIDATION_ERROR); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| returns error, existing profiles and valid profile name, setup fails', (done) => { + // setup + sinon.stub(stringUtils, 'validateSyntax').returns(true); + sinon.stub(helper, 'initiateAskProfileSetup').callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + instance.handle(TEST_CMD, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| returns error, existing profiles and invalid profile name, ask for creation of new profile fails', (done) => { + // setup + sinon.stub(stringUtils, 'validateSyntax').returns(true); + sinon.stub(ui, 'createOrUpdateProfile').callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + instance.handle({}, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| returns error, existing profiles and invalid profile name, setup fails', (done) => { + // setup + sinon.stub(stringUtils, 'validateSyntax').returns(true); + sinon.stub(ui, 'createOrUpdateProfile').callsArgWith(1, null, TEST_PROFILE); + sinon.stub(helper, 'initiateAskProfileSetup').callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + instance.handle({}, (err, askProfile) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(TEST_ERROR_MESSAGE); + expect(askProfile).eq(undefined); + done(); + }); + }); + + it('| successfully configured profiles', (done) => { + // setup + sinon.stub(stringUtils, 'validateSyntax').returns(true); + sinon.stub(ui, 'createOrUpdateProfile').callsArgWith(1, null, TEST_PROFILE); + sinon.stub(helper, 'initiateAskProfileSetup').callsArgWith(1, null, TEST_PROFILE); + + // call + instance.handle(TEST_CMD, (err) => { + // verify + expect(infoStub.args[0][0]).eq(messages.ASK_CLI_CONFIGURATION_MESSAGE); + expect(infoStub.args[1][0]).eq(messages.CONFIGURE_SETUP_SUCCESS_MESSAGE); + expect(infoStub.args[2][0]).eq(`ASK Profile: ${TEST_PROFILE}`); + expect(infoStub.args[3][0]).eq(`AWS Profile: ${TEST_AWS_PROFILE}`); + expect(infoStub.args[4][0]).eq(`Vendor ID: ${TEST_VENDOR_ID}`); + expect(fs.ensureDirSync.callCount).eq(1); + expect(jsonfile.writeFileSync.callCount).eq(1); + expect(err).eq(undefined); + done(); + }); + }); + }); + }); +}); diff --git a/test/unit/commands/init/questions-test.js b/test/unit/commands/configure/questions-test.js similarity index 74% rename from test/unit/commands/init/questions-test.js rename to test/unit/commands/configure/questions-test.js index 08db6af0..91d90a82 100644 --- a/test/unit/commands/init/questions-test.js +++ b/test/unit/commands/configure/questions-test.js @@ -1,8 +1,8 @@ const { expect } = require('chai'); -const questions = require('@src/commands/init/questions'); +const questions = require('@src/commands/configure/questions'); const CONSTANTS = require('@src/utils/constants'); -describe('Command: Init - questions validate test', () => { +describe('Command: Configure - questions validate test', () => { describe('# test request AWS profile name validators', () => { const TEST_LIST = ['1', '2']; @@ -16,7 +16,7 @@ describe('Command: Init - questions validate test', () => { }); it('| input is valid based on profiles list', () => { - TEST_LIST.push(CONSTANTS.COMMAND.INIT.AWS_DEFAULT_PROFILE_NAME); + TEST_LIST.push(CONSTANTS.COMMAND.CONFIGURE.AWS_DEFAULT_PROFILE_NAME); const result = questions.REQUEST_AWS_PROFILE_NAME(TEST_LIST); expect(result[0].validate('3')).equal(true); expect(result[0].default).equal(null); @@ -44,4 +44,16 @@ describe('Command: Init - questions validate test', () => { expect(questions.REQUEST_ACCESS_SECRET_KEY_AND_ID[1].validate('secretAccessKey')).equal(true); }); }); + + describe('# test authCode validators', () => { + it('| invalid authCode', () => { + // call and verify + expect(questions.REQUEST_AUTH_CODE[0].validate('')).equal('Please enter a valid Authorization Code.'); + }); + + it('| valid authCode', () => { + // call and verify + expect(questions.REQUEST_AUTH_CODE[0].validate('authorizationCode')).equal(true); + }); + }); }); diff --git a/test/unit/commands/configure/ui-test.js b/test/unit/commands/configure/ui-test.js new file mode 100644 index 00000000..6e2a8a72 --- /dev/null +++ b/test/unit/commands/configure/ui-test.js @@ -0,0 +1,528 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const inquirer = require('inquirer'); +const CONSTANTS = require('@src/utils/constants'); +const ui = require('@src/commands/configure/ui'); +const messages = require('@src/commands/configure/messages'); +const profileHelper = require('@src/utils/profile-helper'); +const stringUtils = require('@src/utils/string-utils'); + +function validateInquirerConfig(stub, expectedConfig) { + const { message, type, defaultValue, choices } = expectedConfig; + expect(stub.message).equal(message); + expect(stub.type).equal(type); + if (defaultValue) { + expect(stub.default).equal(defaultValue); + } + if (choices) { + expect(stub.choices).deep.equal(choices); + } +} + +describe('Command: Configure - UI test', () => { + const TEST_ERROR = 'error'; + describe('# confirmSettingAws check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| AWS setup confirmation by user', (done) => { + // setup + inquirer.prompt.resolves({ choice: 'true' }); + + // call + ui.confirmSettingAws((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: 'Do you want to link your AWS account in order to host your Alexa skills?', + type: 'confirm', + default: true + }); + + expect(err).equal(null); + expect(response).equal('true'); + done(); + }); + }); + + it('| AWS setup confirmation by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.confirmSettingAws((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: 'Do you want to link your AWS account in order to host your Alexa skills?', + type: 'confirm', + default: true + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# selectEnvironmentVariables check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| Use environment variables confirmation by user', (done) => { + // setup + inquirer.prompt.resolves({ choice: 'yes' }); + + // call + ui.selectEnvironmentVariables((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: 'We have detected you have AWS environment variables. Would you like to setup your profile using those?', + type: 'list', + choices: ['Yes', 'No'] + }); + expect(err).equal(null); + expect(response).equal('yes'); + done(); + }); + }); + + it('| Use environment variables confirmation by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.selectEnvironmentVariables((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: 'We have detected you have AWS environment variables. Would you like to setup your profile using those?', + type: 'list', + choices: ['Yes', 'No'] + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# addNewCredentials check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| Aws access_key_id and secret_access_key entered by user', (done) => { + // setup + const accessKeyId = 'accessKeyId'; + const secretAccessKey = 'secretAccessKey'; + inquirer.prompt.resolves({ + accessKeyId, + secretAccessKey + }); + + // call + ui.addNewCredentials((err, response) => { + // verify + expect(err).equal(null); + expect(response.aws_access_key_id).equal(accessKeyId); + expect(response.aws_secret_access_key).equal(secretAccessKey); + done(); + }); + }); + + it('| Aws access_key_id and secret_access_key entered by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.addNewCredentials((err, response) => { + // verify + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# createNewOrSelectAWSProfile check', () => { + const listOfProfiles = ['ask', 'aws', 'lambda']; + + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| create profile or use existing profile decision by user', (done) => { + // setup + inquirer.prompt.resolves({ chosenProfile: 'lambda' }); + + // call + ui.createNewOrSelectAWSProfile(listOfProfiles, (err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'list', + message: 'Please choose from the following existing AWS profiles or create a new one.' + }); + expect(err).equal(null); + expect(response).equal('lambda'); + done(); + }); + }); + + it('| create profile or use existing profile decision by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.createNewOrSelectAWSProfile(listOfProfiles, (err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'list', + message: 'Please choose from the following existing AWS profiles or create a new one.' + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# createNewProfile check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| valid profile name entered by user', (done) => { + // setup + inquirer.prompt.resolves({ profile: 'lambda' }); + + // call + ui.createNewProfile((error, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: `Please provide a profile name or press enter to use ${CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME} as the profile name: `, + type: 'input', + default: CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME + }); + expect(error).equal(null); + expect(response).equal('lambda'); + done(); + }); + }); + + it('| invalid profile name entered by user', (done) => { + // setup + sinon.stub(profileHelper, 'askProfileSyntaxValidation').returns(false); + inquirer.prompt.resolves({ profile: ' %*^@&!' }); + + // call + ui.createNewProfile((error, response) => { + // verify + expect(error).equal(messages.PROFILE_NAME_VALIDATION_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + it('| profile name entered by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.createNewProfile((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + message: `Please provide a profile name or press enter to use ${CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME} as the profile name: `, + type: 'input', + default: CONSTANTS.COMMAND.CONFIGURE.ASK_DEFAULT_PROFILE_NAME + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# chooseVendorId check', () => { + const VENDOR_PAGE_SIZE = 50; + const vendorInfo = [ + { + vendorName: 'vendor_name1', + vendorId: ' vendor_id1' + }, + { + vendorName: 'vendor_name2', + vendorId: ' vendor_id2' + } + ]; + + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| vendor selection by user', (done) => { + // setup + inquirer.prompt.resolves({ selectedVendor: vendorInfo[0].vendorId }); + + // call + ui.chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, (error, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'rawlist', + message: 'Your Amazon developer account has multiple Vendor IDs. Please choose the Vendor ID for the skills you want to manage.', + }); + expect(error).equal(null); + expect(response).equal('vendor_id1'); + done(); + }); + }); + + it('| vendor selection by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, (err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'rawlist', + message: 'Your Amazon developer account has multiple Vendor IDs. Please choose the Vendor ID for the skills you want to manage.', + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# createOrUpdateProfile check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + sinon.stub(profileHelper, 'askProfileSyntaxValidation'); + }); + + it('| returns error | invalid profile name entered', (done) => { + // setup + const listOfProfiles = [ + 'askProfile1', + 'askProfile2' + ]; + const createNewProfile = 'Create new profile'; + profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); + inquirer.prompt.resolves({ profile: createNewProfile }); + sinon.stub(ui, 'createNewProfile').callsArgWith(0, null, 'askProfile2'); + + + // call + ui.createOrUpdateProfile(listOfProfiles, (error, askProfile) => { + // verify + expect(error).to.equal(messages.PROFILE_NAME_VALIDATION_ERROR); + expect(askProfile).equals(undefined); + done(); + }); + }); + + it('| returns valid profile | createNewProfile returns valid profile', (done) => { + // setup + const listOfProfiles = [ + 'askProfile1', + 'askProfile2' + ]; + const createNewProfile = 'Create new profile'; + profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); + inquirer.prompt.resolves({ profile: createNewProfile }); + sinon.stub(stringUtils, 'validateSyntax').returns(true); + sinon.stub(ui, 'createNewProfile').callsArgWith(0, null, 'askProfile2'); + + // call + ui.createOrUpdateProfile(listOfProfiles, (error, askProfile) => { + // verify + expect(error).to.equal(null); + expect(askProfile).equals(createNewProfile); + done(); + }); + }); + + it('| update an existing ASK profile by user', (done) => { + // setup + const listOfProfiles = [ + 'askProfile1', + 'askProfile2', + '#$*%#$(%$43' + ]; + profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); + inquirer.prompt.resolves({ profile: listOfProfiles[1] }); + + // call + ui.createOrUpdateProfile(listOfProfiles, (error, response) => { + // verify + expect(error).to.equal(null); + expect(response).equal('askProfile2'); + done(); + }); + }); + + it('| invalid profile name read from config file', (done) => { + // setup + const listOfProfiles = [ + 'askProfile1', + '#$*%#$(%$43' + ]; + profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(false); + inquirer.prompt.resolves({ profile: listOfProfiles[1] }); + + // call + ui.createOrUpdateProfile(listOfProfiles, (error, response) => { + // verify + expect(error).equal(messages.PROFILE_NAME_VALIDATION_ERROR); + expect(response).to.equal(undefined); + done(); + }); + }); + + it('| createOrUpdateProfile and inquirer throws exception', (done) => { + // setup + const listOfProfiles = [ + 'askProfile1', + '#$*%#$(%$43' + ]; + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.createOrUpdateProfile(listOfProfiles, (err, response) => { + // verify + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# requestAwsProfileName check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| Aws profile name entered by user', (done) => { + // setup + inquirer.prompt.resolves({ awsProfileName: 'awsProfile' }); + + // call + ui.requestAwsProfileName([], (error, response) => { + // verify + expect(error).to.equal(null); + expect(response).equal('awsProfile'); + done(); + }); + }); + + it('| Aws profile name entered by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.requestAwsProfileName([], (err, response) => { + // verify + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# stringFormatter', () => { + it('| return null if not profile list', () => { + expect(ui.profileFormatter()).to.be.a('null'); + }); + + it('| return null if profile list is empty', () => { + expect(ui.profileFormatter([])).to.be.a('null'); + }); + + it('| return formatted profile list with no aws profile', () => { + const input = [{ + askProfile: 'ask_test', + awsProfile: null + }]; + const expectResult = ['[ask_test] ** NULL **']; + expect(ui.profileFormatter(input)).to.eql(expectResult); + }); + + it('| return formatted profile list with aws profile', () => { + const input = [{ + askProfile: 'ask_test', + awsProfile: 'aws_test' + }]; + const expectResult = ['[ask_test] "aws_test"']; + expect(ui.profileFormatter(input)).to.eql(expectResult); + }); + }); + + describe('# getAuthCode check', () => { + beforeEach(() => { + sinon.stub(inquirer, 'prompt'); + }); + + it('| Authorization code entered by user', (done) => { + // setup + inquirer.prompt.resolves({ authCode: 'authorizationCode' }); + + // call + ui.getAuthCode((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'input', + message: 'Please enter the Authorization Code: ' + }); + + expect(err).equal(null); + expect(response).equal('authorizationCode'); + done(); + }); + }); + + it('| Authorization code entered by user and inquirer throws exception', (done) => { + // setup + inquirer.prompt.rejects(new Error(TEST_ERROR)); + // call + ui.getAuthCode((err, response) => { + // verify + validateInquirerConfig(inquirer.prompt.args[0][0][0], { + type: 'input', + message: 'Please enter the Authorization Code: ' + }); + expect(err.message).equal(TEST_ERROR); + expect(response).equal(undefined); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); +}); diff --git a/test/unit/commands/init/ask-setup-helper-test.js b/test/unit/commands/init/ask-setup-helper-test.js deleted file mode 100644 index f64155e9..00000000 --- a/test/unit/commands/init/ask-setup-helper-test.js +++ /dev/null @@ -1,170 +0,0 @@ -const { assert, expect } = require('chai'); -const sinon = require('sinon'); -const fs = require('fs'); -const jsonfile = require('jsonfile'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); -const profileHelper = require('@src/utils/profile-helper'); -const askSetupHelper = require('@src/commands/init/ask-setup-helper'); -const apiWrapper = require('@src/api/api-wrapper'); -const tools = require('@src/utils/tools'); -const jsonRead = require('@src/utils/json-read'); -const messages = require('@src/commands/init/messages'); -const ui = require('@src/commands/init/ui'); -const jsonUtility = require('@src/utils/json-utility'); - -describe('Command: Init - ASK setup helper test', () => { - describe('# set up ask config', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(oauthWrapper, 'writeToken'); - }); - - it('| token writing successful', () => { - // call - askSetupHelper.setupAskConfig({}, 'askPRofile', (error) => { - // verify - assert.isUndefined(error); - }); - }); - - it('| error while writing tokens', () => { - // setup - const ERROR = 'error while writing tokens'; - oauthWrapper.writeToken.withArgs(sinon.match.any, sinon.match.string).throws(ERROR); - - // call - askSetupHelper.setupAskConfig({}, 'askPRofile', (error) => { - // verify - assert.isDefined(error); - expect(error).equal(`Failed to update the cli_config file with the retrieved token. ${ERROR}`); - }); - }); - - afterEach(() => { - sandbox.restore(); - }); - }); - - describe('# Verify first time profile creation', () => { - beforeEach(() => { - sinon.stub(fs, 'existsSync'); - sinon.stub(fs, 'mkdirSync'); - sinon.stub(profileHelper, 'getListProfile'); - sinon.stub(jsonfile, 'writeFileSync'); - }); - - it('| ask config exists with valid profile', () => { - // setup - fs.existsSync.withArgs(sinon.match.string).returns(true); - profileHelper.getListProfile.returns(['ask']); - - // call and verify - assert.isFalse(askSetupHelper.isFirstTimeCreation()); - }); - - it('| ask config does not exist', () => { - // setup - fs.existsSync.withArgs(sinon.match.string).returns(false); - profileHelper.getListProfile.returns([]); - - // call and verify - assert.isTrue(askSetupHelper.isFirstTimeCreation()); - }); - - it('| ask config exists but no profiles', () => { - // setup - fs.existsSync.withArgs(sinon.match.string).returns(true); - profileHelper.getListProfile.returns([]); - - // call and verify - assert.isTrue(askSetupHelper.isFirstTimeCreation()); - }); - - afterEach(() => { - sinon.restore(); - }); - }); - - describe('# set vendor ID', () => { - beforeEach(() => { - sinon.stub(apiWrapper, 'callListVendor'); - sinon.stub(tools, 'convertDataToJsonObject'); - sinon.stub(jsonRead, 'readFile'); - }); - - it('| no config file found', () => { - // setup - apiWrapper.callListVendor.callsArgOnWith(2, {}, {}); - jsonRead.readFile.withArgs(sinon.match.string).returns(''); - - // call - askSetupHelper.setVendorId('askProfile', false, (err) => { - // verify - expect(err).equal(messages.ASK_CONFIG_NOT_FOUND_ERROR); - }); - }); - - it('| invalid vendor info', () => { - // setup - apiWrapper.callListVendor.callsArgOnWith(2, {}, {}); - jsonRead.readFile.withArgs(sinon.match.string).returns('path/to/homeconfig'); - tools.convertDataToJsonObject.withArgs(sinon.match.any).returns({ }); - - // call - askSetupHelper.setVendorId('askProfile', false, (err) => { - // verify - expect(err).equal(messages.VENDOR_INFO_FETCH_ERROR); - }); - }); - - it('| zero vendor IDs', () => { - // setup - apiWrapper.callListVendor.callsArgOnWith(2, {}, {}); - jsonRead.readFile.withArgs(sinon.match.string).returns('path/to/homeconfig'); - tools.convertDataToJsonObject.withArgs(sinon.match.any).returns({ vendors: [] }); - - // call - askSetupHelper.setVendorId('askProfile', false, (err) => { - // verify - expect(err).equal('There is no vendor ID for your account.'); - }); - }); - - it('| single vendor ID', () => { - // setup - sinon.stub(jsonUtility, 'writeToProperty'); - apiWrapper.callListVendor.callsArgOnWith(2, {}, {}); - jsonRead.readFile.withArgs(sinon.match.string).returns('path/to/homeconfig'); - tools.convertDataToJsonObject.withArgs(sinon.match.any).returns({ vendors: [{ id: 'KGHFT576JH' }] }); - - // call - askSetupHelper.setVendorId('askProfile', false, (err, response) => { - // verify - expect(err).to.be.null; - expect(response).equal('KGHFT576JH'); - }); - }); - - it('| multiple vendor IDs', () => { - // setup - sinon.stub(ui, 'chooseVendorId'); - sinon.stub(jsonUtility, 'writeToProperty'); - ui.chooseVendorId.callsArgOnWith(2, {}, 'KGHFT576JH'); - apiWrapper.callListVendor.callsArgOnWith(2, {}, {}); - jsonRead.readFile.withArgs(sinon.match.string).returns('path/to/homeconfig'); - tools.convertDataToJsonObject.withArgs(sinon.match.any).returns({ vendors: [{ id: 'KGHFT576JH' }, { id: 'KGHFT586JH' }] }); - - // call - askSetupHelper.setVendorId('askProfile', false, (error, response) => { - // verify - expect(error).to.be.null; - expect(response).equal('KGHFT576JH'); - }); - }); - - afterEach(() => { - sinon.restore(); - }); - }); -}); diff --git a/test/unit/commands/init/aws-setup-helper-test.js b/test/unit/commands/init/aws-setup-helper-test.js deleted file mode 100644 index 12611ee5..00000000 --- a/test/unit/commands/init/aws-setup-helper-test.js +++ /dev/null @@ -1,127 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const proxyquire = require('proxyquire'); - -const helper = require('@src/commands/init/aws-setup-helper'); -const ui = require('@src/commands/init/ui'); -const profileHelper = require('@src/utils/profile-helper'); -const stringUtils = require('@src/utils/string-utils'); -const CONSTANTS = require('@src/utils/constants'); - -describe('Command: Init - AWS setup helper test', () => { - const TEST_ASK_PROFILE = 'askProfile'; - - describe('# inspect method handleEnvironmentVariableAwsSetup', () => { - afterEach(() => { - sinon.restore(); - }); - - it('| no env var set for AWS profile, expect callback with false result', (done) => { - // setup - sinon.stub(stringUtils, 'isNonBlankString').returns(false); - // call - helper.handleEnvironmentVariableAwsSetup(TEST_ASK_PROFILE, (err, res) => { - // expect - expect(err).equal(null); - expect(res).equal(false); - done(); - }); - }); - - it('| user selects "no", expect callback with false result', (done) => { - // setup - sinon.stub(stringUtils, 'isNonBlankString').returns(true); - sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, 'No'); - // call - helper.handleEnvironmentVariableAwsSetup(TEST_ASK_PROFILE, (err, res) => { - // expect - expect(err).equal(null); - expect(res).equal(false); - done(); - }); - }); - - it('| user selects desired result, expect callback with the selection', (done) => { - // setup - sinon.stub(stringUtils, 'isNonBlankString').returns(true); - sinon.stub(ui, 'selectEnvironmentVariables').callsArgWith(0, 'Yes'); - sinon.stub(profileHelper, 'setupProfile'); - // call - helper.handleEnvironmentVariableAwsSetup(TEST_ASK_PROFILE, (err, res) => { - // expect - expect(err).equal(null); - expect(res).equal(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); - expect(profileHelper.setupProfile.args[0][0]).equal(CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.AWS_CREDENTIALS); - expect(profileHelper.setupProfile.args[0][1]).equal(TEST_ASK_PROFILE); - done(); - }); - }); - }); - - describe('# inspect method decideAwsProfileName', () => { - afterEach(() => { - sinon.restore(); - }); - - it('| profile list is empty, expect callback with default name', (done) => { - helper.decideAwsProfileName([], (name) => { - expect(name).equal(CONSTANTS.COMMAND.INIT.AWS_DEFAULT_PROFILE_NAME); - done(); - }); - }); - - it('| profile list is not empty, expect callback user input name', (done) => { - sinon.stub(ui, 'requestAwsProfileName').callsArgWith(1, '1'); - helper.decideAwsProfileName(['1', '2'], (name) => { - expect(name).equal('1'); - done(); - }); - }); - }); - - describe('# inspect method openIamCreateUserPage', () => { - const TEST_USER_NAME = 'user'; - let opnStub, proxyHelper; - - beforeEach(() => { - opnStub = sinon.stub(); - proxyHelper = proxyquire('@src/commands/init/aws-setup-helper', { - opn: opnStub - }); - }); - - afterEach(() => { - sinon.restore(); - }); - - it('| when no-browser is set, expect correct console log', (done) => { - // setup - sinon.stub(console, 'log'); - // call - proxyHelper.openIamCreateUserPage(false, TEST_USER_NAME, () => { - // expect - expect(opnStub.callCount).equal(0); - expect(console.log.args[0][0]).equal('\nComplete the IAM user creation with required permissions from the AWS console, ' - + 'then come back to the terminal.'); - expect(console.log.args[1][0]).equal('Please open the following url in your browser:'); - expect(console.log.args[2][0].includes(TEST_USER_NAME)).equal(true); - console.log.restore(); - done(); - }); - }); - - it('| when having access to browser, expect correct console log', (done) => { - // setup - sinon.stub(console, 'log'); - // call - proxyHelper.openIamCreateUserPage(true, TEST_USER_NAME, () => { - // expect - expect(opnStub.args[0][0].includes(TEST_USER_NAME)).equal(true); - expect(console.log.args[0][0]).equal('\nComplete the IAM user creation with required permissions from the AWS console, ' - + 'then come back to the terminal.'); - console.log.restore(); - done(); - }); - }); - }); -}); diff --git a/test/unit/commands/init/aws-setup-wizard-test.js b/test/unit/commands/init/aws-setup-wizard-test.js deleted file mode 100644 index a2b481a5..00000000 --- a/test/unit/commands/init/aws-setup-wizard-test.js +++ /dev/null @@ -1,177 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const fs = require('fs-extra'); -const awsProfileHandler = require('aws-profile-handler'); - -const awsSetupWizard = require('@src/commands/init/aws-setup-wizard'); -const helper = require('@src/commands/init/aws-setup-helper'); -const ui = require('@src/commands/init/ui'); -const profileHelper = require('@src/utils/profile-helper'); -const messages = require('@src/commands/init/messages'); - -describe('Command: Init - AWS setup wizard test', () => { - const TEST_IS_BROWSER = false; - const TEST_ASK_PROFILE = 'askProfile'; - const TEST_AWS_PROFILE = 'awsProfile'; - - describe('# test function startFlow', () => { - afterEach(() => { - sinon.restore(); - }); - - it('| awsProfileHandler listProfiles function throws error, expect error called back', (done) => { - // setup - sinon.stub(fs, 'existsSync').returns(true); - sinon.stub(awsProfileHandler, 'listProfiles').throws(new Error('error')); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, (err) => { - // expect - expect(err.message).equal('error'); - done(); - }); - }); - - it('| confirm not using AWS, expect callback no error and log skip aws init message', (done) => { - // setup - sinon.stub(fs, 'existsSync').returns(false); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, false); - sinon.stub(console, 'log'); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, (err) => { - // expect - expect(err).equal(undefined); - expect(console.log.args[0][0]).equal(messages.SKIP_AWS_INITIALIZATION); - console.log.restore(); - done(); - }); - }); - - it('| setup through env var AWS profile but error happens, expect callback error', (done) => { - // setup - sinon.stub(fs, 'existsSync').returns(false); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, true); - sinon.stub(helper, 'handleEnvironmentVariableAwsSetup').callsArgWith(1, 'error'); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, (err) => { - // expect - expect(err).equal('error'); - done(); - }); - }); - - it('| setup through env var AWS profile, expect no error callback and quit the process', (done) => { - // setup - sinon.stub(fs, 'existsSync').returns(false); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, true); - sinon.stub(helper, 'handleEnvironmentVariableAwsSetup').callsArgWith(1, null, true); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, () => { - // expect - expect(); - done(); - }); - }); - - it('| profile list is empty, expect go to create AWS profile flow', (done) => { - // setup - sinon.stub(fs, 'existsSync').returns(false); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, true); - sinon.stub(helper, 'handleEnvironmentVariableAwsSetup').callsArgWith(1, null, false); - sinon.stub(awsSetupWizard, 'createAwsProfileFlow').callsArgWith(3); - sinon.stub(ui, 'createNewOrSelectAWSProfile'); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, () => { - // expect - expect(awsSetupWizard.createAwsProfileFlow.args[0][0]).equal(false); - expect(awsSetupWizard.createAwsProfileFlow.args[0][1]).equal(TEST_ASK_PROFILE); - expect(awsSetupWizard.createAwsProfileFlow.args[0][2]).deep.equal([]); - done(); - }); - }); - - it('| profile list is not empty and user select to create new profile, expect go to create AWS profile flow', (done) => { - // setup - const TEST_PROFILES_LIST = ['1', '2']; - sinon.stub(fs, 'existsSync').returns(true); - sinon.stub(awsProfileHandler, 'listProfiles').returns(TEST_PROFILES_LIST); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, true); - sinon.stub(helper, 'handleEnvironmentVariableAwsSetup').callsArgWith(1, null, false); - sinon.stub(awsSetupWizard, 'createAwsProfileFlow').callsArgWith(3); - sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, 'Create new profile'); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, () => { - // expect - expect(awsSetupWizard.createAwsProfileFlow.args[0][0]).equal(false); - expect(awsSetupWizard.createAwsProfileFlow.args[0][1]).equal(TEST_ASK_PROFILE); - expect(awsSetupWizard.createAwsProfileFlow.args[0][2]).deep.equal(TEST_PROFILES_LIST); - done(); - }); - }); - - it('| profile list is not empty and user select to update existing profile, expect to link the profile', (done) => { - // setup - const TEST_PROFILES_LIST = ['1', '2']; - sinon.stub(fs, 'existsSync').returns(true); - sinon.stub(awsProfileHandler, 'listProfiles').returns(TEST_PROFILES_LIST); - sinon.stub(ui, 'confirmSettingAws').callsArgWith(0, true); - sinon.stub(helper, 'handleEnvironmentVariableAwsSetup').callsArgWith(1, null, false); - sinon.stub(awsSetupWizard, 'createAwsProfileFlow'); - sinon.stub(ui, 'createNewOrSelectAWSProfile').callsArgWith(1, '1'); - sinon.stub(profileHelper, 'setupProfile'); - // call - awsSetupWizard.startFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, () => { - // expect - expect(profileHelper.setupProfile.args[0][0]).equal('1'); - expect(profileHelper.setupProfile.args[0][1]).equal(TEST_ASK_PROFILE); - done(); - }); - }); - }); - - describe('# test function createAwsProfileFlow', () => { - const TEST_AWS_PROFILES_LIST = ['1', '2', TEST_AWS_PROFILE]; - - afterEach(() => { - sinon.restore(); - }); - - it('| fs ensure file fails, expect error callback', (done) => { - // setup - sinon.stub(fs, 'ensureFileSync').throws(new Error('error')); - // call - awsSetupWizard.createAwsProfileFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, TEST_AWS_PROFILES_LIST, (err) => { - // expect - expect(err.message).equal('error'); - done(); - }); - }); - - it('| fs ensure file fails, expect error callback', (done) => { - // setup - const TEST_CREDENTIALS = {}; - sinon.stub(fs, 'ensureFileSync'); - sinon.stub(helper, 'decideAwsProfileName').callsArgWith(1, TEST_AWS_PROFILE); - sinon.stub(helper, 'openIamCreateUserPage').callsArgWith(2); - sinon.stub(ui, 'addNewCredentials').callsArgWith(0, TEST_CREDENTIALS); - sinon.stub(awsProfileHandler, 'addProfile'); - sinon.stub(profileHelper, 'setupProfile'); - sinon.stub(console, 'log'); - // call - awsSetupWizard.createAwsProfileFlow(TEST_IS_BROWSER, TEST_ASK_PROFILE, TEST_AWS_PROFILES_LIST, (err, finalName) => { - // expect - expect(helper.openIamCreateUserPage.args[0][0]).equal(false); - expect(helper.openIamCreateUserPage.args[0][1]).equal(`ask-cli-${TEST_AWS_PROFILE}`); - expect(awsProfileHandler.addProfile.args[0][0]).equal(TEST_AWS_PROFILE); - expect(awsProfileHandler.addProfile.args[0][1]).equal(TEST_CREDENTIALS); - expect(profileHelper.setupProfile.args[0][0]).equal(TEST_AWS_PROFILE); - expect(profileHelper.setupProfile.args[0][1]).equal(TEST_ASK_PROFILE); - expect(console.log.args[0][0]).equal(`\nAWS profile "${TEST_AWS_PROFILE}" was successfully created. \ -The details are recorded in aws credentials file ($HOME/.aws/credentials).`); - expect(err).equal(null); - expect(finalName).equal(TEST_AWS_PROFILE); - console.log.restore(); - done(); - }); - }); - }); -}); diff --git a/test/unit/commands/init/handler-test.js b/test/unit/commands/init/handler-test.js deleted file mode 100644 index 66236a15..00000000 --- a/test/unit/commands/init/handler-test.js +++ /dev/null @@ -1,251 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const profileHelper = require('@src/utils/profile-helper'); -const awsSetupWizard = require('@src/commands/init/aws-setup-wizard'); -const askSetupHelper = require('@src/commands/init/ask-setup-helper'); -const handler = require('@src/commands/init/handler'); -const messages = require('@src/commands/init/messages'); -const lwaUtil = require('@src/utils/lwa'); -const ui = require('@src/commands/init/ui'); -const stringUtils = require('@src/utils/string-utils'); - -describe('Command: Init - handler test', () => { - describe('# Initialization process handler test', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(profileHelper, 'askProfileSyntaxValidation'); - sandbox.stub(askSetupHelper, 'isFirstTimeCreation'); - sandbox.stub(profileHelper, 'stringFormatter'); - sandbox.stub(profileHelper, 'getListProfile'); - sandbox.stub(console, 'log'); - sandbox.stub(console, 'error'); - sandbox.stub(process, 'exit'); - sandbox.stub(ui, 'createOrUpdateProfile'); - sandbox.stub(askSetupHelper, 'setupAskConfig'); - sandbox.stub(askSetupHelper, 'setVendorId'); - sandbox.stub(awsSetupWizard, 'startFlow'); - sandbox.stub(lwaUtil, 'accessTokenGenerator'); - sandbox.stub(stringUtils, 'isNonBlankString'); - }); - - it('| is first time creation with no profile entered by user: valid profile', () => { - // setup - askSetupHelper.isFirstTimeCreation.returns(true); - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); - - // call - handler.handleOptions({ }); - - // verify - expect(lwaUtil.accessTokenGenerator.called).equal(true); - }); - - it('| has existing profiles and profile not entered by user, error while creating profile', () => { - // setup - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); - profileHelper.stringFormatter.withArgs(sinon.match.array).returns([]); - profileHelper.getListProfile.returns([]); - ui.createOrUpdateProfile.callsArgOnWith(2, {}, 'error', 'askProfile'); - - // call - handler.handleOptions({ }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${messages.PROFILE_NAME_VALIDATION_ERROR}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| has existing profiles and profile not entered by user, no error while creating profile', () => { - // setup - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); - profileHelper.stringFormatter.withArgs(sinon.match.array).returns([]); - profileHelper.getListProfile.returns([]); - ui.createOrUpdateProfile.callsArgOnWith(2, {}, null, 'askProfile'); - - // call - handler.handleOptions({ }); - - // verify - expect(lwaUtil.accessTokenGenerator.withArgs(sinon.match.any, sinon.match.object, sinon.match.func).called).equal(true); - }); - - it('| has existing profiles but profile entered by user: valid profile', () => { - // setup - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(true); - - // call - handler.handleOptions({ profile: 'askProfile' }); - - // verify - expect(lwaUtil.accessTokenGenerator.called).equal(true); - }); - - it('| invalid profile entered by user', () => { - // setup - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(false); - askSetupHelper.isFirstTimeCreation.returns(true); - - // call - handler.handleOptions({ }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${messages.PROFILE_NAME_VALIDATION_ERROR}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| directInitProcess accessTokenGenerator throws error', () => { - // setup - const ERROR_FROM_TOKEN_GENERATOR = 'error from token generator'; - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, ERROR_FROM_TOKEN_GENERATOR, null); - - // call - handler.handleOptions({ profile: 'askProfile' }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${ERROR_FROM_TOKEN_GENERATOR}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| directInitProcess setupAskConfig throws error', () => { - // setup - const ERROR_FROM_ASK_CONFIG_SETUP = 'error from ask config setup'; - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, null, {}); - askSetupHelper.setupAskConfig.callsArgOnWith(2, {}, ERROR_FROM_ASK_CONFIG_SETUP); - - // call - handler.handleOptions({ profile: 'askProfile' }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${ERROR_FROM_ASK_CONFIG_SETUP}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| directInitProcess setVendorId throws error', () => { - // setup - const ERROR_FROM_SET_VENDOR_ID = 'error from set vendor id'; - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, null, {}); - askSetupHelper.setupAskConfig.callsArgOnWith(2, {}, null); - askSetupHelper.setVendorId.callsArgOnWith(2, {}, ERROR_FROM_SET_VENDOR_ID, 'vendorId'); - - // call - handler.handleOptions({ profile: 'askProfile' }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${ERROR_FROM_SET_VENDOR_ID}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| directInitProcess awsResolver throws error', () => { - // setup - const ERROR_FROM_AWS_RESOLVER = 'error from set vendor id'; - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs('askProfile').returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, null, {}); - askSetupHelper.setupAskConfig.callsArgOnWith(2, {}, null); - askSetupHelper.setVendorId.callsArgOnWith(2, {}, null, 'vendorId'); - awsSetupWizard.startFlow.callsArgOnWith(2, {}, ERROR_FROM_AWS_RESOLVER); - - // call - handler.handleOptions({ profile: 'askProfile' }); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${ERROR_FROM_AWS_RESOLVER}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| directInitProcess complete profile setup process', () => { - // setup - const ASK_PROFILE = 'askProfile'; - const AWS_PROFILE = 'awsProfile'; - const VENDOR_ID = 'vendorId'; - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs(ASK_PROFILE).returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, null, {}); - askSetupHelper.setupAskConfig.callsArgOnWith(2, {}, null); - askSetupHelper.setVendorId.callsArgOnWith(2, {}, null, VENDOR_ID); - awsSetupWizard.startFlow.callsArgOnWith(2, {}, null, AWS_PROFILE); - stringUtils.isNonBlankString.withArgs(sinon.match.string).returns(true); - - // call - handler.handleOptions({ profile: ASK_PROFILE }); - - // verify - expect(console.log.args[0][0]).equal(messages.ASK_CLI_INITIALIZATION_MESSAGE); - expect(console.log.args[1][0]) - .equal(`ASK Profile "${ASK_PROFILE}" was successfully created. The details are recorded in ask-cli config ($HOME/.ask/cli_config).`); - expect(console.log.args[2][0]).equal(`Vendor ID set as ${VENDOR_ID}.`); - expect(console.log.args[4][0]).equal(messages.AWS_INITIALIZATION_MESSAGE); - expect(console.log.args[5][0]).equal(`AWS profile "${AWS_PROFILE}" was successfully associated with your ASK profile "${ASK_PROFILE}".`); - expect(console.log.args[7][0]).equal('------------------------- Initialization Complete -------------------------'); - expect(console.log.args[8][0]).equal('Here is the summary for the profile setup: '); - expect(console.log.args[9][0]).equal(` ASK Profile: ${ASK_PROFILE}`); - expect(console.log.args[10][0]).equal(` AWS Profile: ${AWS_PROFILE}`); - expect(console.log.args[11][0]).equal(` Vendor ID: ${VENDOR_ID}`); - }); - - it('| directInitProcess invalid awsProfile', () => { - // setup - const ASK_PROFILE = 'askProfile'; - const AWS_PROFILE = null; - const VENDOR_ID = 'vendorId'; - stringUtils.isNonBlankString.withArgs(sinon.match.string).returns(false); - askSetupHelper.isFirstTimeCreation.returns(false); - profileHelper.askProfileSyntaxValidation.withArgs(ASK_PROFILE).returns(true); - lwaUtil.accessTokenGenerator.callsArgOnWith(2, {}, null, {}); - askSetupHelper.setupAskConfig.callsArgOnWith(2, {}, null); - askSetupHelper.setVendorId.callsArgOnWith(2, {}, null, VENDOR_ID); - awsSetupWizard.startFlow.callsArgOnWith(2, {}, null, AWS_PROFILE); - - // call - handler.handleOptions({ profile: ASK_PROFILE }); - - // verify - expect(console.log.args[9][0]).equal(` No AWS profile linked to profile "${ASK_PROFILE}"`); - }); - - afterEach(() => { - sandbox.restore(); - }); - }); - - describe('# init options handler test', () => { - it('| invalid options', () => { - // setup - sinon.stub(console, 'error'); - sinon.stub(process, 'exit'); - - // call - handler.handleOptions(' '); - - // verify - expect(console.error.args[0][0]).equal(`[Error]: ${messages.INVALID_COMMAND_ERROR}`); - expect(process.exit.args[0][0]).equal(1); - }); - - it('| show list of profiles', () => { - // setup - sinon.stub(console, 'log'); - sinon.spy(profileHelper, 'displayProfile'); - - // call - handler.handleOptions({ listProfiles: true }); - - // verify - expect(profileHelper.displayProfile.called).equal(true); - }); - - afterEach(() => { - sinon.restore(); - }); - }); -}); diff --git a/test/unit/commands/init/ui-test.js b/test/unit/commands/init/ui-test.js deleted file mode 100644 index a178485b..00000000 --- a/test/unit/commands/init/ui-test.js +++ /dev/null @@ -1,276 +0,0 @@ -const { expect } = require('chai'); -const sinon = require('sinon'); -const inquirer = require('inquirer'); -const ui = require('@src/commands/init/ui'); -const messages = require('@src/commands/init/messages'); -const profileHelper = require('@src/utils/profile-helper'); - -describe('Command: Init - UI test', () => { - describe('# confirmSettingAws check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| AWS setup confirmation by user', () => { - // setup - inquirer.prompt.resolves({ choice: 'true' }); - - // call - ui.confirmSettingAws((response) => { - // verify - expect(response).equal('true'); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); - - describe('# selectEnvironmentVariables check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| Use environment variables confirmation by user', () => { - // setup - inquirer.prompt.resolves({ choice: 'true' }); - - // call - ui.selectEnvironmentVariables((response) => { - // verify - expect(response).equal('true'); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); - - describe('# addNewCredentials check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| Aws access_key_id and secret_access_key entered by user', () => { - // setup - const accessKeyId = 'accessKeyId'; - const secretAccessKey = 'secretAccessKey'; - inquirer.prompt.resolves({ - accessKeyId, - secretAccessKey - }); - - // call - ui.addNewCredentials((response) => { - // verify - expect(response.aws_access_key_id).equal(accessKeyId); - expect(response.aws_secret_access_key).equal(secretAccessKey); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); - - describe('# confirmOverwritingProfile check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| confirmation of overwriting profile by user', () => { - // setup - inquirer.prompt.resolves({ overwrite: 'true' }); - - // call - ui.confirmOverwritingProfile('random_profile', (response) => { - // verify - expect(response).equal('true'); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); - - describe('# createNewOrSelectAWSProfile check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| create profile or use existing profile decision by user', () => { - // setup - const listOfProfiles = ['ask', 'aws', 'lambda']; - inquirer.prompt.resolves({ chosenProfile: 'lambda' }); - - // call - ui.createNewOrSelectAWSProfile(listOfProfiles, (response) => { - // verify - expect(response).equal('lambda'); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); - - describe('# createNewProfile check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| valid profile name entered by user', () => { - // setup - inquirer.prompt.resolves({ profile: 'lambda' }); - - // call - ui.createNewProfile((error, response) => { - // verify - expect(error).equal(null); - expect(response).equal('lambda'); - }); - }); - - it('| invalid profile name entered by user', () => { - // setup - sinon.stub(profileHelper, 'askProfileSyntaxValidation').returns(false); - inquirer.prompt.resolves({ profile: ' %*^@&!' }); - - // call - ui.createNewProfile((error, response) => { - // verify - expect(error).equal(messages.PROFILE_NAME_VALIDATION_ERROR); - expect(response).equal(undefined); - }); - }); - - afterEach(() => { - sinon.restore(); - }); - }); - - describe('# chooseVendorId check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| vendor selection by user', () => { - // setup - const VENDOR_PAGE_SIZE = 50; - const vendorInfo = [ - { - vendorName: 'vendor_name1', - vendorId: ' vendor_id1' - }, - { - vendorName: 'vendor_name2', - vendorId: ' vendor_id2' - } - ]; - inquirer.prompt.resolves({ selectedVendor: vendorInfo[0].vendorId }); - - // call - ui.chooseVendorId(VENDOR_PAGE_SIZE, vendorInfo, (response) => { - // verify - expect(response).equal('vendor_id1'); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - sinon.restore(); - }); - }); - - describe('# createOrUpdateProfile check', () => { - const LIST_PAGE_SIZE = 50; - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - sinon.stub(profileHelper, 'askProfileSyntaxValidation'); - }); - - it('| create a new ASK profile', () => { - // setup - const listOfProfiles = [ - 'askProfile1', - 'askProfile2' - ]; - const createNewProfile = 'Create new profile'; - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); - inquirer.prompt.resolves({ profile: createNewProfile }); - - // call - ui.createOrUpdateProfile(LIST_PAGE_SIZE, listOfProfiles, (error) => { - // verify - expect(error).to.equal(null); - }); - }); - - it('| update an existing ASK profile by user', () => { - // setup - const listOfProfiles = [ - 'askProfile1', - 'askProfile2', - '#$*%#$(%$43' - ]; - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(true); - inquirer.prompt.resolves({ profile: listOfProfiles[1] }); - - // call - ui.createOrUpdateProfile(LIST_PAGE_SIZE, listOfProfiles, (error, response) => { - // verify - expect(error).to.equal(null); - expect(response).equal('askProfile2'); - }); - }); - - it('| invalid profile name read from config file', (done) => { - // setup - const listOfProfiles = [ - 'askProfile1', - '#$*%#$(%$43' - ]; - profileHelper.askProfileSyntaxValidation.withArgs(sinon.match.string).returns(false); - inquirer.prompt.resolves({ profile: listOfProfiles[1] }); - - // call - ui.createOrUpdateProfile(LIST_PAGE_SIZE, listOfProfiles, (error, response) => { - // verify - expect(error).equal(messages.PROFILE_NAME_VALIDATION_ERROR); - expect(response).to.equal(undefined); - done(); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - sinon.restore(); - }); - }); - - describe('# requestAwsProfileName check', () => { - beforeEach(() => { - sinon.stub(inquirer, 'prompt'); - }); - - it('| Aws profile name entered by user', (done) => { - // setup - inquirer.prompt.resolves({ awsProfileName: 'awsProfile' }); - - // call - ui.requestAwsProfileName([], (response) => { - // verify - expect(response).equal('awsProfile'); - done(); - }); - }); - - afterEach(() => { - inquirer.prompt.restore(); - }); - }); -}); diff --git a/test/unit/commands/util/upgrade-to-v2/helper-test.js b/test/unit/commands/util/upgrade-to-v2/helper-test.js new file mode 100644 index 00000000..8c065b2f --- /dev/null +++ b/test/unit/commands/util/upgrade-to-v2/helper-test.js @@ -0,0 +1,377 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const fs = require('fs-extra'); +const path = require('path'); + +const helper = require('@src/commands/util/upgrade-to-v2/helper'); +const SkillMetadataController = require('@src/controllers/skill-metadata-controller'); +const Messenger = require('@src/view/messenger'); + +describe('Commands upgrade-to-v2 test - helper test', () => { + const TEST_PROFILE = 'default'; + const TEST_DO_DEBUG = false; + const TEST_SKILL_ID = 'skillId'; + const TEST_CODE_URI = 'codeUri'; + const TEST_RUNTIME = 'runtime'; + const TEST_HANDLER = 'handler'; + const TEST_ROOT_PATH = 'rootPath'; + + describe('# test helper method - extractUpgradeInformation', () => { + const TEST_V1_CONFIG_PATH = 'v1ConfigPath'; + const formV1Config = (skillId, isHosted, lambdaResources) => { + const result = { deploy_settings: {} }; + result.deploy_settings[TEST_PROFILE] = { + skill_id: skillId, + alexaHosted: { + isAlexaHostedSkill: isHosted + }, + resources: { + lambda: { + lambdaResources + } + } + }; + return result; + }; + + beforeEach(() => { + sinon.stub(path, 'join').returns(TEST_V1_CONFIG_PATH); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readJsonSync').returns({}); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| v1 project file does not exist, expect throw error', () => { + // setup + fs.existsSync.returns(false); + // call + try { + helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + } catch (testError) { + // verify + expect(testError.message).equal('Failed to find ask-cli v1 project. ' + + 'Please make sure this command is called at the root of the skill project.'); + } + }); + + it('| skill ID does not exist, expect throw error message', () => { + // setup + fs.readJsonSync.returns(formV1Config(' ', false, {})); + // call + try { + helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + } catch (testError) { + // verify + expect(testError.message).equal(`Failed to find skill_id for profile [${TEST_PROFILE}]. \ +If the skill has never been deployed in v1 ask-cli, please start from v2 structure.`); + } + }); + + it('| skill project is alexa hosted skill, expect quit with not support message', () => { + // setup + fs.readJsonSync.returns(formV1Config(TEST_SKILL_ID, true, {})); + // call + try { + helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + } catch (testError) { + // verify + expect(testError.message).equal('Alexa Hosted Skill is currently not supported to upgrade.'); + } + }); + + describe('test helper method - extractUpgradeInformation : _validateLambdaResource', () => { + [ + { + testCase: '| validate lambda resources fails at alexaUsage, expect throw error message', + lambdas: [ + { alexaUsage: [] } + ], + expectError: 'Please make sure your alexaUsage is not empty.' + }, + { + testCase: '| validate lambda resources fails at codeUri, expect throw error message', + lambdas: [ + { + alexaUsage: ['custom/default'], + codeUri: ' ' + } + ], + expectError: 'Please make sure your codeUri is set to the path of your Lambda code.' + }, + { + testCase: '| validate lambda resources fails at runtime, expect throw error message', + lambdas: [ + { + alexaUsage: ['custom/default'], + codeUri: TEST_CODE_URI, + runtime: ' ' + } + ], + expectError: `Please make sure your runtime for codeUri ${TEST_CODE_URI} is set.` + }, + { + testCase: '| validate lambda resources fails at handler, expect throw error message', + lambdas: [ + { + alexaUsage: ['custom/default'], + codeUri: TEST_CODE_URI, + runtime: TEST_RUNTIME, + handler: '' + } + ], + expectError: `Please make sure your handler for codeUri ${TEST_CODE_URI} is set.` + } + ].forEach(({ testCase, lambdas, expectError }) => { + it(testCase, () => { + // setup + fs.readJsonSync.returns(formV1Config(TEST_SKILL_ID, false, lambdas)); + // call + try { + helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + } catch (testError) { + // verify + expect(testError.message).equal(expectError); + } + }); + }); + }); + + describe('test helper method - extractUpgradeInformation : _collectLambdaMapFromResource', () => { + let warnStub; + + beforeEach(() => { + warnStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + warn: warnStub + }); + }); + + it('| when no Lambda ARN exists, skip the upgrade with a warning message', () => { + // setup + const TEST_LAMBDAS = [ + { + alexaUsage: ['custom/default'], + codeUri: TEST_CODE_URI, + runtime: TEST_RUNTIME, + handler: TEST_HANDLER + } + ]; + fs.readJsonSync.returns(formV1Config(TEST_SKILL_ID, false, TEST_LAMBDAS)); + // call + try { + const info = helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + // verify + expect(warnStub.args[0][0]).equal('Skip Lambda resource with alexaUsage "custom/default" since this Lambda is not deployed.'); + expect(info).deep.equal({ + skillId: TEST_SKILL_ID, + lambdaResources: {} + }); + } catch (testError) { + expect(testError).equal(undefined); + } + }); + + it('| when no Lambda ARN exists, skip the upgrade with a warning message', () => { + // setup + const TEST_LAMBDAS = [ + { + alexaUsage: ['custom/default'], + codeUri: TEST_CODE_URI, + runtime: TEST_RUNTIME, + handler: TEST_HANDLER + } + ]; + fs.readJsonSync.returns(formV1Config(TEST_SKILL_ID, false, TEST_LAMBDAS)); + // call + try { + const info = helper.extractUpgradeInformation(TEST_ROOT_PATH, TEST_PROFILE); + // verify + expect(warnStub.args[0][0]).equal('Skip Lambda resource with alexaUsage "custom/default" since this Lambda is not deployed.'); + expect(info).deep.equal({ + skillId: TEST_SKILL_ID, + lambdaResources: {} + }); + } catch (testError) { + expect(testError).equal(undefined); + } + }); + }); + }); + + describe('# test helper method - previewUpgrade', () => { + afterEach(() => { + sinon.restore(); + }); + + it('| skillCodeController buildSkillCode fails, expect callback error', (done) => { + // setup + sinon.stub(SkillCodeController.prototype, 'buildCode').callsArgWith(0, 'error'); + // call + helper.buildSkillCode(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal('error'); + expect(res).equal(undefined); + done(); + }); + }); + + it('| skillCodeController buildSkillCode passes, expect no error callback', (done) => { + // setup + sinon.stub(SkillCodeController.prototype, 'buildCode').callsArgWith(0); + // call + helper.buildSkillCode(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal(null); + expect(res).equal(undefined); + done(); + }); + }); + }); + + describe('# test helper method - moveOldProjectToLegacyFolder', () => { + afterEach(() => { + sinon.restore(); + }); + + it('| skillInfraController deploySkillInfrastructure fails, expect callback error', (done) => { + // setup + sinon.stub(SkillInfrastructureController.prototype, 'deployInfrastructure').callsArgWith(0, 'error'); + // call + helper.deploySkillInfrastructure(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal('error'); + expect(res).equal(undefined); + done(); + }); + }); + + it('| skillInfraController deploySkillInfrastructure passes, expect no error callback', (done) => { + // setup + sinon.stub(SkillInfrastructureController.prototype, 'deployInfrastructure').callsArgWith(0); + // call + helper.deploySkillInfrastructure(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal(undefined); + expect(res).equal(undefined); + done(); + }); + }); + }); + + describe('# test helper method - createV2ProjectSkeleton', () => { + let infoStub; + beforeEach(() => { + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub, + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| skillMetaController enableSkill fails, expect callback error', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0, 'error'); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err) => { + expect(err).equal('error'); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + + it('| skillMetaController enableSkill passes, expect no error callback', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal(undefined); + expect(res).equal(undefined); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + }); + + describe('# test helper method - downloadSkillPackage', () => { + let infoStub; + beforeEach(() => { + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub, + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| skillMetaController enableSkill fails, expect callback error', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0, 'error'); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err) => { + expect(err).equal('error'); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + + it('| skillMetaController enableSkill passes, expect no error callback', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal(undefined); + expect(res).equal(undefined); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + }); + + describe('# test helper method - handleExistingLambdaCode', () => { + let infoStub; + beforeEach(() => { + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub, + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| skillMetaController enableSkill fails, expect callback error', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0, 'error'); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err) => { + expect(err).equal('error'); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + + it('| skillMetaController enableSkill passes, expect no error callback', (done) => { + // setup + sinon.stub(SkillMetadataController.prototype, 'enableSkill').callsArgWith(0); + // call + helper.enableSkill(TEST_PROFILE, TEST_DO_DEBUG, (err, res) => { + // verify + expect(err).equal(undefined); + expect(res).equal(undefined); + expect(infoStub.args[0][0]).equal('\n==================== Enable Skill ===================='); + done(); + }); + }); + }); +}); diff --git a/test/unit/controller/authorization-controller/index-test.js b/test/unit/controller/authorization-controller/index-test.js new file mode 100644 index 00000000..ef014c1d --- /dev/null +++ b/test/unit/controller/authorization-controller/index-test.js @@ -0,0 +1,485 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const queryString = require('querystring'); +const portscanner = require('portscanner'); +const proxyquire = require('proxyquire'); + +const httpClient = require('@src/clients/http-client'); +const LWAClient = require('@src/clients/lwa-auth-code-client'); +const AuthorizationController = require('@src/controllers/authorization-controller'); +const messages = require('@src/controllers/authorization-controller/messages'); +const AppConfig = require('@src/model/app-config'); +const CONSTANTS = require('@src/utils/constants'); +const Messenger = require('@src/view/messenger'); +const SpinnerView = require('@src/view/spinner-view'); +const LocalHostServer = require('@src/controllers/authorization-controller/server'); + +describe('Controller test - Authorization controller test', () => { + const DEFAULT_CLIENT_ID = CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_ID; + const DEFAULT_SCOPE = CONSTANTS.LWA.DEFAULT_SCOPES; + const authorizePath = CONSTANTS.LWA.DEFAULT_AUTHORIZE_PATH; + const authorizeHost = CONSTANTS.LWA.DEFAULT_AUTHORIZE_HOST; + const TEST_STATE = 'state'; + const TEST_PROFILE = 'testProfile'; + const TEST_ENVIRONMENT_PROFILE = CONSTANTS.PLACEHOLDER.ENVIRONMENT_VAR.PROFILE_NAME; + const TEST_DO_DEBUG = false; + const TEST_NEED_BROWSER = false; + const TEST_ERROR_MESSAGE = 'errorMessage'; + const TEST_AUTH_CODE = 'authCode'; + const TEST_PORT = CONSTANTS.LWA.LOCAL_PORT; + const TEST_RESPONSE = { + body: { + access_token: 'access_token', + refresh_token: 'refresh_token', + expires_in: 3600, + expires_at: new Date('05 October 2011 14:48 UTC').toISOString() + } + }; + const currentDatePlusHalfAnHour = new Date(new Date(Date.now()).getTime() + (0.5 * 60 * 60 * 1000)).toISOString(); + const VALID_ACCESS_TOKEN = { + access_token: 'accessToken', + refresh_token: 'refreshToken', + token_type: 'bearer', + expires_in: 3600, + expires_at: currentDatePlusHalfAnHour + }; + const TEST_CONFIG = { + askProfile: TEST_PROFILE, + needBrowser: TEST_NEED_BROWSER, + doDebug: TEST_DO_DEBUG, + auth_client_type: 'LWA', + state: TEST_STATE, + }; + + describe('# test _getAuthClientInstance', () => { + it('| returns undefined', () => { + // setup + const authorizationController = new AuthorizationController({ auth_client_type: 'UNKNOWN' }); + + // call and verify + expect(authorizationController.oauthClient).eq(undefined); + }); + }); + + describe('# test getAuthorizeUrl', () => { + it('| returns valid authorization url', () => { + // setup + const authorizationController = new AuthorizationController(TEST_CONFIG); + const queryParams = { + response_type: 'code', + client_id: DEFAULT_CLIENT_ID, + state: TEST_STATE, + scope: DEFAULT_SCOPE + }; + const uri = `${authorizeHost}${authorizePath}?${queryString.stringify(queryParams)}`; + + const url = authorizationController.getAuthorizeUrl(); + // call and verify + expect(url).eq(uri); + }); + }); + + describe('# test getAccessTokenUsingAuthCode', () => { + beforeEach(() => { + sinon.stub(httpClient, 'request'); + }); + + it('| returns valid access token', (done) => { + // setup + httpClient.request.callsArgWith(3, null, TEST_RESPONSE); + const authorizationController = new AuthorizationController(TEST_CONFIG); + + // call + authorizationController.getAccessTokenUsingAuthCode(TEST_AUTH_CODE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_RESPONSE.body); + done(); + }); + }); + + it('| returns error', (done) => { + // setup + httpClient.request.callsArgWith(3, TEST_ERROR_MESSAGE); + const authorizationController = new AuthorizationController(TEST_CONFIG); + + // call + authorizationController.getAccessTokenUsingAuthCode(TEST_AUTH_CODE, (error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + afterEach(() => { + sinon.restore(); + }); + }); + + describe('# test tokenRefreshAndRead', () => { + let getTokenStub; + const TEST_ENV_ACCESS_TOKEN = 'envAccessToken'; + const TEST_ENV_REFRESH_TOKEN = 'envRefreshToken'; + const authorizationController = new AuthorizationController(TEST_CONFIG); + beforeEach(() => { + getTokenStub = sinon.stub(); + sinon.stub(AppConfig, 'getInstance').returns({ + getToken: getTokenStub + }); + }); + + describe('# returns valid token', () => { + afterEach(() => { + sinon.restore(); + delete process.env.ASK_REFRESH_TOKEN; + delete process.env.ASK_ACCESS_TOKEN; + }); + + it('| non-environment profile, expired access token', (done) => { + // setup + sinon.stub(AuthorizationController.prototype, '_getRefreshTokenAndUpdateConfig').callsArgWith(1, null, VALID_ACCESS_TOKEN); + getTokenStub.withArgs(TEST_PROFILE).returns(TEST_RESPONSE.body); + + // call + authorizationController.tokenRefreshAndRead(TEST_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(VALID_ACCESS_TOKEN); + done(); + }); + }); + + it('| non-environment profile, valid access token', (done) => { + // setup + getTokenStub.withArgs(TEST_PROFILE).returns(VALID_ACCESS_TOKEN); + + // call + authorizationController.tokenRefreshAndRead(TEST_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(VALID_ACCESS_TOKEN.access_token); + done(); + }); + }); + + it('| environment profile, valid refresh token', (done) => { + // setup + process.env.ASK_REFRESH_TOKEN = TEST_ENV_REFRESH_TOKEN; + sinon.stub(AuthorizationController.prototype, '_getRefreshTokenAndUpdateConfig').callsArgWith(1, null, VALID_ACCESS_TOKEN); + + // call + authorizationController.tokenRefreshAndRead(TEST_ENVIRONMENT_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(VALID_ACCESS_TOKEN); + done(); + }); + }); + + it('| environment profile, invalid refresh token, valid access token', (done) => { + // setup + process.env.ASK_ACCESS_TOKEN = TEST_ENV_ACCESS_TOKEN; + // call + authorizationController.tokenRefreshAndRead(TEST_ENVIRONMENT_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_ENV_ACCESS_TOKEN); + done(); + }); + }); + }); + + describe('# returns error', () => { + afterEach(() => { + sinon.restore(); + delete process.env.ASK_REFRESH_TOKEN; + delete process.env.ASK_ACCESS_TOKEN; + }); + + it('| non-environment profile, expired access token, _getRefreshTokenAndUpdateConfig fails', (done) => { + // setup + sinon.stub(AuthorizationController.prototype, '_getRefreshTokenAndUpdateConfig').callsArgWith(1, TEST_ERROR_MESSAGE); + getTokenStub.withArgs(TEST_PROFILE).returns(TEST_RESPONSE.body); + + // call + authorizationController.tokenRefreshAndRead(TEST_PROFILE, (error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| environment profile, valid refresh token, _getRefreshTokenAndUpdateConfig fails', (done) => { + // setup + process.env.ASK_REFRESH_TOKEN = TEST_ENV_REFRESH_TOKEN; + sinon.stub(AuthorizationController.prototype, '_getRefreshTokenAndUpdateConfig').callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + authorizationController.tokenRefreshAndRead(TEST_ENVIRONMENT_PROFILE, (error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).to.deep.eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| environment profile, invalid refresh token, invalid access token', (done) => { + // call + authorizationController.tokenRefreshAndRead(TEST_ENVIRONMENT_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(messages.ASK_ENV_VARIABLES_ERROR_MESSAGE); + expect(accessToken).eq(undefined); + done(); + }); + }); + }); + }); + + describe('# test _getRefreshTokenAndUpdateConfig', () => { + let setTokenStub, writeStub; + const authorizationController = new AuthorizationController(TEST_CONFIG); + + beforeEach(() => { + setTokenStub = sinon.stub(); + writeStub = sinon.stub(); + sinon.stub(AppConfig, 'getInstance').returns({ + setToken: setTokenStub, + write: writeStub + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| returns valid access token and updates config', (done) => { + // setup + sinon.stub(AuthorizationController.prototype, '_getRefreshToken').callsArgWith(1, null, TEST_RESPONSE.body); + + // call + authorizationController._getRefreshTokenAndUpdateConfig(TEST_PROFILE, (error, accessToken) => { + // verify + expect(setTokenStub.args[0][0]).eq(TEST_PROFILE); + expect(setTokenStub.args[0][1]).to.deep.eq(TEST_RESPONSE.body); + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_RESPONSE.body.access_token); + done(); + }); + }); + + it('| returns error', (done) => { + // setup + sinon.stub(AuthorizationController.prototype, '_getRefreshToken').callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + authorizationController._getRefreshTokenAndUpdateConfig(TEST_PROFILE, (error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(undefined); + done(); + }); + }); + }); + + describe('# test _getRefreshToken', () => { + let getTokenStub; + const authorizationController = new AuthorizationController(TEST_CONFIG); + + beforeEach(() => { + sinon.stub(httpClient, 'request'); + getTokenStub = sinon.stub(); + sinon.stub(AppConfig, 'getInstance').returns({ + getToken: getTokenStub + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| returns valid access token', (done) => { + // setup + httpClient.request.callsArgWith(3, null, TEST_RESPONSE); + getTokenStub.withArgs(TEST_PROFILE).returns(TEST_RESPONSE.body); + + // call + authorizationController._getRefreshToken(TEST_PROFILE, (error, accessToken) => { + // verify + expect(error).eq(null); + expect(accessToken).to.deep.eq(TEST_RESPONSE.body); + done(); + }); + }); + + it('| returns error', (done) => { + // setup + httpClient.request.callsArgWith(3, TEST_ERROR_MESSAGE); + getTokenStub.withArgs(TEST_PROFILE).returns(TEST_RESPONSE.body); + + // call + authorizationController._getRefreshToken(TEST_PROFILE, (error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + }); + + describe('# test getTokensByListeningOnPort', () => { + let proxyHelper, opnStub, infoStub; + + beforeEach(() => { + sinon.stub(httpClient, 'request'); + sinon.stub(portscanner, 'checkPortStatus'); + infoStub = sinon.stub(); + sinon.stub(Messenger, 'getInstance').returns({ + info: infoStub + }); + opnStub = sinon.stub(); + proxyHelper = proxyquire('@src/controllers/authorization-controller', { + opn: opnStub + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('| returns error, from postScanner library', (done) => { + // setup + const authorizationController = new AuthorizationController(TEST_CONFIG); + portscanner.checkPortStatus.callsArgWith(1, TEST_ERROR_MESSAGE); + + // call + authorizationController.getTokensByListeningOnPort((error, response) => { + // verify + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(undefined); + done(); + }); + }); + + it('| returns error, port is open', (done) => { + // setup + const authorizationController = new AuthorizationController(TEST_CONFIG); + portscanner.checkPortStatus.callsArgWith(1, null, 'open'); + + // call + authorizationController.getTokensByListeningOnPort((error, response) => { + // verify + expect(error).eq(messages.PORT_OCCUPIED_WARN_MESSAGE); + expect(response).eq(undefined); + done(); + }); + }); + + it('| returns error, port is open but listening response on port fails', (done) => { + // setup + sinon.stub(proxyHelper.prototype, '_listenResponseFromLWA').callsArgWith(1, TEST_ERROR_MESSAGE); + portscanner.checkPortStatus.callsArgWith(1, null, 'closed'); + proxyHelper.prototype.oauthClient = new LWAClient(TEST_CONFIG); + // call + proxyHelper.prototype.getTokensByListeningOnPort((error, response) => { + // verify + expect(infoStub.args[0][0]).eq(messages.AUTH_MESSAGE); + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(undefined); + done(); + }); + }); + + it('| returns error, port is open but LWA client getAccessToken fails', (done) => { + // setup + sinon.stub(proxyHelper.prototype, '_listenResponseFromLWA').callsArgWith(1, null, TEST_AUTH_CODE); + portscanner.checkPortStatus.callsArgWith(1, null, 'closed'); + proxyHelper.prototype.oauthClient = new LWAClient(TEST_CONFIG); + httpClient.request.callsArgWith(3, TEST_ERROR_MESSAGE); + // call + proxyHelper.prototype.getTokensByListeningOnPort((error, response) => { + // verify + expect(infoStub.args[0][0]).eq(messages.AUTH_MESSAGE); + expect(error).eq(TEST_ERROR_MESSAGE); + expect(response).eq(TEST_ERROR_MESSAGE); + done(); + }); + }); + + it('| returns valid token', (done) => { + // setup + sinon.stub(proxyHelper.prototype, '_listenResponseFromLWA').callsArgWith(1, null, TEST_AUTH_CODE); + portscanner.checkPortStatus.callsArgWith(1, null, 'closed'); + proxyHelper.prototype.oauthClient = new LWAClient(TEST_CONFIG); + httpClient.request.callsArgWith(3, null, TEST_RESPONSE); + // call + proxyHelper.prototype.getTokensByListeningOnPort((error, accessToken) => { + // verify + expect(infoStub.args[0][0]).eq(messages.AUTH_MESSAGE); + expect(error).eq(null); + expect(accessToken).eq(TEST_RESPONSE.body); + done(); + }); + }); + }); + + describe('# test _listenResponseFromLWA', () => { + const authorizationController = new AuthorizationController(TEST_CONFIG); + + afterEach(() => { + sinon.restore(); + }); + + it('| should create a server and start spinner to indicate listening on port', () => { + // setup + const socket = { + unref: () => {} + }; + const spinnerStartStub = sinon.stub(SpinnerView.prototype, 'start'); + const listenStub = sinon.stub(LocalHostServer.prototype, 'listen').callsArgWith(0); + const registerEventStub = sinon.stub(LocalHostServer.prototype, 'registerEvent').callsArgWith(1, socket); + const createStub = sinon.stub(LocalHostServer.prototype, 'create'); + + // call + authorizationController._listenResponseFromLWA(TEST_PORT, () => {}); + + // verify + expect(registerEventStub.callCount).eq(1); + expect(spinnerStartStub.args[0][0]).eq(` Listening on http://localhost:${TEST_PORT}...`); + expect(spinnerStartStub.callCount).eq(1); + expect(listenStub.callCount).eq(1); + expect(createStub.callCount).eq(1); + }); + + it('| local host server returns error', (done) => { + // setup + const spinnerTerminateStub = sinon.stub(SpinnerView.prototype, 'terminate'); + sinon.stub(LocalHostServer.prototype, 'create').callsArgWith(0, TEST_ERROR_MESSAGE); + + // call + authorizationController._listenResponseFromLWA(TEST_PORT, (err, authCode) => { + // verify + expect(spinnerTerminateStub.callCount).eq(1); + expect(err).eq(TEST_ERROR_MESSAGE); + expect(authCode).eq(undefined); + done(); + }); + }); + + it('| local server returns valid authCode', (done) => { + // setup + const spinnerTerminateStub = sinon.stub(SpinnerView.prototype, 'terminate'); + sinon.stub(LocalHostServer.prototype, 'create').callsArgWith(0, null, TEST_AUTH_CODE); + + // call + authorizationController._listenResponseFromLWA(TEST_PORT, (err, authCode) => { + // verify + expect(spinnerTerminateStub.callCount).eq(1); + expect(err).eq(null); + expect(authCode).eq(TEST_AUTH_CODE); + done(); + }); + }); + }); +}); diff --git a/test/unit/controller/authorization-controller/server-test.js b/test/unit/controller/authorization-controller/server-test.js new file mode 100644 index 00000000..bc3e2f52 --- /dev/null +++ b/test/unit/controller/authorization-controller/server-test.js @@ -0,0 +1,125 @@ +const { expect } = require('chai'); +const http = require('http'); +const sinon = require('sinon'); +const url = require('url'); + +const LocalServer = require('@src/controllers/authorization-controller/server'); +const CONSTANTS = require('@src/utils/constants'); +const messages = require('@src/controllers/authorization-controller/messages'); + +describe('# Server test - Local server test', () => { + const TEST_PORT = CONSTANTS.LWA.LOCAL_PORT; + const TEST_EVENT = 'testEvent'; + const listenStub = sinon.stub(); + const onStub = sinon.stub(); + const closeStub = sinon.stub(); + const unrefStub = sinon.stub(); + + const TEST_SERVER = { + listen: listenStub, + on: onStub, + close: closeStub, + unref: unrefStub + }; + + afterEach(() => { + sinon.restore(); + }); + + it('| test server methods', () => { + // setup + const httpStub = sinon.stub(http, 'createServer').returns(TEST_SERVER); + const localServer = new LocalServer(TEST_PORT); + + // call + localServer.create(() => {}); + localServer.listen(() => {}); + localServer.registerEvent(TEST_EVENT, () => {}); + localServer.destroy(); + + // verify + expect(httpStub.callCount).eq(1); + expect(listenStub.callCount).eq(1); + expect(onStub.callCount).eq(1); + expect(closeStub.callCount).eq(1); + expect(unrefStub.callCount).eq(1); + expect(localServer.server).to.deep.eq(TEST_SERVER); + }); + + it('| test _defaultServerCallback | valid authCode retrieved', () => { + // setup + const serverDestroyStub = sinon.stub(LocalServer.prototype, 'destroy'); + const requestDestroyStub = sinon.stub(); + const localServer = new LocalServer(TEST_PORT); + const callback = (error, authCode) => { + if (error) { return error; } + return authCode; + }; + const request = { + url: '/cb?code', + socket: { + destroy: requestDestroyStub + } + }; + const requestQuery = { + query: { + code: 'authCode' + } + }; + sinon.stub(url, 'parse').returns(requestQuery); + const endStub = sinon.stub(); + const response = { + on: sinon.stub().callsArgWith(1), + end: endStub + }; + + // call + const defaultServerCallback = localServer._defaultServerCallback(callback); + defaultServerCallback(request, response); + + // verify + expect(serverDestroyStub.callCount).eq(1); + expect(requestDestroyStub.callCount).eq(1); + expect(endStub.callCount).eq(1); + expect(endStub.args[0][0]).eq(messages.ASK_SIGN_IN_SUCCESS_MESSAGE); + }); + + it('| test _defaultServerCallback | returns error', () => { + // setup + const serverDestroyStub = sinon.stub(LocalServer.prototype, 'destroy'); + const requestDestroyStub = sinon.stub(); + const localServer = new LocalServer(TEST_PORT); + const callback = (error, authCode) => { + if (error) { return error; } + return authCode; + }; + const request = { + url: '/cb?error', + socket: { + destroy: requestDestroyStub + } + }; + const requestQuery = { + query: { + error: 'error', + error_description: 'errorDescription' + } + }; + sinon.stub(url, 'parse').returns(requestQuery); + const endStub = sinon.stub(); + const response = { + on: sinon.stub().callsArgWith(1), + end: endStub + }; + + // call + const defaultServerCallback = localServer._defaultServerCallback(callback); + defaultServerCallback(request, response); + + // verify + expect(serverDestroyStub.callCount).eq(1); + expect(requestDestroyStub.callCount).eq(1); + expect(endStub.callCount).eq(1); + expect(endStub.args[0][0]).eq(`Error: ${requestQuery.query.error}\nError description: ${requestQuery.query.error_description}`); + }); +}); diff --git a/test/unit/controller/skill-infrastructure-controller-test.js b/test/unit/controller/skill-infrastructure-controller-test.js index c56918c7..cb9ecd74 100644 --- a/test/unit/controller/skill-infrastructure-controller-test.js +++ b/test/unit/controller/skill-infrastructure-controller-test.js @@ -11,7 +11,7 @@ const SkillInfrastructureController = require('@src/controllers/skill-infrastruc const DeployDelegate = require('@src/controllers/skill-infrastructure-controller/deploy-delegate'); const MultiTasksView = require('@src/view/multi-tasks-view'); const jsonView = require('@src/view/json-view'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const hashUtils = require('@src/utils/hash-utils'); const CONSTANTS = require('@src/utils/constants'); @@ -350,7 +350,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| manifest update correctly but hash fails, expect error called back', (done) => { // setup - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, 'hash error'); // call skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { @@ -367,7 +367,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| manifest update correctly but hash is same, expect called back with nothing', (done) => { // setup ResourcesConfig.getInstance().setSkillMetaLastDeployHash(TEST_PROFILE, 'TEST_HASH'); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); // call skillInfraController.updateSkillManifestWithDeployResult(TEST_DEPLOY_RESULT, (err, res) => { @@ -382,7 +382,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| manifest update correctly but skill manifest update fails, expect update error called back', (done) => { // setup - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0, 'update error'); // call @@ -398,7 +398,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| manifest update correctly, expect success message and new hash set', (done) => { // setup - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(hashUtils, 'getHash').callsArgWith(1, null, 'TEST_HASH'); sinon.stub(SkillInfrastructureController.prototype, '_ensureSkillManifestGotUpdated').callsArgWith(0); // call @@ -569,7 +569,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| SMAPI update manifest connection fails, expect error called back', (done) => { // setup - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, 'error'); // call skillInfraController._ensureSkillManifestGotUpdated((err, res) => { @@ -588,7 +588,7 @@ describe('Controller test - skill infrastructure controller test', () => { message: 'unauthrized' } }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); // call skillInfraController._ensureSkillManifestGotUpdated((err, res) => { @@ -604,7 +604,7 @@ describe('Controller test - skill infrastructure controller test', () => { const TEST_SMAPI_RESPONSE = { statusCode: 202 }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); sinon.stub(SkillInfrastructureController.prototype, '_pollSkillStatus').callsArgWith(2, 'poll error'); // call @@ -624,7 +624,7 @@ describe('Controller test - skill infrastructure controller test', () => { const TEST_POLL_RESPONSE = { body: 'invalid' }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); sinon.stub(SkillInfrastructureController.prototype, '_pollSkillStatus').callsArgWith(2, null, TEST_POLL_RESPONSE); // call @@ -650,7 +650,7 @@ describe('Controller test - skill infrastructure controller test', () => { } } }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); sinon.stub(SkillInfrastructureController.prototype, '_pollSkillStatus').callsArgWith(2, null, TEST_POLL_RESPONSE); // call @@ -676,7 +676,7 @@ describe('Controller test - skill infrastructure controller test', () => { } } }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); sinon.stub(SkillInfrastructureController.prototype, '_pollSkillStatus').callsArgWith(2, null, TEST_POLL_RESPONSE); // call @@ -699,7 +699,7 @@ describe('Controller test - skill infrastructure controller test', () => { it('| poll skill status but error happens when polling status', (done) => { // setup - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, 'error'); // call skillInfraController._pollSkillStatus(testSmapiClient, TEST_SKILL_ID, (err, res) => { @@ -718,7 +718,7 @@ describe('Controller test - skill infrastructure controller test', () => { message: 'unauthrized' } }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); // call skillInfraController._pollSkillStatus(testSmapiClient, TEST_SKILL_ID, (err, res) => { @@ -741,7 +741,7 @@ describe('Controller test - skill infrastructure controller test', () => { } } }; - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); sinon.stub(httpClient, 'request').callsArgWith(3, null, TEST_SMAPI_RESPONSE); // call skillInfraController._pollSkillStatus(testSmapiClient, TEST_SKILL_ID, (err, res) => { diff --git a/test/unit/controller/skill-metadata-controller-test.js b/test/unit/controller/skill-metadata-controller-test.js index 0b2de5f3..38d37bbb 100644 --- a/test/unit/controller/skill-metadata-controller-test.js +++ b/test/unit/controller/skill-metadata-controller-test.js @@ -6,7 +6,7 @@ const jsonView = require('@src/view/json-view'); const ResourcesConfig = require('@src/model/resources-config'); const httpClient = require('@src/clients/http-client'); const SkillMetadataController = require('@src/controllers/skill-metadata-controller'); -const oauthWrapper = require('@src/utils/oauth-wrapper'); +const AuthorizationController = require('@src/controllers/authorization-controller'); const Manifest = require('@src/model/manifest'); const Messenger = require('@src/view/messenger'); const zipUtils = require('@src/utils/zip-utils'); @@ -169,7 +169,7 @@ describe('Controller test - skill metadata controller test', () => { beforeEach(() => { new Manifest(FIXTURE_MANIFEST_FILE_PATH); new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { @@ -683,7 +683,7 @@ describe('Controller test - skill metadata controller test', () => { beforeEach(() => { new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { @@ -748,7 +748,7 @@ describe('Controller test - skill metadata controller test', () => { beforeEach(() => { new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { @@ -814,7 +814,7 @@ describe('Controller test - skill metadata controller test', () => { const skillMetaController = new SkillMetadataController(TEST_CONFIGURATION); beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { @@ -880,7 +880,7 @@ describe('Controller test - skill metadata controller test', () => { beforeEach(() => { new ResourcesConfig(FIXTURE_RESOURCES_CONFIG_FILE_PATH); - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { @@ -946,7 +946,7 @@ describe('Controller test - skill metadata controller test', () => { const skillMetaController = new SkillMetadataController(TEST_CONFIGURATION); beforeEach(() => { - sinon.stub(oauthWrapper, 'tokenRefreshAndRead').callsArgWith(2); + sinon.stub(AuthorizationController.prototype, 'tokenRefreshAndRead').callsArgWith(1); }); afterEach(() => { diff --git a/test/unit/fixture/model/app-config-no-profiles.json b/test/unit/fixture/model/app-config-no-profiles.json new file mode 100644 index 00000000..43e0c99c --- /dev/null +++ b/test/unit/fixture/model/app-config-no-profiles.json @@ -0,0 +1,3 @@ +{ + "profiles": {} +} \ No newline at end of file diff --git a/test/unit/fixture/model/cli_config b/test/unit/fixture/model/cli_config new file mode 100644 index 00000000..7b07ed05 --- /dev/null +++ b/test/unit/fixture/model/cli_config @@ -0,0 +1,3 @@ +{ + "name": "ask-cli" +} \ No newline at end of file diff --git a/test/unit/model/abstract-config-file-test.js b/test/unit/model/abstract-config-file-test.js index 0cb155c9..ecd94b8a 100644 --- a/test/unit/model/abstract-config-file-test.js +++ b/test/unit/model/abstract-config-file-test.js @@ -11,6 +11,7 @@ describe('Model test - abstract config file test', () => { const FIXTURE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model'); const JSON_CONFIG_PATH = path.join(FIXTURE_PATH, 'json-config.json'); const YAML_CONFIG_PATH = path.join(FIXTURE_PATH, 'json-config-yaml.yaml'); + const CONFIG_FILE = path.join(FIXTURE_PATH, 'cli_config'); describe('# inspect correctness for constructor and content loading', () => { const NOT_EXISTING_CONFIG_PATH = path.join(FIXTURE_PATH, 'out-of-noWhere.json'); @@ -18,6 +19,21 @@ describe('Model test - abstract config file test', () => { const INVALID_YAML_CONFIG_PATH = path.join(FIXTURE_PATH, 'invalid-yaml.yaml'); const NOT_SUPPORTED_FILE_PATH = path.join(FIXTURE_PATH, 'unsupported-file-type.notsupport'); + it('| init with valid config file name, expect to create a ConfigFile instance successfully', () => { + let configContent, jsonContent; + try { + // call + configContent = new ConfigFile(CONFIG_FILE); + jsonContent = jsonfile.readFileSync(CONFIG_FILE); + } catch (err) { + expect(err).equal(null); + } + // verify + expect(configContent).to.be.instanceOf(ConfigFile); + expect(configContent.fileType).equal('JSON'); + expect(configContent.content).deep.eq(jsonContent); + }); + it('| init with valid JSON file path, expect to create a ConfigFile instance successfully', () => { let configContent, jsonContent; try { diff --git a/test/unit/model/app-config-test.js b/test/unit/model/app-config-test.js index 5f1408f2..f15161a1 100644 --- a/test/unit/model/app-config-test.js +++ b/test/unit/model/app-config-test.js @@ -1,5 +1,4 @@ -const expect = require('chai').expect; -const sinon = require('sinon'); +const { expect } = require('chai'); const path = require('path'); const fs = require('fs'); const jsonfile = require('jsonfile'); @@ -9,6 +8,7 @@ const AppConfig = require('@src/model/app-config'); describe('Model test - app config test', () => { const FIXTURE_PATH = path.join(process.cwd(), 'test', 'unit', 'fixture', 'model'); const APP_CONFIG_PATH = path.join(FIXTURE_PATH, 'app-config.json'); + const APP_CONFIG_NO_PROFILES_PATH = path.join(FIXTURE_PATH, 'app-config-no-profiles.json'); const YAML_APP_CONFIG_PATH = path.join(FIXTURE_PATH, 'app-config.yaml'); describe('# inspect correctness for constructor, getInstance and dispose', () => { @@ -96,26 +96,26 @@ describe('Model test - app config test', () => { { field: 'AwsProfile', profile: TEST_PROFILE, - newValue: "awsProfile new", - oldValue: "awsProfile" + newValue: 'awsProfile new', + oldValue: 'awsProfile' }, { field: 'Token', profile: TEST_PROFILE, - newValue: "token new", + newValue: 'token new', oldValue: { - "access_token": "accessToken", - "refresh_token": "refreshToken", - "token_type": "bearer", - "expires_in": 3600, - "expires_at": "expiresAt" + access_token: 'accessToken', + refresh_token: 'refreshToken', + token_type: 'bearer', + expires_in: 3600, + expires_at: 'expiresAt' } }, { field: 'VendorId', profile: TEST_PROFILE, - newValue: "vendorId new", - oldValue: "vendorId" + newValue: 'vendorId new', + oldValue: 'vendorId' } ].forEach(({ field, @@ -137,4 +137,28 @@ describe('Model test - app config test', () => { AppConfig.dispose(); }); }); + + describe('# inspect correctness of getProfilesList', () => { + it('| test with empty profiles config file, expect empty array', () => { + // setup + new AppConfig(APP_CONFIG_NO_PROFILES_PATH); + + // call & verify + expect(AppConfig.getInstance().getProfilesList().length).to.equal(0); + }); + + it('| test with valid profiles config file, expect array of objects', () => { + // setup + new AppConfig(APP_CONFIG_PATH); + + // call & verify + expect(AppConfig.getInstance().getProfilesList().length).to.equal(2); + expect(AppConfig.getInstance().getProfilesList()).to.deep.equal([{ askProfile: 'testProfile', awsProfile: 'awsProfile' }, + { askProfile: 'default', awsProfile: 'default' }]); + }); + + afterEach(() => { + AppConfig.dispose(); + }); + }); }); diff --git a/test/unit/run-test.js b/test/unit/run-test.js index 08818f1a..ca3b857f 100644 --- a/test/unit/run-test.js +++ b/test/unit/run-test.js @@ -15,17 +15,17 @@ require('module-alias/register'); '@test/unit/commands/abstract-command-test', '@test/unit/commands/api/publishing/withdraw/helper-test.js', '@test/unit/commands/api/validation/validate-skill/helper-test.js', - // command - init - '@test/unit/commands/init/ask-setup-helper-test', - '@test/unit/commands/init/aws-setup-wizard-test', - '@test/unit/commands/init/aws-setup-helper-test', - '@test/unit/commands/init/handler-test', - '@test/unit/commands/init/ui-test', - '@test/unit/commands/init/questions-test', // command - new '@test/unit/commands/v2new/index-test', '@test/unit/commands/v2new/ui-test', '@test/unit/commands/v2new/helper-test', + // command - configure + '@test/unit/commands/configure/index-test', + '@test/unit/commands/configure/ui-test', + '@test/unit/commands/configure/helper-test', + '@test/unit/commands/configure/questions-test', + '@test/unit/commands/configure/ask-profile-setup-helper-test', + '@test/unit/commands/configure/aws-profile-setup-helper-test', // command - deploy '@test/unit/commands/deploy/index-test', '@test/unit/commands/deploy/helper-test', @@ -36,6 +36,7 @@ require('module-alias/register'); '@test/unit/clients/http-client-test', '@test/unit/clients/smapi-client-test', '@test/unit/clients/git-client-test', + '@test/unit/clients/lwa-auth-code-client-test', '@test/unit/clients/aws-client/s3-client-test', '@test/unit/clients/aws-client/cloudformation-client-test', '@test/unit/clients/aws-client/aws-util-test', @@ -49,6 +50,8 @@ require('module-alias/register'); '@test/unit/model/yaml-parser-test', '@test/unit/model/regional-stack-file-test', // controller + '@test/unit/controller/authorization-controller/index-test', + '@test/unit/controller/authorization-controller/server-test', '@test/unit/controller/skill-metadata-controller-test', '@test/unit/controller/skill-code-controller-test', '@test/unit/controller/code-builder-test', @@ -65,6 +68,7 @@ require('module-alias/register'); '@test/unit/utils/zip-utils-test', '@test/unit/utils/hash-utils-test', '@test/unit/utils/retry-utility-test', + '@test/unit/utils/provider-chain-utils-test', // FUNCTION TEST diff --git a/test/unit/utils/lwa-test.js b/test/unit/utils/lwa-test.js deleted file mode 100644 index b00f7494..00000000 --- a/test/unit/utils/lwa-test.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict'; - -const expect = require('chai').expect; -const sinon = require('sinon'); -const lwa = require('../../../lib/utils/lwa'); -const portscanner = require('portscanner'); -const oauthWrapper = require('../../../lib/utils/oauth-wrapper'); -const http = require('http'); - -describe('Utils: lwa testing', () => { - describe('# _requestTokens', () => { - let sandbox; - - beforeEach(() => { - sandbox = sinon.sandbox.create(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('| should callback with validate tokens', () => { - const TOKEN = 'valid token'; - let OAuth = {}; - OAuth.authorizationCode = {}; - OAuth.authorizationCode.getToken = (tokenConfig, callback) => { - if (tokenConfig) { - callback(null, 'authCode'); - } - }; - OAuth.accessToken = {}; - OAuth.accessToken.create = () => { - let response = {}; - response.token = TOKEN; - return response; - }; - - lwa._requestTokens('authCode', 'fake_url', OAuth, (error, result) => { - expect(error).to.be.null; - expect(result).to.equal(TOKEN); - }); - - }); - - it('| should error out', () => { - let OAuth = {}; - OAuth.authorizationCode = {}; - OAuth.authorizationCode.getToken = (tokenConfig, callback) => { - if (tokenConfig) { - callback('error'); - } - }; - lwa._requestTokens('authCode', 'fake_url', OAuth, (error, result) => { - expect(error).to.include('Cannot obtain access token. '); - expect(result).to.be.undefined; - }); - }) - }); - - describe('# accessTokenGenerator', () => { - let sandbox; - - beforeEach(() => { - let OAuth = {}; - OAuth.authorizationCode = {}; - OAuth.authorizationCode.authorizeURL = (tokenConfig) => { - if (tokenConfig) { - return 'localhost'; - } - }; - - sandbox = sinon.sandbox.create(); - sandbox.stub(oauthWrapper, 'createOAuth').returns(OAuth); - sandbox.stub(lwa, '_getAuthCode'); - sandbox.stub(lwa, '_requestTokens'); - sandbox.stub(lwa, '_listenResponseFromLWA'); - sandbox.stub(portscanner, 'checkPortStatus'); - sandbox.stub(console, 'warn'); - sandbox.stub(console, 'info'); - sandbox.stub(console, 'log'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - - it('| should go through non-browser route to fetch the tokens', () => { - - lwa.accessTokenGenerator('credentials', 'scopes', 'state', false, () => { - }); - expect(console.log.called).to.be.true; - }); - - it('| should go through browser route and callback with an error', (done) => { - portscanner.checkPortStatus.callsArgWith(1, 'error'); - lwa.accessTokenGenerator('credentials', 'scopes', 'state', true, (error) => { - expect(error).to.equal('error'); - expect(lwa._listenResponseFromLWA.calledOnce).to.be.false; - done(); - }); - - }); - - it('| should use browser but find the port was open', () => { - portscanner.checkPortStatus.callsArgWith(1, null, 'open'); - lwa.accessTokenGenerator('credentials', 'scopes', 'state', true, () => {}); - expect(lwa._listenResponseFromLWA.calledOnce).to.be.false; - expect(console.warn.calledOnce).to.be.true; - expect(console.info.calledOnce).to.be.true; - }); - - it('| should go through browser but find the port was closed', () => { - portscanner.checkPortStatus.callsArgWith(1, null, 'closed'); - lwa.accessTokenGenerator('credentials', 'scopes', 'state', true, () => {}); - expect(console.log.called).to.be.true; - expect(console.warn.calledOnce).to.be.false; - expect(console.info.calledOnce).to.be.false; - - }); - }); - - describe('# _listenResponseFromLWA', () => { - let sandbox; - - beforeEach(() => { - let server = {}; - server.on = (input, callback) => {}; - server.listen = (input, callback) => {}; - - sandbox = sinon.sandbox.create(); - sandbox.stub(http, 'createServer').returns(server); - }); - - afterEach(() => { - sandbox.restore(); - }); - - - it('| should create a server', () => { - lwa._listenResponseFromLWA(9090, ()=> {}); - expect(http.createServer.calledOnce).to.be.true; - }); - }); -}); diff --git a/test/unit/utils/oauth-wrapper-test.js b/test/unit/utils/oauth-wrapper-test.js deleted file mode 100644 index bd84225d..00000000 --- a/test/unit/utils/oauth-wrapper-test.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -const expect = require('chai').expect; -const sinon = require('sinon'); -const oauth2 = require('simple-oauth2'); -const oauthWrapper = require('../../../lib/utils/oauth-wrapper'); -const CONSTANTS = require('../../../lib/utils/constants'); - -describe('oauth-wrapper unit test', () => { - describe('# createOAuth', () => { - let sandbox; - beforeEach(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(console, 'warn'); - sandbox.stub(oauth2, 'create'); - }); - afterEach(() => { - sandbox.restore(); - }); - - it('| should take input credentials and generate oauth2 instance', () => { - const TEST_ID = '123'; - const TEST_SECRET = '321'; - oauthWrapper.createOAuth(TEST_ID, TEST_SECRET); - let oauthInput = oauth2.create.getCall(0).args[0]; - expect(oauthInput.client.id).to.equal(TEST_ID); - expect(oauthInput.client.secret).to.equal(TEST_SECRET); - }); - - it('| should use CLI credentials when input parameters are omitted', () => { - oauthWrapper.createOAuth(); - let oauthInput = oauth2.create.getCall(0).args[0]; - expect(oauthInput.client.clientId).to.equal(CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_ID); - expect(oauthInput.client.clientConfirmation).to.equal(CONSTANTS.LWA.CLI_INTERNAL_ONLY_LWA_CLIENT.CLIENT_CONFIRMATION); - }); - }); -}); diff --git a/test/unit/utils/provider-chain-utils-test.js b/test/unit/utils/provider-chain-utils-test.js new file mode 100644 index 00000000..5997a96b --- /dev/null +++ b/test/unit/utils/provider-chain-utils-test.js @@ -0,0 +1,41 @@ +const { expect } = require('chai'); +const providerChainUtils = require('@src/utils/provider-chain-utils'); + +describe('Utils test - provider chain utility', () => { + describe('# test function resolveProviderChain', () => { + [ + { + testCase: 'input is an empty array', + input: [], + expectation: null + }, + { + testCase: 'input array has empty strings', + input: ['', ''], + expectation: null + }, + { + testCase: 'input array has valid values', + input: ['test', 'cli'], + expectation: 'test' + }, + { + testCase: 'input array has mix of valid and invalid values', + input: ['', 'cli'], + expectation: 'cli' + }, + { + testCase: 'input has undefined as string', + input: ['undefined'], + expectation: null + }, + ].forEach(({ testCase, input, expectation }) => { + it(`| ${testCase}, expect resolveProviderChain ${expectation}`, () => { + // call + const callResult = providerChainUtils.resolveProviderChain(input); + // verify + expect(callResult).equal(expectation); + }); + }); + }); +}); diff --git a/test/unit/utils/string-utils-test.js b/test/unit/utils/string-utils-test.js index c96ccd77..27d1e50a 100644 --- a/test/unit/utils/string-utils-test.js +++ b/test/unit/utils/string-utils-test.js @@ -252,4 +252,70 @@ describe('Utils test - string utility', () => { }); }); }); + + describe('# test function validateSyntax', () => { + [ + { + testCase: 'input value is null', + value: null, + expectation: false + }, + { + testCase: 'input value is undefined', + value: undefined, + expectation: false + }, + { + testCase: 'input value is empty', + value: '', + expectation: false + } + + ].forEach(({ testCase, value, expectation }) => { + it(`| ${testCase}, is a valid profile: ${expectation}`, () => { + // call + const callResult = stringUtils.validateSyntax('PROFILE_NAME', value); + // verify + expect(callResult).equal(expectation); + }); + }); + + describe('# test syntax for profile name', () => { + [ + { + testCase: 'input value is with non alphanumeric characters', + value: 'default-!!!???09', + expectation: false + }, + { + testCase: 'input value is with non alphanumeric characters test 2', + value: 'http://lambda.arn.com', + expectation: false + }, + { + testCase: 'input value is all consisted of non alphanumeric characters', + value: '://!@#$%^&*()<>?_', + expectation: false + }, + { + testCase: 'input value includes non-latin character', + value: '中文 にほんご', + expectation: false + }, + { + testCase: 'input value is a valid profile name', + value: 'askProfile', + expectation: true + } + + ].forEach(({ testCase, value, expectation }) => { + it(`| ${testCase}, is a valid profile: ${expectation}`, () => { + // call + const callResult = stringUtils.validateSyntax('PROFILE_NAME', value); + // verify + expect(callResult).equal(expectation); + }); + }); + }); + }); });