diff --git a/docs/docs/cmd/teams/user/user-app-add.mdx b/docs/docs/cmd/teams/user/user-app-add.mdx index 34a602b7a06..6eb7e7d20b4 100644 --- a/docs/docs/cmd/teams/user/user-app-add.mdx +++ b/docs/docs/cmd/teams/user/user-app-add.mdx @@ -13,8 +13,11 @@ m365 teams user app add [options] ## Options ```md definition-list -`--id ` -: The ID of the app to install. +`--id [id]` +: The ID of the app to install. Specify either `id` or `name` but not both. + +`--name [name]` +: Name of the app to install. Specify either `id` or `name` but not both. `--userId [userId]` : The ID of the user to install the app for. Specify either `userId` or `userName` but not both. @@ -31,16 +34,16 @@ The `id` has to be the ID of the app from the Microsoft Teams App Catalog. Do no ## Examples -Install an app from the catalog for the specified user by id. +Install an app by id from the catalog for the specified user by id. ```sh m365 teams user app add --id 4440558e-8c73-4597-abc7-3644a64c4bce --userId 2609af39-7775-4f94-a3dc-0dd67657e900 ``` -Install an app from the catalog for the specified user by name. +Install an app by name from the catalog for the specified user by name. ```sh -m365 teams user app add --id 4440558e-8c73-4597-abc7-3644a64c4bce --userName admin@contoso.com +m365 teams user app add --name HelloWorld --userName admin@contoso.com ``` ## Response diff --git a/src/m365/teams/commands/user/user-app-add.spec.ts b/src/m365/teams/commands/user/user-app-add.spec.ts index cf8d206dcc4..fc9c5a9eca4 100644 --- a/src/m365/teams/commands/user/user-app-add.spec.ts +++ b/src/m365/teams/commands/user/user-app-add.spec.ts @@ -12,6 +12,7 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './user-app-add.js'; +import { settingsNames } from '../../../../settingsNames.js'; describe(commands.USER_APP_ADD, () => { let log: string[]; @@ -45,7 +46,10 @@ describe(commands.USER_APP_ADD, () => { afterEach(() => { sinonUtil.restore([ - request.post + request.get, + request.post, + cli.getSettingWithDefaultValue, + cli.handleMultipleResultsFound ]); }); @@ -92,6 +96,32 @@ describe(commands.USER_APP_ADD, () => { assert.notStrictEqual(actual, true); }); + it('fails validation if id and name are specified', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { id: '15d7a78e-fd77-4599-97a5-dbb6372846c5', name: 'TeamsApp', userId: '15d7a78e-fd77-4599-97a5-dbb6372846c5' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if neither id nor name are specified', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + const actual = await command.validate({ options: { userId: '15d7a78e-fd77-4599-97a5-dbb6372846c5' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('passes validation when the input is correct', async () => { const actual = await command.validate({ options: { @@ -112,7 +142,7 @@ describe(commands.USER_APP_ADD, () => { assert.strictEqual(actual, true); }); - it('adds app from the catalog for the specified user by id', async () => { + it('adds app by id from the catalog for the specified user by id', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps` && JSON.stringify(opts.data) === `{"teamsApp@odata.bind":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/4440558e-8c73-4597-abc7-3644a64c4bce"}`) { @@ -130,7 +160,7 @@ describe(commands.USER_APP_ADD, () => { } as any); }); - it('adds app from the catalog for the specified user by name', async () => { + it('adds app by id from the catalog for the specified user by name', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/users/admin%40contoso.com/teamwork/installedApps` && JSON.stringify(opts.data) === `{"teamsApp@odata.bind":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/4440558e-8c73-4597-abc7-3644a64c4bce"}`) { @@ -148,6 +178,95 @@ describe(commands.USER_APP_ADD, () => { } as any); }); + it('adds app by name from the catalog for the specified user', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps?$filter=displayName eq 'TeamsApp'`) { + return { + "value": [ + { + "id": "YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=", + "displayName": "TeamsApp" + } + ] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps` && + JSON.stringify(opts.data) === `{"teamsApp@odata.bind":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY="}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + userId: 'c527a470-a882-481c-981c-ee6efaba85c7', + name: 'TeamsApp' + } + } as any); + }); + + it('fails to get teams app when app by name does not exists', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps?$filter=displayName eq 'TeamsApp'`) { + return { value: [] }; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { + options: { + debug: true, + userId: 'c527a470-a882-481c-981c-ee6efaba85c7', + name: 'TeamsApp' + } + } as any), new CommandError('The specified Teams app does not exist')); + }); + + it('handles selecting single result when multiple teams apps with the specified name found and cli is set to prompt', async () => { + let addRequestIssued = false; + + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps?$filter=displayName eq 'TeamsApp'`) { + return { + "value": [ + { + "id": "ZDczZWVjZmQtYzFkNS00MzY2LWJkMjEtZDUyOTM1ZThkYjkxIyMxLjYuMC4wIyNQdWJsaXNoZWQ=", + "displayName": "TeamsApp" + }, + { + "id": "NmY0ODM2N2EtMjVmMC00NjNmLTlmMGQtMmFiZTBiYmYzNzRjIyMxLjAuMCMjUHVibGlzaGVk", + "displayName": "TeamsApp" + } + ] + }; + } + + throw 'Invalid request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: "ZDczZWVjZmQtYzFkNS00MzY2LWJkMjEtZDUyOTM1ZThkYjkxIyMxLjYuMC4wIyNQdWJsaXNoZWQ=" }); + + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps` && + JSON.stringify(opts.data) === `{"teamsApp@odata.bind":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/ZDczZWVjZmQtYzFkNS00MzY2LWJkMjEtZDUyOTM1ZThkYjkxIyMxLjYuMC4wIyNQdWJsaXNoZWQ="}`) { + addRequestIssued = true; + return Promise.resolve(); + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { verbose: true, userId: 'c527a470-a882-481c-981c-ee6efaba85c7', name: 'TeamsApp' } }); + assert(addRequestIssued); + }); + it('correctly handles error while installing teams app', async () => { const error = { "error": { diff --git a/src/m365/teams/commands/user/user-app-add.ts b/src/m365/teams/commands/user/user-app-add.ts index 525fb4c6747..385527043c0 100644 --- a/src/m365/teams/commands/user/user-app-add.ts +++ b/src/m365/teams/commands/user/user-app-add.ts @@ -1,3 +1,4 @@ +import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; @@ -11,7 +12,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + name?: string; userId?: string; userName?: string; } @@ -37,6 +39,8 @@ class TeamsUserAppAddCommand extends GraphCommand { #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { + id: typeof args.options.id !== 'undefined', + name: typeof args.options.name !== 'undefined', userId: typeof args.options.userId !== 'undefined', userName: typeof args.options.userName !== 'undefined' }); @@ -46,7 +50,10 @@ class TeamsUserAppAddCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: '--id ' + option: '--id [id]' + }, + { + option: '--name [name]' }, { option: '--userId [userId]' @@ -60,7 +67,7 @@ class TeamsUserAppAddCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.id)) { + if (args.options.id && !validation.isValidGuid(args.options.id)) { return `${args.options.id} is not a valid GUID`; } @@ -78,13 +85,19 @@ class TeamsUserAppAddCommand extends GraphCommand { } #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'name'] }); this.optionSets.push({ options: ['userId', 'userName'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { + const appId: string = await this.getAppId(args); const userId: string = (args.options.userId ?? args.options.userName) as string; const endpoint: string = `${this.resource}/v1.0`; + if (this.verbose) { + await logger.logToStderr(`Removing app with ID ${appId} for user ${args.options.userId}`); + } + const requestOptions: CliRequestOptions = { url: `${endpoint}/users/${formatting.encodeQueryParameter(userId)}/teamwork/installedApps`, headers: { @@ -93,7 +106,7 @@ class TeamsUserAppAddCommand extends GraphCommand { }, responseType: 'json', data: { - 'teamsApp@odata.bind': `${endpoint}/appCatalogs/teamsApps/${args.options.id}` + 'teamsApp@odata.bind': `${endpoint}/appCatalogs/teamsApps/${appId}` } }; @@ -104,6 +117,34 @@ class TeamsUserAppAddCommand extends GraphCommand { this.handleRejectedODataJsonPromise(err); } } + + private async getAppId(args: CommandArgs): Promise { + if (args.options.id) { + return args.options.id; + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/appCatalogs/teamsApps?$filter=displayName eq '${formatting.encodeQueryParameter(args.options.name as string)}'`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const response = await request.get<{ value: { id: string; }[] }>(requestOptions); + + if (response.value.length === 1) { + return response.value[0].id; + } + + if (response.value.length === 0) { + throw `The specified Teams app does not exist`; + } + + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', response.value); + const result: { id: string } = (await cli.handleMultipleResultsFound(`Multiple Teams apps with name '${args.options.name}' found.`, resultAsKeyValuePair)) as { id: string }; + return result.id; + } } export default new TeamsUserAppAddCommand(); \ No newline at end of file