diff --git a/lib/typescript/botskills/.nycrc b/lib/typescript/botskills/.nycrc index 0ea144fef9..7d07652e8c 100644 --- a/lib/typescript/botskills/.nycrc +++ b/lib/typescript/botskills/.nycrc @@ -11,7 +11,8 @@ ], "reporter": [ "html", - "text" + "text", + "cobertura" ], "sourceMap": true, "cache": true diff --git a/lib/typescript/botskills/src/botskills-connect.ts b/lib/typescript/botskills/src/botskills-connect.ts index 8dcb931a2c..396858dbb9 100644 --- a/lib/typescript/botskills/src/botskills-connect.ts +++ b/lib/typescript/botskills/src/botskills-connect.ts @@ -6,7 +6,7 @@ import * as program from 'commander'; import { existsSync } from 'fs'; import { extname, isAbsolute, join, resolve } from 'path'; -import { connectSkill } from './functionality'; +import { ConnectSkill } from './functionality'; import { ConsoleLogger, ILogger } from './logger'; import { ICognitiveModelFile, IConnectConfiguration } from './models'; import { validatePairOfArgs } from './utils'; @@ -163,4 +163,4 @@ configuration.logger = logger; // End of arguments validation -connectSkill( configuration); +new ConnectSkill(logger).connectSkill( configuration); diff --git a/lib/typescript/botskills/src/functionality/connectSkill.ts b/lib/typescript/botskills/src/functionality/connectSkill.ts index 5fa5082112..d0af0414b1 100644 --- a/lib/typescript/botskills/src/functionality/connectSkill.ts +++ b/lib/typescript/botskills/src/functionality/connectSkill.ts @@ -3,236 +3,250 @@ * Licensed under the MIT License. */ -import { existsSync, writeFileSync } from 'fs'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; import { isAbsolute, join, resolve } from 'path'; import { get } from 'request-promise-native'; import { ConsoleLogger, ILogger} from '../logger'; import { IAction, IConnectConfiguration, ISkillFIle, ISkillManifest, IUtteranceSource } from '../models'; -import { authenticate, ChildProcessUtils } from '../utils'; - -async function runCommand(command: string[], description: string): Promise { - logger.command(description, command.join(' ')); - const cmd: string = command[0]; - const commandArgs: string[] = command.slice(1) - .filter((arg: string) => arg); - - try { - - return await childProcessUtils.execute(cmd, commandArgs) - // tslint:disable-next-line:typedef - .catch((err) => { - throw new Error(`The execution of the ${cmd} command failed with the following error:\n${err}`); - }); - } catch (err) { - throw err; +import { AuthenticationUtils, ChildProcessUtils } from '../utils'; + +export class ConnectSkill { + private logger: ILogger; + private childProcessUtils: ChildProcessUtils; + private authenticationUtils: AuthenticationUtils; + + constructor(logger: ILogger) { + this.logger = logger || new ConsoleLogger(); + this.childProcessUtils = new ChildProcessUtils(); + this.authenticationUtils = new AuthenticationUtils(); } -} -async function getRemoteManifest(manifestUrl: string): Promise { - try { - return get({ - uri: manifestUrl, - json: true - }); - } catch (err) { - logger.error(`There was a problem while getting the remote manifest:\n${err}`); - throw err; + public async runCommand(command: string[], description: string): Promise { + this.logger.command(description, command.join(' ')); + const cmd: string = command[0]; + const commandArgs: string[] = command.slice(1) + .filter((arg: string) => arg); + + try { + + return await this.childProcessUtils.execute(cmd, commandArgs) + // tslint:disable-next-line:typedef + .catch((err) => { + throw new Error(`The execution of the ${cmd} command failed with the following error:\n${err}`); + }); + } catch (err) { + throw err; + } } -} -function getLocalManifest(manifestPath: string): ISkillManifest { - const skillManifestPath: string = isAbsolute(manifestPath) ? manifestPath : join(resolve('./'), manifestPath); - if (!existsSync(skillManifestPath)) { - logger.error( - `The 'localManifest' argument leads to a non-existing file. Please make sure to provide a valid path to your Skill manifest.`); - process.exit(1); + public async getRemoteManifest(manifestUrl: string): Promise { + try { + return get({ + uri: manifestUrl, + json: true + }); + } catch (err) { + throw new Error(`There was a problem while getting the remote manifest:\n${err}`); + } } - // tslint:disable-next-line: non-literal-require - return require(skillManifestPath); -} + private getLocalManifest(manifestPath: string): ISkillManifest { + const skillManifestPath: string = isAbsolute(manifestPath) ? manifestPath : join(resolve('./'), manifestPath); -function validateManifestSchema(skillManifest: ISkillManifest): void { - if (!skillManifest.name) { - logger.error(`Missing property 'name' of the manifest`); - } - if (!skillManifest.id) { - logger.error(`Missing property 'id' of the manifest`); - } else if (skillManifest.id.match(/^\d|[^\w]/g) !== null) { - // tslint:disable-next-line:max-line-length - logger.error(`The 'id' of the manifest contains some characters not allowed. Make sure the 'id' contains only letters, numbers and underscores, but doesn't start with number.`); - } - if (!skillManifest.endpoint) { - logger.error(`Missing property 'endpoint' of the manifest`); - } - if (!skillManifest.authenticationConnections) { - logger.error(`Missing property 'authenticationConnections' of the manifest`); - } - if (!skillManifest.actions || !skillManifest.actions[0]) { - logger.error(`Missing property 'actions' of the manifest`); + if (!existsSync(skillManifestPath)) { + throw new Error( + `The 'localManifest' argument leads to a non-existing file. Please make sure to provide a valid path to your Skill manifest.`); + } + + return JSON.parse(readFileSync(skillManifestPath, 'UTF8')); } -} -// tslint:disable-next-line:max-func-body-length -async function updateDispatch(configuration: IConnectConfiguration, manifest: ISkillManifest): Promise { - try { - // Initializing variables for the updateDispatch scope - const dispatchFile: string = `${configuration.dispatchName}.dispatch`; - const dispatchJsonFile: string = `${configuration.dispatchName}.json`; - const dispatchFilePath: string = join(configuration.dispatchFolder, dispatchFile); - const dispatchJsonFilePath: string = join(configuration.dispatchFolder, dispatchJsonFile); - const intentName: string = manifest.id; - let luisDictionary: Map; - - logger.message('Getting intents for dispatch...'); - luisDictionary = manifest.actions.filter((action: IAction) => action.definition.triggers.utteranceSources) - .reduce((acc: IUtteranceSource[], val: IAction) => acc.concat(val.definition.triggers.utteranceSources), []) - .reduce((acc: string[], val: IUtteranceSource) => acc.concat(val.source), []) - .reduce( - (acc: Map, val: string) => { - const luis: string[] = val.split('#'); - if (acc.has(luis[0])) { - const previous: string | undefined = acc.get(luis[0]); - acc.set(luis[0], previous + luis[1]); - } else { - acc.set(luis[0], luis[1]); - } + private validateManifestSchema(skillManifest: ISkillManifest): void { + if (!skillManifest.name) { + this.logger.error(`Missing property 'name' of the manifest`); + } + if (!skillManifest.id) { + this.logger.error(`Missing property 'id' of the manifest`); + } else if (skillManifest.id.match(/^\d|[^\w]/g) !== null) { + // tslint:disable-next-line:max-line-length + this.logger.error(`The 'id' of the manifest contains some characters not allowed. Make sure the 'id' contains only letters, numbers and underscores, but doesn't start with number.`); + } + if (!skillManifest.endpoint) { + this.logger.error(`Missing property 'endpoint' of the manifest`); + } + if (!skillManifest.authenticationConnections) { + this.logger.error(`Missing property 'authenticationConnections' of the manifest`); + } + if (!skillManifest.actions || !skillManifest.actions[0]) { + this.logger.error(`Missing property 'actions' of the manifest`); + } + } - return acc; - }, - new Map()); - - logger.message('Adding skill to Dispatch'); - await Promise.all( - Array.from(luisDictionary.entries()) - .map(async(item: [string, string]) => { - const luisApp: string = item[0]; - const luFile: string = `${luisApp}.lu`; - const luisFile: string = `${luisApp}.luis`; - const luFilePath: string = join(configuration.luisFolder, luFile); - const luisFilePath: string = join(configuration.luisFolder, luisFile); - - // Validate 'ludown' arguments - if (!existsSync(configuration.luisFolder)) { - throw(new Error(`Path to the LUIS folder (${configuration.luisFolder}) leads to a nonexistent folder.`)); - } else if (!existsSync(luFilePath)) { - throw(new Error(`Path to the ${luisApp}.lu file leads to a nonexistent file.`)); + // tslint:disable-next-line:max-func-body-length + public async updateDispatch(configuration: IConnectConfiguration, manifest: ISkillManifest): Promise { + try { + // Initializing variables for the updateDispatch scope + const dispatchFile: string = `${configuration.dispatchName}.dispatch`; + const dispatchJsonFile: string = `${configuration.dispatchName}.json`; + const dispatchFilePath: string = join(configuration.dispatchFolder, dispatchFile); + const dispatchJsonFilePath: string = join(configuration.dispatchFolder, dispatchJsonFile); + const intentName: string = manifest.id; + let luisDictionary: Map; + + this.logger.message('Getting intents for dispatch...'); + luisDictionary = manifest.actions.filter((action: IAction) => action.definition.triggers.utteranceSources) + .reduce((acc: IUtteranceSource[], val: IAction) => acc.concat(val.definition.triggers.utteranceSources), []) + .reduce((acc: string[], val: IUtteranceSource) => acc.concat(val.source), []) + .reduce( + (acc: Map, val: string) => { + const luis: string[] = val.split('#'); + if (acc.has(luis[0])) { + const previous: string | undefined = acc.get(luis[0]); + acc.set(luis[0], previous + luis[1]); + } else { + acc.set(luis[0], luis[1]); + } + + return acc; + }, + new Map()); + + this.logger.message('Adding skill to Dispatch'); + await Promise.all( + Array.from(luisDictionary.entries()) + .map(async(item: [string, string]) => { + const luisApp: string = item[0]; + const luFile: string = `${luisApp}.lu`; + const luisFile: string = `${luisApp}.luis`; + const luFilePath: string = join(configuration.luisFolder, luFile); + const luisFilePath: string = join(configuration.luisFolder, luisFile); + + // Validate 'ludown' arguments + if (!existsSync(configuration.luisFolder)) { + throw(new Error(`Path to the LUIS folder (${configuration.luisFolder}) leads to a nonexistent folder.`)); + } else if (!existsSync(luFilePath)) { + throw(new Error(`Path to the ${luisApp}.lu file leads to a nonexistent file.`)); + } + + // Validate 'dispatch add' arguments + if (!existsSync(configuration.dispatchFolder)) { + throw(new Error(`Path to the Dispatch folder (${configuration.dispatchFolder}) leads to a nonexistent folder.`)); + } else if (!existsSync(dispatchFilePath)) { + throw(new Error(`Path to the ${dispatchFile} file leads to a nonexistent file.`)); + } + + // Parse LU file + this.logger.message(`Parsing ${luisApp} LU file...`); + const ludownParseCommand: string[] = ['ludown', 'parse', 'toluis']; + ludownParseCommand.push(...['--in', luFilePath]); + ludownParseCommand.push(...['--luis_culture', configuration.language]); + ludownParseCommand.push(...['--out_folder', configuration.luisFolder]); + ludownParseCommand.push(...['--out', `"${luisFile}"`]); + + await this.runCommand(ludownParseCommand, `Parsing ${luisApp} LU file`); + + if (!existsSync(luisFilePath)) { + // tslint:disable-next-line: max-line-length + throw(new Error(`Path to ${luisFile} (${luisFilePath}) leads to a nonexistent file. Make sure the ludown command is being executed successfully`)); + } + // Update Dispatch file + const dispatchAddCommand: string[] = ['dispatch', 'add']; + dispatchAddCommand.push(...['--type', 'file']); + dispatchAddCommand.push(...['--name', intentName]); + dispatchAddCommand.push(...['--filePath', luisFilePath]); + dispatchAddCommand.push(...['--intentName', intentName]); + dispatchAddCommand.push(...['--dataFolder', configuration.dispatchFolder]); + dispatchAddCommand.push(...['--dispatch', dispatchFilePath]); + + await this.runCommand(dispatchAddCommand, `Executing dispatch add for the ${luisApp} LU file`); + })); + + // Check if it is necessary to train the skill + if (!configuration.noTrain) { + this.logger.message('Running dispatch refresh...'); + const dispatchRefreshCommand: string[] = ['dispatch', 'refresh']; + dispatchRefreshCommand.push(...['--dispatch', dispatchFilePath]); + dispatchRefreshCommand.push(...['--dataFolder', configuration.dispatchFolder]); + + this.logger.message(await this.runCommand( + dispatchRefreshCommand, + `Executing dispatch refresh for the ${configuration.dispatchName} file`)); + + if (!existsSync(dispatchJsonFilePath)) { + // tslint:disable-next-line: max-line-length + throw(new Error(`Path to ${dispatchJsonFile} (${dispatchJsonFilePath}) leads to a nonexistent file. Make sure the dispatch refresh command is being executed successfully`)); + } + + this.logger.message('Running LuisGen...'); + const luisgenCommand: string[] = ['luisgen']; + luisgenCommand.push(dispatchJsonFilePath); + luisgenCommand.push(...[`-${configuration.lgLanguage}`, `"DispatchLuis"`]); + luisgenCommand.push(...['-o', configuration.lgOutFolder]); + + await this.runCommand(luisgenCommand, `Executing luisgen for the ${configuration.dispatchName} file`); } + this.logger.success('Successfully updated Dispatch model'); + } catch (err) { + throw new Error(`An error ocurred while updating the Dispatch model:\n${err}`); + } + } - // Validate 'dispatch add' arguments - if (!existsSync(configuration.dispatchFolder)) { - throw(new Error(`Path to the Dispatch folder (${configuration.dispatchFolder}) leads to a nonexistent folder.`)); - } else if (!existsSync(dispatchFilePath)) { - throw(new Error(`Path to the ${dispatchFile} file leads to a nonexistent file.`)); + public async connectSkill(configuration: IConnectConfiguration): Promise { + try { + // Validate if no manifest path or URL was passed + if (!configuration.localManifest && !configuration.remoteManifest) { + throw new Error(`Either the 'localManifest' or 'remoteManifest' argument should be passed.`); } + // Take skillManifest + const skillManifest: ISkillManifest = configuration.localManifest + ? this.getLocalManifest(configuration.localManifest) + : await this.getRemoteManifest(configuration.remoteManifest); - // Parse LU file - logger.message(`Parsing ${luisApp} LU file...`); - const ludownParseCommand: string[] = ['ludown', 'parse', 'toluis']; - ludownParseCommand.push(...['--in', luFilePath]); - ludownParseCommand.push(...['--luis_culture', configuration.language]); - ludownParseCommand.push(...['--out_folder', configuration.luisFolder]); - ludownParseCommand.push(...['--out', `"${luisApp}.luis"`]); + // Manifest schema validation + this.validateManifestSchema(skillManifest); - await runCommand(ludownParseCommand, `Parsing ${luisApp} LU file`); + if (this.logger.isError) { - if (!existsSync(luisFilePath)) { - // tslint:disable-next-line: max-line-length - throw(new Error(`Path to ${luisFile} (${luisFilePath}) leads to a nonexistent file. Make sure the ludown command is being executed successfully`)); - } - // Update Dispatch file - const dispatchAddCommand: string[] = ['dispatch', 'add']; - dispatchAddCommand.push(...['--type', 'file']); - dispatchAddCommand.push(...['--name', manifest.id]); - dispatchAddCommand.push(...['--filePath', luisFilePath]); - dispatchAddCommand.push(...['--intentName', intentName]); - dispatchAddCommand.push(...['--dataFolder', configuration.dispatchFolder]); - dispatchAddCommand.push(...['--dispatch', dispatchFilePath]); - - await runCommand(dispatchAddCommand, `Executing dispatch add for the ${luisApp} LU file`); - })); - - // Check if it is necessary to train the skill - if (!configuration.noTrain) { - logger.message('Running dispatch refresh...'); - const dispatchRefreshCommand: string[] = ['dispatch', 'refresh']; - dispatchRefreshCommand.push(...['--dispatch', dispatchFilePath]); - dispatchRefreshCommand.push(...['--dataFolder', configuration.dispatchFolder]); - - logger.message( - await runCommand(dispatchRefreshCommand, `Executing dispatch refresh for the ${configuration.dispatchName} file`) - ); - - if (!existsSync(dispatchJsonFilePath)) { - // tslint:disable-next-line: max-line-length - throw(new Error(`Path to ${dispatchJsonFile} (${dispatchJsonFilePath}) leads to a nonexistent file. Make sure the dispatch refresh command is being executed successfully`)); + return false; } + // End of manifest schema validation - logger.message('Running LuisGen...'); - const luisgenCommand: string[] = ['luisgen']; - luisgenCommand.push(dispatchJsonFilePath); - luisgenCommand.push(...[`-${configuration.lgLanguage}`, `"DispatchLuis"`]); - luisgenCommand.push(...['-o', configuration.lgOutFolder]); - - await runCommand(luisgenCommand, `Executing luisgen for the ${configuration.dispatchName} file`); - } - logger.success('Successfully updated Dispatch model'); - } catch (err) { - throw new Error(`An error ocurred while updating the Dispatch model:\n${err}`); - } -} - -export async function connectSkill(configuration: IConnectConfiguration): Promise { - try { - if (configuration.logger) { - logger = configuration.logger; - } - // Take skillManifest - const skillManifest: ISkillManifest = configuration.localManifest - ? getLocalManifest(configuration.localManifest) - : await getRemoteManifest(configuration.remoteManifest); + // Take VA Skills configurations + //tslint:disable-next-line: no-var-requires non-literal-require + const assistantSkillsFile: ISkillFIle = require(configuration.skillsFile); + const assistantSkills: ISkillManifest[] = assistantSkillsFile.skills || []; - // Manifest schema validation - validateManifestSchema(skillManifest); + // Check if the skill is already connected to the assistant + if (assistantSkills.find((assistantSkill: ISkillManifest) => assistantSkill.id === skillManifest.id)) { + this.logger.warning(`The skill '${skillManifest.name}' is already registered.`); - if (logger.isError) { - process.exit(1); - } - // End of manifest schema validation + return false; + } - // Take VA Skills configurations - //tslint:disable-next-line: no-var-requires non-literal-require - const assistantSkillsFile: ISkillFIle = require(configuration.skillsFile); - const assistantSkills: ISkillManifest[] = assistantSkillsFile.skills || []; + // Updating Dispatch + this.logger.message('Updating Dispatch'); + await this.updateDispatch(configuration, skillManifest); - // Check if the skill is already connected to the assistant - if (assistantSkills.find((assistantSkill: ISkillManifest) => assistantSkill.id === skillManifest.id)) { - logger.warning(`The skill '${skillManifest.name}' is already registered.`); - process.exit(1); - } + // Adding the skill manifest to the assistant skills array + this.logger.warning(`Appending '${skillManifest.name}' manifest to your assistant's skills configuration file.`); + assistantSkills.push(skillManifest); - // Updating Dispatch - logger.message('Updating Dispatch'); - await updateDispatch(configuration, skillManifest); + // Updating the assistant skills file's skills property with the assistant skills array + assistantSkillsFile.skills = assistantSkills; - // Adding the skill manifest to the assistant skills array - logger.warning(`Appending '${skillManifest.name}' manifest to your assistant's skills configuration file.`); - assistantSkills.push(skillManifest); + // Writing (and overriding) the assistant skills file + writeFileSync(configuration.skillsFile, JSON.stringify(assistantSkillsFile, undefined, 4)); + this.logger.success(`Successfully appended '${skillManifest.name}' manifest to your assistant's skills configuration file!`); - // Updating the assistant skills file's skills property with the assistant skills array - assistantSkillsFile.skills = assistantSkills; + // Configuring bot auth settings + this.logger.message('Configuring bot auth settings'); + await this.authenticationUtils.authenticate(configuration, skillManifest, this.logger); - // Writing (and overriding) the assistant skills file - writeFileSync(configuration.skillsFile, JSON.stringify(assistantSkillsFile, undefined, 4)); - logger.success(`Successfully appended '${skillManifest.name}' manifest to your assistant's skills configuration file!`); + return true; + } catch (err) { + this.logger.error(`There was an error while connecting the Skill to the Assistant:\n${err}`); - // Configuring bot auth settings - logger.message('Configuring bot auth settings'); - await authenticate(configuration, skillManifest, logger); - } catch (err) { - logger.error(`There was an error while connecting the Skill to the Assistant:\n${err}`); + return false; + } } } - -let logger: ILogger = new ConsoleLogger(); -const childProcessUtils: ChildProcessUtils = new ChildProcessUtils(); diff --git a/lib/typescript/botskills/src/functionality/index.ts b/lib/typescript/botskills/src/functionality/index.ts index c114eb6066..7e639c76f5 100644 --- a/lib/typescript/botskills/src/functionality/index.ts +++ b/lib/typescript/botskills/src/functionality/index.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. */ -export { connectSkill } from './connectSkill'; +export { ConnectSkill } from './connectSkill'; export { DisconnectSkill } from './disconnectSkill'; export { ListSkill } from './listSkill'; diff --git a/lib/typescript/botskills/src/utils/authenticationUtils.ts b/lib/typescript/botskills/src/utils/authenticationUtils.ts index 0b76691afb..137d2e1649 100644 --- a/lib/typescript/botskills/src/utils/authenticationUtils.ts +++ b/lib/typescript/botskills/src/utils/authenticationUtils.ts @@ -16,217 +16,223 @@ import { ISkillManifest } from '../models'; import { ChildProcessUtils } from './'; -const scopeMap: Map = new Map([ - ['Files.Read.Selected', '5447fe39-cb82-4c1a-b977-520e67e724eb'], - ['Files.ReadWrite.Selected', '17dde5bd-8c17-420f-a486-969730c1b827'], - ['Files.ReadWrite.AppFolder', '8019c312-3263-48e6-825e-2b833497195b'], - ['Reports.Read.All', '02e97553-ed7b-43d0-ab3c-f8bace0d040c'], - ['Sites.ReadWrite.All', '89fe6a52-be36-487e-b7d8-d061c450a026'], - ['Member.Read.Hidden', '658aa5d8-239f-45c4-aa12-864f4fc7e490'], - ['Tasks.ReadWrite.Shared', 'c5ddf11b-c114-4886-8558-8a4e557cd52b'], - ['Tasks.Read.Shared', '88d21fd4-8e5a-4c32-b5e2-4a1c95f34f72'], - ['Contacts.ReadWrite.Shared', 'afb6c84b-06be-49af-80bb-8f3f77004eab'], - ['Contacts.Read.Shared', '242b9d9e-ed24-4d09-9a52-f43769beb9d4'], - ['Calendars.ReadWrite.Shared', '12466101-c9b8-439a-8589-dd09ee67e8e9'], - ['Calendars.Read.Shared', '2b9c4092-424d-4249-948d-b43879977640'], - ['Mail.Send.Shared', 'a367ab51-6b49-43bf-a716-a1fb06d2a174'], - ['Mail.ReadWrite.Shared', '5df07973-7d5d-46ed-9847-1271055cbd51'], - ['Mail.Read.Shared', '7b9103a5-4610-446b-9670-80643382c1fa'], - ['User.Read', 'e1fe6dd8-ba31-4d61-89e7-88639da4683d'], - ['User.ReadWrite', 'b4e74841-8e56-480b-be8b-910348b18b4c'], - ['User.ReadBasic.All', 'b340eb25-3456-403f-be2f-af7a0d370277'], - ['User.Read.All', 'a154be20-db9c-4678-8ab7-66f6cc099a59'], - ['User.ReadWrite.All', '204e0828-b5ca-4ad8-b9f3-f32a958e7cc4'], - ['Group.Read.All', '5f8c59db-677d-491f-a6b8-5f174b11ec1d'], - ['Group.ReadWrite.All', '4e46008b-f24c-477d-8fff-7bb4ec7aafe0'], - ['Directory.Read.All', '06da0dbc-49e2-44d2-8312-53f166ab848a'], - ['Directory.ReadWrite.All', 'c5366453-9fb0-48a5-a156-24f0c49a4b84'], - ['Directory.AccessAsUser.All', '0e263e50-5827-48a4-b97c-d940288653c7'], - ['Mail.Read', '570282fd-fa5c-430d-a7fd-fc8dc98a9dca'], - ['Mail.ReadWrite', '024d486e-b451-40bb-833d-3e66d98c5c73'], - ['Mail.Send', 'e383f46e-2787-4529-855e-0e479a3ffac0'], - ['Calendars.Read', '465a38f9-76ea-45b9-9f34-9e8b0d4b0b42'], - ['Calendars.ReadWrite', '1ec239c2-d7c9-4623-a91a-a9775856bb36'], - ['Contacts.Read', 'ff74d97f-43af-4b68-9f2a-b77ee6968c5d'], - ['Contacts.ReadWrite', 'd56682ec-c09e-4743-aaf4-1a3aac4caa21'], - ['Files.Read', '10465720-29dd-4523-a11a-6a75c743c9d9'], - ['Files.ReadWrite', '5c28f0bf-8a70-41f1-8ab2-9032436ddb65'], - ['Files.Read.All', 'df85f4d6-205c-4ac5-a5ea-6bf408dba283'], - ['Files.ReadWrite.All', '863451e7-0667-486c-a5d6-d135439485f0'], - ['Sites.Read.All', '205e70e5-aba6-4c52-a976-6d2d46c48043'], - ['openid', '37f7f235-527c-4136-accd-4a02d197296e'], - ['offline_access', '7427e0e9-2fba-42fe-b0c0-848c9e6a8182'], - ['People.Read', 'ba47897c-39ec-4d83-8086-ee8256fa737d'], - ['Notes.Create', '9d822255-d64d-4b7a-afdb-833b9a97ed02'], - ['Notes.ReadWrite.CreatedByApp', 'ed68249d-017c-4df5-9113-e684c7f8760b'], - ['Notes.Read', '371361e4-b9e2-4a3f-8315-2a301a3b0a3d'], - ['Notes.ReadWrite', '615e26af-c38a-4150-ae3e-c3b0d4cb1d6a'], - ['Notes.Read.All', 'dfabfca6-ee36-4db2-8208-7a28381419b3'], - ['Notes.ReadWrite.All', '64ac0503-b4fa-45d9-b544-71a463f05da0'], - ['Tasks.Read', 'f45671fb-e0fe-4b4b-be20-3d3ce43f1bcb'], - ['Tasks.ReadWrite', '2219042f-cab5-40cc-b0d2-16b1540b4c5f'], - ['email', '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'], - ['profile', '14dad69e-099b-42c9-810b-d002981feec1'] -]); - -function getScopeId(scope: string): string { - return scopeMap.get(scope) || ''; -} - -function createScopeManifest(scopes: string[]): IScopeManifest[] { - return [{ - resourceAppId: '00000003-0000-0000-c000-000000000000', - resourceAccess: scopes.filter((scope: string) => scopeMap.has(scope)) - .map((scope: string) => { - return { - id: getScopeId(scope), - type: 'Scope' - }; - }) - }]; -} - -// tslint:disable-next-line:max-func-body-length export-name -export async function authenticate(configuration: IConnectConfiguration, manifest: ISkillManifest, logger: ILogger): Promise { - // configuring bot auth settings - logger.message('Checking for authentication settings ...'); - if (manifest.authenticationConnections) { - const aadConfig: IAuthenticationConnection | undefined = manifest.authenticationConnections.find( - (connection: IAuthenticationConnection) => connection.serviceProviderId === 'Azure Active Directory v2'); - if (aadConfig) { - logger.message('Configuring Azure AD connection ...'); - - let connectionName: string = aadConfig.id; - const newScopes: string[] = aadConfig.scopes.split(', '); - let scopes: string[] = newScopes.slice(0); - - // check for existing aad connection - const listAuthSettingsCommand: string[] = ['az', 'bot', 'authsetting', 'list']; - listAuthSettingsCommand.push(...['-n', configuration.botName]); - listAuthSettingsCommand.push(...['-g', configuration.resourceGroup]); - - logger.command('Checking for existing aad connections', listAuthSettingsCommand.join(' ')); - - const connectionsResult: string = await childProcessUtils.tryExecute(listAuthSettingsCommand); - const connections: IAzureAuthSetting[] = JSON.parse(connectionsResult); - const aadConnection: IAzureAuthSetting | undefined = connections.find( - (connection: IAzureAuthSetting) => connection.properties.serviceProviderDisplayName === 'Azure Active Directory v2'); - if (aadConnection) { - const settingName: string = aadConnection.name.split('/')[1]; - - // Get current aad auth setting - const showAuthSettingsCommand: string[] = ['az', 'bot', 'authsetting', 'show']; - showAuthSettingsCommand.push(...['-n', configuration.botName]); - showAuthSettingsCommand.push(...['-g', configuration.resourceGroup]); - showAuthSettingsCommand.push(...['-c', settingName]); - - logger.command('Getting current aad auth settings', showAuthSettingsCommand.join(' ')); - - const botAuthSettingResult: string = await childProcessUtils.tryExecute(showAuthSettingsCommand); - const botAuthSetting: IAzureAuthSetting = JSON.parse(botAuthSettingResult); - const existingScopes: string[] = botAuthSetting.properties.scopes.split(' '); - scopes = scopes.concat(existingScopes); - connectionName = settingName; - - // delete current aad auth connection - const deleteAuthSettingCommand: string[] = ['az', 'bot', 'authsetting', 'delete']; - deleteAuthSettingCommand.push(...['-n', configuration.botName]); - deleteAuthSettingCommand.push(...['-g', configuration.resourceGroup]); - deleteAuthSettingCommand.push(...['-c', settingName]); - - logger.command('Deleting current bot authentication setting', deleteAuthSettingCommand.join(' ')); - - const deleteResult: string = await childProcessUtils.tryExecute(deleteAuthSettingCommand); - } - - // update appsettings.json - logger.message('Updating appsettings.json ...'); - // tslint:disable-next-line:non-literal-require - const appSettings: IAppSettingOauthConnection = require(configuration.appSettingsFile); - - // check for and remove existing aad connections - if (appSettings.oauthConnections) { - appSettings.oauthConnections = appSettings.oauthConnections.filter( - (connection: IOauthConnection) => connection.provider !== 'Azure Active Directory v2'); - } - - // set or add new oauth setting - const oauthSetting: IOauthConnection = { name: connectionName, provider: 'Azure Active Directory v2' }; - if (!appSettings.oauthConnections) { - appSettings.oauthConnections = [oauthSetting]; - } else { - appSettings.oauthConnections.push(oauthSetting); - } - - // update appsettings.json - writeFileSync(configuration.appSettingsFile, JSON.stringify(appSettings, undefined, 4)); - - // Remove duplicate scopes - scopes = [...new Set(scopes)]; - const scopeManifest: IScopeManifest[] = createScopeManifest(scopes); - - // get the information of the app - const azureAppShowCommand: string[] = ['az', 'ad', 'app', 'show']; - azureAppShowCommand.push(...['--id', appSettings.microsoftAppId]); - - logger.command('Getting the app information', azureAppShowCommand.join(' ')); - - const azureAppShowResult: string = await childProcessUtils.tryExecute(azureAppShowCommand); - const azureAppReplyUrls: IAppShowReplyUrl = JSON.parse(azureAppShowResult); - - // get the Reply Urls from the app - const replyUrlsSet: Set = new Set(azureAppReplyUrls.replyUrls); - // append the necessary Url if it's not already present - replyUrlsSet.add('https://token.botframework.com/.auth/web/redirect'); - const replyUrls: string[] = [...replyUrlsSet]; - - // Update MSA scopes - logger.message('Configuring MSA app scopes ...'); - const azureAppUpdateCommand: string[] = ['az', 'ad', 'app', 'update']; - azureAppUpdateCommand.push(...['--id', appSettings.microsoftAppId]); - azureAppUpdateCommand.push(...['--reply-urls', replyUrls.join(' ')]); - const scopeManifestText: string = JSON.stringify(scopeManifest) - .replace(/\"/g, '\''); - azureAppUpdateCommand.push(...['--required-resource-accesses', `"${scopeManifestText}"`]); +export class AuthenticationUtils { + public childProcessUtils: ChildProcessUtils; + + private scopeMap: Map = new Map([ + ['Files.Read.Selected', '5447fe39-cb82-4c1a-b977-520e67e724eb'], + ['Files.ReadWrite.Selected', '17dde5bd-8c17-420f-a486-969730c1b827'], + ['Files.ReadWrite.AppFolder', '8019c312-3263-48e6-825e-2b833497195b'], + ['Reports.Read.All', '02e97553-ed7b-43d0-ab3c-f8bace0d040c'], + ['Sites.ReadWrite.All', '89fe6a52-be36-487e-b7d8-d061c450a026'], + ['Member.Read.Hidden', '658aa5d8-239f-45c4-aa12-864f4fc7e490'], + ['Tasks.ReadWrite.Shared', 'c5ddf11b-c114-4886-8558-8a4e557cd52b'], + ['Tasks.Read.Shared', '88d21fd4-8e5a-4c32-b5e2-4a1c95f34f72'], + ['Contacts.ReadWrite.Shared', 'afb6c84b-06be-49af-80bb-8f3f77004eab'], + ['Contacts.Read.Shared', '242b9d9e-ed24-4d09-9a52-f43769beb9d4'], + ['Calendars.ReadWrite.Shared', '12466101-c9b8-439a-8589-dd09ee67e8e9'], + ['Calendars.Read.Shared', '2b9c4092-424d-4249-948d-b43879977640'], + ['Mail.Send.Shared', 'a367ab51-6b49-43bf-a716-a1fb06d2a174'], + ['Mail.ReadWrite.Shared', '5df07973-7d5d-46ed-9847-1271055cbd51'], + ['Mail.Read.Shared', '7b9103a5-4610-446b-9670-80643382c1fa'], + ['User.Read', 'e1fe6dd8-ba31-4d61-89e7-88639da4683d'], + ['User.ReadWrite', 'b4e74841-8e56-480b-be8b-910348b18b4c'], + ['User.ReadBasic.All', 'b340eb25-3456-403f-be2f-af7a0d370277'], + ['User.Read.All', 'a154be20-db9c-4678-8ab7-66f6cc099a59'], + ['User.ReadWrite.All', '204e0828-b5ca-4ad8-b9f3-f32a958e7cc4'], + ['Group.Read.All', '5f8c59db-677d-491f-a6b8-5f174b11ec1d'], + ['Group.ReadWrite.All', '4e46008b-f24c-477d-8fff-7bb4ec7aafe0'], + ['Directory.Read.All', '06da0dbc-49e2-44d2-8312-53f166ab848a'], + ['Directory.ReadWrite.All', 'c5366453-9fb0-48a5-a156-24f0c49a4b84'], + ['Directory.AccessAsUser.All', '0e263e50-5827-48a4-b97c-d940288653c7'], + ['Mail.Read', '570282fd-fa5c-430d-a7fd-fc8dc98a9dca'], + ['Mail.ReadWrite', '024d486e-b451-40bb-833d-3e66d98c5c73'], + ['Mail.Send', 'e383f46e-2787-4529-855e-0e479a3ffac0'], + ['Calendars.Read', '465a38f9-76ea-45b9-9f34-9e8b0d4b0b42'], + ['Calendars.ReadWrite', '1ec239c2-d7c9-4623-a91a-a9775856bb36'], + ['Contacts.Read', 'ff74d97f-43af-4b68-9f2a-b77ee6968c5d'], + ['Contacts.ReadWrite', 'd56682ec-c09e-4743-aaf4-1a3aac4caa21'], + ['Files.Read', '10465720-29dd-4523-a11a-6a75c743c9d9'], + ['Files.ReadWrite', '5c28f0bf-8a70-41f1-8ab2-9032436ddb65'], + ['Files.Read.All', 'df85f4d6-205c-4ac5-a5ea-6bf408dba283'], + ['Files.ReadWrite.All', '863451e7-0667-486c-a5d6-d135439485f0'], + ['Sites.Read.All', '205e70e5-aba6-4c52-a976-6d2d46c48043'], + ['openid', '37f7f235-527c-4136-accd-4a02d197296e'], + ['offline_access', '7427e0e9-2fba-42fe-b0c0-848c9e6a8182'], + ['People.Read', 'ba47897c-39ec-4d83-8086-ee8256fa737d'], + ['Notes.Create', '9d822255-d64d-4b7a-afdb-833b9a97ed02'], + ['Notes.ReadWrite.CreatedByApp', 'ed68249d-017c-4df5-9113-e684c7f8760b'], + ['Notes.Read', '371361e4-b9e2-4a3f-8315-2a301a3b0a3d'], + ['Notes.ReadWrite', '615e26af-c38a-4150-ae3e-c3b0d4cb1d6a'], + ['Notes.Read.All', 'dfabfca6-ee36-4db2-8208-7a28381419b3'], + ['Notes.ReadWrite.All', '64ac0503-b4fa-45d9-b544-71a463f05da0'], + ['Tasks.Read', 'f45671fb-e0fe-4b4b-be20-3d3ce43f1bcb'], + ['Tasks.ReadWrite', '2219042f-cab5-40cc-b0d2-16b1540b4c5f'], + ['email', '64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0'], + ['profile', '14dad69e-099b-42c9-810b-d002981feec1'] + ]); + + constructor() { + this.childProcessUtils = new ChildProcessUtils(); + } - logger.command('Updating the app information', azureAppUpdateCommand.join(' ')); + private getScopeId(scope: string): string { + return this.scopeMap.get(scope) || ''; + } - const errorResult: string = await childProcessUtils.tryExecute(azureAppUpdateCommand); - // Catch error: Updates to converged applications are not allowed in this version. - if (errorResult) { - logger.warning('Could not configure scopes automatically.'); - // manualScopesRequired = true - } + private createScopeManifest(scopes: string[]): IScopeManifest[] { + return [{ + resourceAppId: '00000003-0000-0000-c000-000000000000', + resourceAccess: scopes.filter((scope: string) => this.scopeMap.has(scope)) + .map((scope: string) => { + return { + id: this.getScopeId(scope), + type: 'Scope' + }; + }) + }]; + } - logger.message('Updating bot oauth settings ...'); - const authSettingCommand: string[] = ['az', 'bot', 'authsetting', 'create']; - authSettingCommand.push(...['--name', configuration.botName]); - authSettingCommand.push(...['--resource-group', configuration.resourceGroup]); - authSettingCommand.push(...['--setting-name', connectionName]); - authSettingCommand.push(...['--client-id', `"${appSettings.microsoftAppId}"`]); - authSettingCommand.push(...['--client-secret', `"${appSettings.microsoftAppPassword}"`]); - authSettingCommand.push(...['--service', 'Aadv2']); - authSettingCommand.push(...['--parameters', `clientId="${appSettings.microsoftAppId}"`]); - authSettingCommand.push(...[`clientSecret="${appSettings.microsoftAppPassword}"`, 'tenantId=common']); - authSettingCommand.push(...['--provider-scope-string', `"${scopes.join(' ')}"`]); - - logger.command('Creating the updated bot authentication setting', authSettingCommand.join(' ')); - - await childProcessUtils.tryExecute(authSettingCommand); - - logger.message('Authentication process finished successfully.'); - } else { - if (manifest.authenticationConnections.length > 0) { - logger.warning(`Could not configure authentication connection automatically.`); - logger.warning(`You must configure one of the following connection types manually in the Azure Portal: -${manifest.authenticationConnections.map((authConn: IAuthenticationConnection) => authConn.serviceProviderId) - .join(', ')}`); + // tslint:disable-next-line:max-func-body-length export-name + public async authenticate(configuration: IConnectConfiguration, manifest: ISkillManifest, logger: ILogger): Promise { + // configuring bot auth settings + logger.message('Checking for authentication settings ...'); + if (manifest.authenticationConnections) { + const aadConfig: IAuthenticationConnection | undefined = manifest.authenticationConnections.find( + (connection: IAuthenticationConnection) => connection.serviceProviderId === 'Azure Active Directory v2'); + if (aadConfig) { + logger.message('Configuring Azure AD connection ...'); + + let connectionName: string = aadConfig.id; + const newScopes: string[] = aadConfig.scopes.split(', '); + let scopes: string[] = newScopes.slice(0); + + // check for existing aad connection + const listAuthSettingsCommand: string[] = ['az', 'bot', 'authsetting', 'list']; + listAuthSettingsCommand.push(...['-n', configuration.botName]); + listAuthSettingsCommand.push(...['-g', configuration.resourceGroup]); + + logger.command('Checking for existing aad connections', listAuthSettingsCommand.join(' ')); + + const connectionsResult: string = await this.childProcessUtils.tryExecute(listAuthSettingsCommand); + const connections: IAzureAuthSetting[] = JSON.parse(connectionsResult); + const aadConnection: IAzureAuthSetting | undefined = connections.find( + (connection: IAzureAuthSetting) => connection.properties.serviceProviderDisplayName === 'Azure Active Directory v2'); + if (aadConnection) { + const settingName: string = aadConnection.name.split('/')[1]; + + // Get current aad auth setting + const showAuthSettingsCommand: string[] = ['az', 'bot', 'authsetting', 'show']; + showAuthSettingsCommand.push(...['-n', configuration.botName]); + showAuthSettingsCommand.push(...['-g', configuration.resourceGroup]); + showAuthSettingsCommand.push(...['-c', settingName]); + + logger.command('Getting current aad auth settings', showAuthSettingsCommand.join(' ')); + + const botAuthSettingResult: string = await this.childProcessUtils.tryExecute(showAuthSettingsCommand); + const botAuthSetting: IAzureAuthSetting = JSON.parse(botAuthSettingResult); + const existingScopes: string[] = botAuthSetting.properties.scopes.split(' '); + scopes = scopes.concat(existingScopes); + connectionName = settingName; + + // delete current aad auth connection + const deleteAuthSettingCommand: string[] = ['az', 'bot', 'authsetting', 'delete']; + deleteAuthSettingCommand.push(...['-n', configuration.botName]); + deleteAuthSettingCommand.push(...['-g', configuration.resourceGroup]); + deleteAuthSettingCommand.push(...['-c', settingName]); + + logger.command('Deleting current bot authentication setting', deleteAuthSettingCommand.join(' ')); + + const deleteResult: string = await this.childProcessUtils.tryExecute(deleteAuthSettingCommand); + } + + // update appsettings.json + logger.message('Updating appsettings.json ...'); + // tslint:disable-next-line:non-literal-require + const appSettings: IAppSettingOauthConnection = require(configuration.appSettingsFile); + + // check for and remove existing aad connections + if (appSettings.oauthConnections) { + appSettings.oauthConnections = appSettings.oauthConnections.filter( + (connection: IOauthConnection) => connection.provider !== 'Azure Active Directory v2'); + } + + // set or add new oauth setting + const oauthSetting: IOauthConnection = { name: connectionName, provider: 'Azure Active Directory v2' }; + if (!appSettings.oauthConnections) { + appSettings.oauthConnections = [oauthSetting]; + } else { + appSettings.oauthConnections.push(oauthSetting); + } + + // update appsettings.json + writeFileSync(configuration.appSettingsFile, JSON.stringify(appSettings, undefined, 4)); + + // Remove duplicate scopes + scopes = [...new Set(scopes)]; + const scopeManifest: IScopeManifest[] = this.createScopeManifest(scopes); + + // get the information of the app + const azureAppShowCommand: string[] = ['az', 'ad', 'app', 'show']; + azureAppShowCommand.push(...['--id', appSettings.microsoftAppId]); + + logger.command('Getting the app information', azureAppShowCommand.join(' ')); + + const azureAppShowResult: string = await this.childProcessUtils.tryExecute(azureAppShowCommand); + const azureAppReplyUrls: IAppShowReplyUrl = JSON.parse(azureAppShowResult); + + // get the Reply Urls from the app + const replyUrlsSet: Set = new Set(azureAppReplyUrls.replyUrls); + // append the necessary Url if it's not already present + replyUrlsSet.add('https://token.botframework.com/.auth/web/redirect'); + const replyUrls: string[] = [...replyUrlsSet]; + + // Update MSA scopes + logger.message('Configuring MSA app scopes ...'); + const azureAppUpdateCommand: string[] = ['az', 'ad', 'app', 'update']; + azureAppUpdateCommand.push(...['--id', appSettings.microsoftAppId]); + azureAppUpdateCommand.push(...['--reply-urls', replyUrls.join(' ')]); + const scopeManifestText: string = JSON.stringify(scopeManifest) + .replace(/\"/g, '\''); + azureAppUpdateCommand.push(...['--required-resource-accesses', `"${scopeManifestText}"`]); + + logger.command('Updating the app information', azureAppUpdateCommand.join(' ')); + + const errorResult: string = await this.childProcessUtils.tryExecute(azureAppUpdateCommand); + // Catch error: Updates to converged applications are not allowed in this version. + if (errorResult) { + logger.warning('Could not configure scopes automatically.'); + // manualScopesRequired = true + } + + logger.message('Updating bot oauth settings ...'); + const authSettingCommand: string[] = ['az', 'bot', 'authsetting', 'create']; + authSettingCommand.push(...['--name', configuration.botName]); + authSettingCommand.push(...['--resource-group', configuration.resourceGroup]); + authSettingCommand.push(...['--setting-name', connectionName]); + authSettingCommand.push(...['--client-id', `"${appSettings.microsoftAppId}"`]); + authSettingCommand.push(...['--client-secret', `"${appSettings.microsoftAppPassword}"`]); + authSettingCommand.push(...['--service', 'Aadv2']); + authSettingCommand.push(...['--parameters', `clientId="${appSettings.microsoftAppId}"`]); + authSettingCommand.push(...[`clientSecret="${appSettings.microsoftAppPassword}"`, 'tenantId=common']); + authSettingCommand.push(...['--provider-scope-string', `"${scopes.join(' ')}"`]); + + logger.command('Creating the updated bot authentication setting', authSettingCommand.join(' ')); + + await this.childProcessUtils.tryExecute(authSettingCommand); + + logger.message('Authentication process finished successfully.'); } else { - logger.warning('There\'s no authentication connection in your Skill\'s manifest.'); + if (manifest.authenticationConnections.length > 0) { + logger.warning(`Could not configure authentication connection automatically.`); + logger.warning(`You must configure one of the following connection types manually in the Azure Portal: + ${manifest.authenticationConnections.map((authConn: IAuthenticationConnection) => authConn.serviceProviderId) + .join(', ')}`); + } else { + logger.warning('There\'s no authentication connection in your Skill\'s manifest.'); + } + // $manualAuthRequired = $true } - // $manualAuthRequired = $true } } } - -const childProcessUtils: ChildProcessUtils = new ChildProcessUtils(); diff --git a/lib/typescript/botskills/src/utils/index.ts b/lib/typescript/botskills/src/utils/index.ts index ee025ebed7..bced0c9d3e 100644 --- a/lib/typescript/botskills/src/utils/index.ts +++ b/lib/typescript/botskills/src/utils/index.ts @@ -4,5 +4,5 @@ */ export { ChildProcessUtils } from './childProcessUtils'; -export { authenticate } from './authenticationUtils'; +export { AuthenticationUtils } from './authenticationUtils'; export { validatePairOfArgs } from './validationUtils'; diff --git a/lib/typescript/botskills/test/flow/connectTest.js b/lib/typescript/botskills/test/flow/connectTest.js index de11a56c61..338bc98605 100644 --- a/lib/typescript/botskills/test/flow/connectTest.js +++ b/lib/typescript/botskills/test/flow/connectTest.js @@ -2,3 +2,362 @@ * Copyright(c) Microsoft Corporation.All rights reserved. * Licensed under the MIT License. */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const sandbox = require('sinon').createSandbox(); +const testLogger = require('../models/testLogger'); +const botskills = require('../../lib/index'); +let logger; +let connector; + +describe("The connect command", function () { + + beforeEach(function () { + fs.writeFileSync(path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + JSON.stringify( + { + "skills": [ + { + "id": "testSkill" + }, + { + "id": "testDispatch" + } + ] + }, + null, 4)); + logger = new testLogger.TestLogger(); + connector = new botskills.ConnectSkill(logger); + }) + + describe("should show an error", function () { + it("when there is no skills File", async function () { + const config = { + botName: '', + localManifest: '', + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: '', + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: Either the 'localManifest' or 'remoteManifest' argument should be passed.`); + }); + + it("when the localManifest points to a nonexisting Skill manifest file", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'nonexistentSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: '', + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: The 'localManifest' argument leads to a non-existing file. Please make sure to provide a valid path to your Skill manifest.`); + }); + + it("when the Skill is missing all mandatory fields", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'invalidSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: '', + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + const errorMessages = [ +`Missing property 'name' of the manifest`, +`Missing property 'id' of the manifest`, +`Missing property 'endpoint' of the manifest`, +`Missing property 'authenticationConnections' of the manifest`, +`Missing property 'actions' of the manifest` + ] + await connector.connectSkill(config); + const ErrorList = logger.getError(); + ErrorList.forEach((errorMessage, index) => { + assert.strictEqual(errorMessage, errorMessages[index]); + }); + }); + + it("when the Skill has an invalid id field", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'invalidSkillid.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: '', + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `The 'id' of the manifest contains some characters not allowed. Make sure the 'id' contains only letters, numbers and underscores, but doesn't start with number.`); + }); + + it("when the remoteManifest points to a nonexisting Skill manifest URL", async function() { + const config = { + botName: '', + localManifest: '', + remoteManifest: 'http://nonexistentSkill.azurewebsites.net/api/skill/manifest', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: '', + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nRequestError: Error: getaddrinfo ENOTFOUND nonexistentskill.azurewebsites.net nonexistentskill.azurewebsites.net:80`); + }); + + it("when the luisFolder leads to a nonexistent folder", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'nonexistentLuisFolder')), + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to the LUIS folder (${config.luisFolder}) leads to a nonexistent folder.`); + }); + + it("when the .lu file path leads to a nonexistent file", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles')), + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to the connectableSkill.lu file leads to a nonexistent file.`); + }); + + it("when the dispatch folder path leads to a nonexistent folder", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'luisFolder')), + dispatchFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'nonexistentDispatchFolder')), + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to the Dispatch folder (${config.dispatchFolder}) leads to a nonexistent folder.`); + }); + + it("when the .dispatch file path leads to a nonexistent file", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: 'nonexistentDispatchFile', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'luisFolder')), + dispatchFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'dispatchFolder')), + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to the ${config.dispatchName}.dispatch file leads to a nonexistent file.`); + }); + + it("when the .luis file path leads to a nonexistent file", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: 'connectableSkill', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'luFolder')), + dispatchFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'dispatchFolder')), + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + sandbox.replace(connector.childProcessUtils, 'execute', (command, args) => { + return Promise.resolve('Mocked function successfully'); + }); + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to ${config.dispatchName}.luis (${path.join(config.luisFolder, config.dispatchName)}.luis) leads to a nonexistent file. Make sure the ludown command is being executed successfully`); + }); + + it("when the dispatch refresh fails and the dispatch .json file is missing", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: 'connectableSkill', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'luisFolder')), + dispatchFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'dispatchFolder')), + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + sandbox.replace(connector.childProcessUtils, 'execute', (command, args) => { + return Promise.resolve('Mocked function successfully'); + }); + await connector.connectSkill(config); + const ErrorList = logger.getError(); + assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while connecting the Skill to the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to ${config.dispatchName}.json (${path.join(config.dispatchFolder, config.dispatchName)}.json) leads to a nonexistent file. Make sure the dispatch refresh command is being executed successfully`); + }); + }); + + describe("should show a warning", function () { + it("when the Skill is already connected", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'testSkill.json')), + remoteManifest: '', + dispatchName: '', + language: '', + luisFolder: '', + dispatchFolder: '', + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + await connector.connectSkill(config); + const WarningList = logger.getWarning(); + assert.strictEqual(WarningList[WarningList.length - 1], `The skill 'Test Skill' is already registered.`); + }); + + }); + + describe("should show a message", function () { + it("when the skill is successfully connected to the Assistant", async function () { + const config = { + botName: '', + localManifest: path.resolve(__dirname, path.join('..', 'mockedFiles', 'connectableSkill.json')), + remoteManifest: '', + dispatchName: 'connectableSkill', + language: '', + luisFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'successfulConnectFiles')), + dispatchFolder: path.resolve(__dirname, path.join('..', 'mockedFiles', 'successfulConnectFiles')), + outFolder: '', + lgOutFolder: '', + skillsFile: path.resolve(__dirname, path.join('..', 'mockedFiles', 'filledSkillsArray.json')), + resourceGroup: '', + appSettingsFile: '', + cognitiveModelsFile: '', + lgLanguage: '', + logger: logger + }; + sandbox.replace(connector.childProcessUtils, 'execute', (command, args) => { + return Promise.resolve('Mocked function successfully'); + }); + sandbox.replace(connector.authenticationUtils, 'authenticate', (configuration, manifest, logger) => { + return Promise.resolve('Mocked function successfully'); + }); + await connector.connectSkill(config); + const MessageList = logger.getMessage(); + assert.strictEqual(MessageList[MessageList.length - 1], `Configuring bot auth settings`); + }); + }); +}); diff --git a/lib/typescript/botskills/test/flow/disconnectTest.js b/lib/typescript/botskills/test/flow/disconnectTest.js index ed6b40c243..3418fa563c 100644 --- a/lib/typescript/botskills/test/flow/disconnectTest.js +++ b/lib/typescript/botskills/test/flow/disconnectTest.js @@ -61,7 +61,7 @@ describe("The disconnect command", function () { }) describe("should show an error", function () { - it("when there's no skills File", async function () { + it("when there is no skills File", async function () { const config = { skillId : "", skillsFile : "", @@ -121,7 +121,7 @@ describe("The disconnect command", function () { assert.strictEqual(ErrorList[ErrorList.length - 1], `Could not find file ${config.dispatchName}.dispatch. Please provide the 'dispatchName' and 'dispatchFolder' parameters.`); }); - it("when the 'dispatch refresh' fails to create the Dispatch JSON file", async function () { + it("when the dispatch refresh fails to create the Dispatch JSON file", async function () { const config = { skillId : "testDispatch", skillsFile: path.resolve(__dirname, '../mockedFiles/filledSkillsArray.json'), @@ -144,7 +144,7 @@ describe("The disconnect command", function () { assert.strictEqual(ErrorList[ErrorList.length - 1], `There was an error while disconnecting the Skill ${config.skillId} from the Assistant:\nError: An error ocurred while updating the Dispatch model:\nError: Path to ${config.dispatchName}.json (${path.join(config.dispatchFolder, config.dispatchName)}.json) leads to a nonexistent file. Make sure the dispatch refresh command is being executed successfully`); }); - it("when the 'lgOutFolder' argument is invalid ", async function () { + it("when the lgOutFolder argument is invalid ", async function () { const config = { skillId : "testDispatch", skillsFile: path.resolve(__dirname, '../mockedFiles/filledSkillsArray.json'), @@ -167,7 +167,7 @@ describe("The disconnect command", function () { assert.strictEqual(ErrorList[ErrorList.length - 1], `The 'lgOutFolder' argument is absent or leads to a non-existing folder.\nPlease make sure to provide a valid path to your LUISGen output folder.`); }); - it("when the 'lgLanguage' argument is invalid", async function () { + it("when the lgLanguage argument is invalid", async function () { const config = { skillId : "testDispatch", skillsFile: path.resolve(__dirname, '../mockedFiles/filledSkillsArray.json'), diff --git a/lib/typescript/botskills/test/flow/listTest.js b/lib/typescript/botskills/test/flow/listTest.js index f849b3b0e9..7d01d0712f 100644 --- a/lib/typescript/botskills/test/flow/listTest.js +++ b/lib/typescript/botskills/test/flow/listTest.js @@ -33,7 +33,7 @@ describe("The list command", function () { describe("should show an error", function () { - it("when there's no skills File", async function () { + it("when there is no skills File", async function () { const config = { skillsFile: '', logger: this.logger @@ -61,7 +61,7 @@ describe("The list command", function () { }); describe("should show a message", function () { - it("when there's no skills connected to the assistant", async function () { + it("when there is no skills connected to the assistant", async function () { const config = { skillsFile: path.resolve(__dirname, '../mockedFiles/emptySkillsArray.json'), logger: this.logger @@ -72,7 +72,7 @@ describe("The list command", function () { assert.strictEqual(MessageList[MessageList.length - 1], `There are no Skills connected to the assistant.`); }); - it("when there's no skills array defined in the Assistant Skills configuration file", async function () { + it("when there is no skills array defined in the Assistant Skills configuration file", async function () { const config = { skillsFile: path.resolve(__dirname, '../mockedFiles/undefinedSkillsArray.json'), logger: this.logger @@ -83,7 +83,7 @@ describe("The list command", function () { assert.strictEqual(MessageList[MessageList.length - 1], `There are no Skills connected to the assistant.`); }); - it("when there's a skill in the Assistant Skills configuration file", async function () { + it("when there is a skill in the Assistant Skills configuration file", async function () { const config = { skillsFile: path.resolve(__dirname, '../mockedFiles/filledSkillsArray.json'), logger: this.logger diff --git a/lib/typescript/botskills/test/mocha.opts b/lib/typescript/botskills/test/mocha.opts index ac5b4d0906..36a355c731 100644 --- a/lib/typescript/botskills/test/mocha.opts +++ b/lib/typescript/botskills/test/mocha.opts @@ -1,3 +1,4 @@ +--reporter mocha-junit-reporter --timeout 50000 --colors --recursive diff --git a/lib/typescript/botskills/test/mockedFiles/connectableSkill.json b/lib/typescript/botskills/test/mockedFiles/connectableSkill.json new file mode 100644 index 0000000000..7b9256186e --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/connectableSkill.json @@ -0,0 +1,27 @@ +{ + "id": "connectableSkill", + "msaAppId": "00000000-0000-0000-0000-000000000000", + "name": "Test Skill", + "endpoint": "https://bftestskill.azurewebsites.net/api/skill/messages", + "description": "This is a test skill to use for testing purpose.", + "authenticationConnections": [], + "actions": [ + { + "id": "connectableSkill_testAction", + "definition": { + "description": "Test Action", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "connectableSkill#Test" + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/dispatchFolder/connectableSkill.dispatch b/lib/typescript/botskills/test/mockedFiles/dispatchFolder/connectableSkill.dispatch new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/typescript/botskills/test/mockedFiles/invalidSkill.json b/lib/typescript/botskills/test/mockedFiles/invalidSkill.json new file mode 100644 index 0000000000..b31cf81c21 --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/invalidSkill.json @@ -0,0 +1,4 @@ +{ + "msaAppId": "00000000-0000-0000-0000-000000000000", + "description": "This is a test skill to use for testing purpose." +} \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/invalidSkillid.json b/lib/typescript/botskills/test/mockedFiles/invalidSkillid.json new file mode 100644 index 0000000000..64c73d2950 --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/invalidSkillid.json @@ -0,0 +1,27 @@ +{ + "id": "test-./Skill", + "msaAppId": "00000000-0000-0000-0000-000000000000", + "name": "Test Skill", + "endpoint": "https://bftestskill.azurewebsites.net/api/skill/messages", + "description": "This is a test skill to use for testing purpose.", + "authenticationConnections": [], + "actions": [ + { + "id": "testSkill_testAction", + "definition": { + "description": "Test Action", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "test#Test" + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/luFolder/connectableSkill.lu b/lib/typescript/botskills/test/mockedFiles/luFolder/connectableSkill.lu new file mode 100644 index 0000000000..6abd3f17cc --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/luFolder/connectableSkill.lu @@ -0,0 +1 @@ +This is a mocked .lu file \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.lu b/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.lu new file mode 100644 index 0000000000..6abd3f17cc --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.lu @@ -0,0 +1 @@ +This is a mocked .lu file \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.luis b/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.luis new file mode 100644 index 0000000000..2b96c45f3c --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/luisFolder/connectableSkill.luis @@ -0,0 +1 @@ +This is a mocked .luis file \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.dispatch b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.dispatch new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.json b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.lu b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.lu new file mode 100644 index 0000000000..6abd3f17cc --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.lu @@ -0,0 +1 @@ +This is a mocked .lu file \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.luis b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.luis new file mode 100644 index 0000000000..2b96c45f3c --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/connectableSkill.luis @@ -0,0 +1 @@ +This is a mocked .luis file \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/filledSkillsArray.json b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/filledSkillsArray.json new file mode 100644 index 0000000000..74a5a8a92a --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/successfulConnectFiles/filledSkillsArray.json @@ -0,0 +1,10 @@ +{ + "skills": [ + { + "id": "testSkill" + }, + { + "id": "testDispatch" + } + ] +} \ No newline at end of file diff --git a/lib/typescript/botskills/test/mockedFiles/testSkill.json b/lib/typescript/botskills/test/mockedFiles/testSkill.json new file mode 100644 index 0000000000..019ec96b7b --- /dev/null +++ b/lib/typescript/botskills/test/mockedFiles/testSkill.json @@ -0,0 +1,27 @@ +{ + "id": "testSkill", + "msaAppId": "00000000-0000-0000-0000-000000000000", + "name": "Test Skill", + "endpoint": "https://bftestskill.azurewebsites.net/api/skill/messages", + "description": "This is a test skill to use for testing purpose.", + "authenticationConnections": [], + "actions": [ + { + "id": "testSkill_testAction", + "definition": { + "description": "Test Action", + "slots": [], + "triggers": { + "utteranceSources": [ + { + "locale": "en", + "source": [ + "test#Test" + ] + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/lib/typescript/botskills/test/models/testLogger.js b/lib/typescript/botskills/test/models/testLogger.js index eb650be75c..6d148a5ff0 100644 --- a/lib/typescript/botskills/test/models/testLogger.js +++ b/lib/typescript/botskills/test/models/testLogger.js @@ -13,12 +13,12 @@ class TestLogger { this._success = new Array(); this._warning = new Array(); this._command = new Array(); - this._isError = false; - this._isVerbose = false; + this.isError = false; + this.isVerbose = false; } error(message) { this._error.push(message); - this._isError = true; + this.isError = true; } message(message) { this._message.push(message); @@ -37,9 +37,6 @@ class TestLogger { this.message(message); } } - get isVerbose() { return this._isVerbose; } - set isVerbose(value) { this._isVerbose = value || false; } - isError() { return this._isError; } getMessage() { return this._message; } getError() { return this._error; } getSuccess() { return this._success; }