Skip to content

Commit

Permalink
Extends 'teams user app add' command with support for specifying name…
Browse files Browse the repository at this point in the history
… of the app. Closes pnp#5703
  • Loading branch information
nanddeepn authored and waldekmastykarz committed Jan 20, 2024
1 parent d2ff47d commit 96e49d5
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 12 deletions.
13 changes: 8 additions & 5 deletions docs/docs/cmd/teams/user/user-app-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ m365 teams user app add [options]
## Options

```md definition-list
`--id <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.
Expand All @@ -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 [email protected]
m365 teams user app add --name HelloWorld --userName [email protected]
```

## Response
Expand Down
125 changes: 122 additions & 3 deletions src/m365/teams/commands/user/user-app-add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -45,7 +46,10 @@ describe(commands.USER_APP_ADD, () => {

afterEach(() => {
sinonUtil.restore([
request.post
request.get,
request.post,
cli.getSettingWithDefaultValue,
cli.handleMultipleResultsFound
]);
});

Expand Down Expand Up @@ -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: {
Expand All @@ -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) === `{"[email protected]":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/4440558e-8c73-4597-abc7-3644a64c4bce"}`) {
Expand All @@ -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) === `{"[email protected]":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/4440558e-8c73-4597-abc7-3644a64c4bce"}`) {
Expand All @@ -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) === `{"[email protected]":"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) === `{"[email protected]":"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": {
Expand Down
49 changes: 45 additions & 4 deletions src/m365/teams/commands/user/user-app-add.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,7 +12,8 @@ interface CommandArgs {
}

interface Options extends GlobalOptions {
id: string;
id?: string;
name?: string;
userId?: string;
userName?: string;
}
Expand All @@ -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'
});
Expand All @@ -46,7 +50,10 @@ class TeamsUserAppAddCommand extends GraphCommand {
#initOptions(): void {
this.options.unshift(
{
option: '--id <id>'
option: '--id [id]'
},
{
option: '--name [name]'
},
{
option: '--userId [userId]'
Expand All @@ -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`;
}

Expand All @@ -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<void> {
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: {
Expand All @@ -93,7 +106,7 @@ class TeamsUserAppAddCommand extends GraphCommand {
},
responseType: 'json',
data: {
'[email protected]': `${endpoint}/appCatalogs/teamsApps/${args.options.id}`
'[email protected]': `${endpoint}/appCatalogs/teamsApps/${appId}`
}
};

Expand All @@ -104,6 +117,34 @@ class TeamsUserAppAddCommand extends GraphCommand {
this.handleRejectedODataJsonPromise(err);
}
}

private async getAppId(args: CommandArgs): Promise<string> {
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();

0 comments on commit 96e49d5

Please sign in to comment.