Skip to content
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
15 changes: 9 additions & 6 deletions docs/docs/cmd/teams/app/app-remove.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ m365 teams app remove [options]
## Options

```md definition-list
`-i, --id <id>`
: ID of the Teams app to remove. Needs to be available in your organization\'s app catalog.
`-i, --id [id]`
: ID of the Teams app to remove. Needs to be available in your organization\'s app catalog. Specify either `id` or `name`.

`-n, --name [name]`
: Name of the Teams app to remove. Needs to be available in your organization\'s app catalog. Specify either `id` or `name`.

`-f, --force`
: Don't prompt for confirming removing the app
: Don't prompt for confirming removing the app.
```

<Global />
Expand All @@ -28,16 +31,16 @@ You can only remove a Teams app as a global administrator.

## Examples

Remove the Teams app with ID _83cece1e-938d-44a1-8b86-918cf6151957_ from the organization's app catalog. Will prompt for confirmation before actually removing the app.
Remove the Teams app by ID from the organization's app catalog. Will prompt for confirmation before actually removing the app.

```sh
m365 teams app remove --id 83cece1e-938d-44a1-8b86-918cf6151957
```

Remove the Teams app with ID _83cece1e-938d-44a1-8b86-918cf6151957_ from the organization's app catalog. Don't prompt for confirmation.
Remove the Teams app by name from the organization's app catalog. Don't prompt for confirmation.

```sh
m365 teams app remove --id 83cece1e-938d-44a1-8b86-918cf6151957 --force
m365 teams app remove --name HelloWorld --force
```

## Response
Expand Down
99 changes: 90 additions & 9 deletions src/m365/teams/commands/app/app-remove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe(commands.APP_REMOVE, () => {

afterEach(() => {
sinonUtil.restore([
request.get,
request.delete,
Cli.prompt
]);
Expand All @@ -65,6 +66,24 @@ describe(commands.APP_REMOVE, () => {
assert.notStrictEqual(command.description, null);
});

it('fails validation if both id and name options are passed', async () => {
const actual = await command.validate({
options: {
id: 'e3e29acb-8c79-412b-b746-e6c39ff4cd22',
name: 'TeamsApp'
}
}, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if both id and name options are not passed', async () => {
const actual = await command.validate({
options: {
}
}, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if the id is not a valid GUID.', async () => {
const actual = await command.validate({
options: { id: 'invalid' }
Expand All @@ -81,7 +100,7 @@ describe(commands.APP_REMOVE, () => {
assert.strictEqual(actual, true);
});

it('remove Teams app in the tenant app catalog with confirmation', async () => {
it('removes Teams app by id in the tenant app catalog with confirmation (debug)', async () => {
let removeTeamsAppCalled = false;
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/e3e29acb-8c79-412b-b746-e6c39ff4cd22`) {
Expand All @@ -96,7 +115,7 @@ describe(commands.APP_REMOVE, () => {
assert(removeTeamsAppCalled);
});

it('remove Teams app in the tenant app catalog with confirmation (debug)', async () => {
it('removes Teams app by id in the tenant app catalog without confirmation', async () => {
let removeTeamsAppCalled = false;
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/e3e29acb-8c79-412b-b746-e6c39ff4cd22`) {
Expand All @@ -107,12 +126,36 @@ describe(commands.APP_REMOVE, () => {
throw 'Invalid request';
});

await command.action(logger, { options: { debug: true, filePath: 'teamsapp.zip', id: `e3e29acb-8c79-412b-b746-e6c39ff4cd22`, force: true } });
sinon.stub(Cli, 'prompt').resolves({ continue: true });

await command.action(logger, { options: { debug: true, filePath: 'teamsapp.zip', id: `e3e29acb-8c79-412b-b746-e6c39ff4cd22` } });
assert(removeTeamsAppCalled);
});

it('remove Teams app in the tenant app catalog without confirmation', async () => {
it('aborts removing Teams app when prompt not confirmed', async () => {
sinon.stub(Cli, 'prompt').resolves({ continue: false });

command.action(logger, { options: { id: `e3e29acb-8c79-412b-b746-e6c39ff4cd22` } });
assert(requests.length === 0);
});

it('removes Teams app by name in the tenant app catalog without confirmation (debug)', async () => {
let removeTeamsAppCalled = false;

sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps?$filter=displayName eq 'TeamsApp'&$select=id`) {
return {
"value": [
{
"id": "e3e29acb-8c79-412b-b746-e6c39ff4cd22",
"displayName": "TeamsApp"
}
]
};
}
throw 'Invalid request';
});

sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/e3e29acb-8c79-412b-b746-e6c39ff4cd22`) {
removeTeamsAppCalled = true;
Expand All @@ -124,15 +167,53 @@ describe(commands.APP_REMOVE, () => {

sinon.stub(Cli, 'prompt').resolves({ continue: true });

await command.action(logger, { options: { debug: true, filePath: 'teamsapp.zip', id: `e3e29acb-8c79-412b-b746-e6c39ff4cd22` } });
await command.action(logger, { options: { debug: true, name: 'TeamsApp' } });
assert(removeTeamsAppCalled);
});

it('aborts removing Teams app when prompt not confirmed', async () => {
sinon.stub(Cli, 'prompt').resolves({ continue: false });
it('fails to get Teams app when app 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'&$select=id`) {
return { value: [] };
}
throw 'Invalid request';
});

command.action(logger, { options: { id: `e3e29acb-8c79-412b-b746-e6c39ff4cd22` } });
assert(requests.length === 0);
await assert.rejects(command.action(logger, {
options: {
debug: true,
name: 'TeamsApp',
force: true
}
} as any), new CommandError('The specified Teams app does not exist'));
});

it('handles error when multiple Teams apps with the specified name found', async () => {
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/appCatalogs/teamsApps?$filter=displayName eq 'TeamsApp'&$select=id`) {
return {
"value": [
{
"id": "e3e29acb-8c79-412b-b746-e6c39ff4cd22",
"displayName": "TeamsApp"
},
{
"id": "5b31c38c-2584-42f0-aa47-657fb3a84230",
"displayName": "TeamsApp"
}
]
};
}
throw 'Invalid request';
});

await assert.rejects(command.action(logger, {
options: {
debug: true,
name: 'TeamsApp',
force: true
}
} as any), new CommandError(`Multiple Teams apps with name 'TeamsApp' found. Found: e3e29acb-8c79-412b-b746-e6c39ff4cd22, 5b31c38c-2584-42f0-aa47-657fb3a84230.`));
});

it('correctly handles error when removing app', async () => {
Expand Down
76 changes: 60 additions & 16 deletions src/m365/teams/commands/app/app-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Cli } from '../../../../cli/Cli.js';
import { Logger } from '../../../../cli/Logger.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { formatting } from '../../../../utils/formatting.js';
import { validation } from '../../../../utils/validation.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
Expand All @@ -12,7 +13,8 @@ interface CommandArgs {

interface Options extends GlobalOptions {
force?: boolean;
id: string;
id?: string;
name?: string;
}

class TeamsAppRemoveCommand extends GraphCommand {
Expand All @@ -30,20 +32,26 @@ class TeamsAppRemoveCommand extends GraphCommand {
this.#initTelemetry();
this.#initOptions();
this.#initValidators();
this.#initOptionSets();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
force: (!(!args.options.force)).toString()
force: (!(!args.options.force)).toString(),
id: typeof args.options.id !== 'undefined',
name: typeof args.options.name !== 'undefined'
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-i, --id <id>'
option: '-i, --id [id]'
},
{
option: '-n, --name [name]'
},
{
option: '-f, --force'
Expand All @@ -54,7 +62,7 @@ class TeamsAppRemoveCommand 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 @@ -63,22 +71,26 @@ class TeamsAppRemoveCommand extends GraphCommand {
);
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
const { id: appId } = args.options;
#initOptionSets(): void {
this.optionSets.push({ options: ['id', 'name'] });
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
const removeApp = async (): Promise<void> => {
if (this.verbose) {
await logger.logToStderr(`Removing app with ID ${args.options.id}`);
}
try {
const appId: string = await this.getAppId(args.options);

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/appCatalogs/teamsApps/${appId}`,
headers: {
accept: 'application/json;odata.metadata=none'
if (this.verbose) {
await logger.logToStderr(`Removing app with ID ${args.options.id}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await logger.logToStderr(`Removing app with ID ${args.options.id}`);
await logger.logToStderr(`Removing app with ID ${appId}`);

}
};

try {
const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/appCatalogs/teamsApps/${appId}`,
headers: {
accept: 'application/json;odata.metadata=none'
}
};

await request.delete(requestOptions);
}
catch (err: any) {
Expand All @@ -94,14 +106,46 @@ class TeamsAppRemoveCommand extends GraphCommand {
type: 'confirm',
name: 'continue',
default: false,
message: `Are you sure you want to remove the Teams app ${appId} from the app catalog?`
message: `Are you sure you want to remove the Teams app ${args.options.id || args.options.name} from the app catalog?`
});

if (result.continue) {
await removeApp();
}
}
}

private async getAppId(options: Options): Promise<any> {
if (options.id) {
return options.id;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a verbose logging line here

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/appCatalogs/teamsApps?$filter=displayName eq '${formatting.encodeQueryParameter(options.name as string)}'&$select=id`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

const response = await request.get<{ value: { id: string; }[] }>(requestOptions);
const app: { id: string; } | undefined = response.value[0];

if (!app) {
throw `The specified Teams app does not exist`;
}

if (response.value.length > 1) {
const resultAsKeyValuePair: any = {};
response.value.forEach((obj) => {
resultAsKeyValuePair[obj.id] = obj;
});

return Cli.handleMultipleResultsFound(`Multiple Teams apps with name '${options.name}' found.`, resultAsKeyValuePair);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should await this and type the result.

Suggested change
return Cli.handleMultipleResultsFound(`Multiple Teams apps with name '${options.name}' found.`, resultAsKeyValuePair);
const result = await Cli.handleMultipleResultsFound<{ id: string; }>(`Multiple Teams apps with name '${options.name}' found.`, resultAsKeyValuePair);
return result.id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, and we also missed a test case for this :-)

}

return app.id;
}
}

export default new TeamsAppRemoveCommand();