Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extends 'teams user app add' command with support for specifying name of the app. Closes #5703 #5709

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/docs/cmd/teams/user/user-app-add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ m365 teams user app add [options]
## Options

```md definition-list
`--id <id>`
`--id [id]`
: The ID of the app to install.

`--name [name]`
: Name of the app to install.

`--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 admin@contoso.com
m365 teams user app add --name HelloWorld --userName admin@contoso.com
```

## Response
Expand Down
126 changes: 123 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) === `{"teamsApp@odata.bind":"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) === `{"teamsApp@odata.bind":"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/4440558e-8c73-4597-abc7-3644a64c4bce"}`) {
Expand All @@ -148,6 +178,96 @@ 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();
return;
}

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
51 changes: 46 additions & 5 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: {
'teamsApp@odata.bind': `${endpoint}/appCatalogs/teamsApps/${args.options.id}`
'teamsApp@odata.bind': `${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();
export default new TeamsUserAppAddCommand();