Skip to content

Commit

Permalink
change for pnp#5446
Browse files Browse the repository at this point in the history
  • Loading branch information
reshmee011 committed Sep 5, 2023
1 parent 515b009 commit ca23553
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 22 deletions.
13 changes: 11 additions & 2 deletions docs/docs/cmd/teams/user/user-app-remove.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ m365 teams user app remove [options]
`--id <id>`
: The unique id of the app instance installed for the user.

`--userId <userId>`
: The ID of the user to uninstall the app for.
`--userId [userId]`
: The ID of the user to uninstall the app for. Specify `userId` or `userName` but not both.

`--userName [userName]`
: The UPN of the user to uninstall the app for. Specify `userId` or `userName` but not both.

`-f, --force`
: Confirm removal of app for user.
Expand All @@ -38,6 +41,12 @@ Uninstall an app for the specified user.
m365 teams user app remove --id YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY= --userId 2609af39-7775-4f94-a3dc-0dd67657e900
```

Uninstall an app for the specified user using its UPN.

```sh
m365 teams user app remove --id YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY= --userName [email protected]
```

## Response

The command won't return a response on success.
99 changes: 83 additions & 16 deletions src/m365/teams/commands/user/user-app-remove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { formatting } from '../../../../utils/formatting.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './user-app-remove.js';

describe(commands.USER_APP_REMOVE, () => {
const userId = '15d7a78e-fd77-4599-97a5-dbb6372846c6';
const userName = '[email protected]';
const appId = 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=';
let log: string[];
let logger: Logger;
let promptOptions: any;
Expand Down Expand Up @@ -72,7 +76,35 @@ describe(commands.USER_APP_REMOVE, () => {
const actual = await command.validate({
options: {
userId: 'invalid',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY='
id: appId
}
}, commandInfo);
assert.notStrictEqual(actual, true);
});

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

it('fails validation if the userName is not a valid UPN.', async () => {
const actual = await command.validate({
options: {
userName: "no-an-email",
id: appId
}
}, commandInfo);
assert.notStrictEqual(actual, true);
});

it('fails validation if the both userId and userName are provided.', async () => {
const actual = await command.validate({
options: {
userId: userId,
userName: userName
}
}, commandInfo);
assert.notStrictEqual(actual, true);
Expand All @@ -81,8 +113,18 @@ describe(commands.USER_APP_REMOVE, () => {
it('passes validation when the input is correct', async () => {
const actual = await command.validate({
options: {
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=',
userId: '15d7a78e-fd77-4599-97a5-dbb6372846c5'
id: appId,
userId: userId
}
}, commandInfo);
assert.strictEqual(actual, true);
});

it('passes validation when the input is correct (userName)', async () => {
const actual = await command.validate({
options: {
id: appId,
userName: userName
}
}, commandInfo);
assert.strictEqual(actual, true);
Expand All @@ -91,8 +133,8 @@ describe(commands.USER_APP_REMOVE, () => {
it('prompts before removing the app when confirmation argument is not passed', async () => {
await command.action(logger, {
options: {
userId: 'c527a470-a882-481c-981c-ee6efaba85c7',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY='
userId: userId,
id: appId
}
} as any);
let promptIssued = false;
Expand All @@ -107,25 +149,25 @@ describe(commands.USER_APP_REMOVE, () => {

await command.action(logger, {
options: {
userId: 'c527a470-a882-481c-981c-ee6efaba85c7',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY='
userId: userId,
id: appId
}
} as any);
assert(requestDeleteSpy.notCalled);
});

it('removes the app for the specified user when confirmation is specified (debug)', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if ((opts.url as string).indexOf(`/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps/YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=`) > -1) {
if ((opts.url as string).indexOf(`/users/${userId}/teamwork/installedApps/${appId}`) > -1) {
return;
}
throw 'Invalid request';
});

await command.action(logger, {
options: {
userId: 'c527a470-a882-481c-981c-ee6efaba85c7',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=',
userId: userId,
id: appId,
debug: true,
force: true
}
Expand All @@ -134,7 +176,7 @@ describe(commands.USER_APP_REMOVE, () => {

it('removes the app for the specified user when prompt is confirmed (debug)', async () => {
sinon.stub(request, 'delete').callsFake((opts) => {
if ((opts.url as string).indexOf(`/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps/YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=`) > -1) {
if ((opts.url as string).indexOf(`/users/${userId}/teamwork/installedApps/${appId}`) > -1) {
return Promise.resolve();
}
throw 'Invalid request';
Expand All @@ -145,8 +187,33 @@ describe(commands.USER_APP_REMOVE, () => {

await command.action(logger, {
options: {
userId: 'c527a470-a882-481c-981c-ee6efaba85c7',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=',
userId: userId,
id: appId,
debug: true
}
} as any);
});

it('removes the app for the specified user using username', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if ((opts.url as string).indexOf(`/users/${userId}/teamwork/installedApps/${appId}`) > -1) {
return Promise.resolve();
}

if ((opts.url as string).indexOf(`/users/${formatting.encodeQueryParameter(userName)}/id`) > -1) {
return { "value": userId };
}

throw 'Invalid request';
});

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

await command.action(logger, {
options: {
userName: userName,
id: appId,
debug: true
}
} as any);
Expand All @@ -166,16 +233,16 @@ describe(commands.USER_APP_REMOVE, () => {
};

sinon.stub(request, 'delete').callsFake(async (opts) => {
if ((opts.url as string).indexOf(`/users/c527a470-a882-481c-981c-ee6efaba85c7/teamwork/installedApps/YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=`) > -1) {
if ((opts.url as string).indexOf(`/users/${userId}/teamwork/installedApps/${appId}`) > -1) {
throw error;
}
throw 'Invalid request';
});

await assert.rejects(command.action(logger, {
options: {
userId: 'c527a470-a882-481c-981c-ee6efaba85c7',
id: 'YzUyN2E0NzAtYTg4Mi00ODFjLTk4MWMtZWU2ZWZhYmE4NWM3IyM0ZDFlYTA0Ny1mMTk2LTQ1MGQtYjJlOS0wZDI4NTViYTA1YTY=',
userId: userId,
id: appId,
force: true
}
} as any), new CommandError(error.error.message));
Expand Down
41 changes: 37 additions & 4 deletions src/m365/teams/commands/user/user-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 @@ -13,6 +14,7 @@ interface CommandArgs {
interface Options extends GlobalOptions {
id: string;
userId: string;
userName: string;
force?: boolean;
}

Expand All @@ -31,11 +33,14 @@ class TeamsUserAppRemoveCommand extends GraphCommand {
this.#initTelemetry();
this.#initOptions();
this.#initValidators();
this.#initOptionSets();
}

#initTelemetry(): void {
this.telemetry.push((args: CommandArgs) => {
Object.assign(this.telemetryProperties, {
userId: typeof args.options.userId !== 'undefined',
userName: typeof args.options.userName !== 'undefined',
force: (!!args.options.force).toString()
});
});
Expand All @@ -47,7 +52,10 @@ class TeamsUserAppRemoveCommand extends GraphCommand {
option: '--id <id>'
},
{
option: '--userId <userId>'
option: '--userId [userId]'
},
{
option: '--userName [userName]'
},
{
option: '-f, --force'
Expand All @@ -58,21 +66,30 @@ class TeamsUserAppRemoveCommand extends GraphCommand {
#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (!validation.isValidGuid(args.options.userId)) {
if (args.options.userId && !validation.isValidGuid(args.options.userId)) {
return `${args.options.userId} is not a valid GUID`;
}

if (args.options.userName && !validation.isValidUserPrincipalName(args.options.userName)) {
return `${args.options.userName} is not a valid userName`;
}

return true;
}
);
}

#initOptionSets(): void {
this.optionSets.push({ options: ['userId', 'userName'] });
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
const removeApp = async (): Promise<void> => {
const userId: string = (await this.getUserId(args)).value;
const endpoint: string = `${this.resource}/v1.0`;

const requestOptions: CliRequestOptions = {
url: `${endpoint}/users/${args.options.userId}/teamwork/installedApps/${args.options.id}`,
url: `${endpoint}/users/${formatting.encodeQueryParameter(userId)}/teamwork/installedApps/${args.options.id}`,
headers: {
'accept': 'application/json;odata.metadata=none'
},
Expand All @@ -95,14 +112,30 @@ class TeamsUserAppRemoveCommand extends GraphCommand {
type: 'confirm',
name: 'continue',
default: false,
message: `Are you sure you want to remove the app with id ${args.options.id} for user ${args.options.userId}?`
message: `Are you sure you want to remove the app with id ${args.options.id} for user ${args.options.userId ?? args.options.userName}?`
});

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

private async getUserId(args: CommandArgs): Promise<{ value: string }> {
if (args.options.userId) {
return { value: args.options.userId };
}

const requestOptions: CliRequestOptions = {
url: `${this.resource}/v1.0/users/${formatting.encodeQueryParameter(args.options.userName)}/id`,
headers: {
accept: 'application/json;odata.metadata=none'
},
responseType: 'json'
};

return request.get<{ value: string; }>(requestOptions);
}
}

export default new TeamsUserAppRemoveCommand();

0 comments on commit ca23553

Please sign in to comment.