Skip to content

Commit

Permalink
Adds 'teams team app list' command. Closes #4129
Browse files Browse the repository at this point in the history
  • Loading branch information
MathijsVerbeeck authored and milanholemans committed Nov 25, 2022
1 parent dcd63df commit e6a208f
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 2 deletions.
78 changes: 78 additions & 0 deletions docs/docs/cmd/teams/team/team-app-list.md
@@ -0,0 +1,78 @@
# teams team app list

List apps installed in the specified team

## Usage

```sh
m365 teams team app list [options]
```

## Options

`-i, --teamId [teamId]`
: The id of the Microsoft Teams team. Specify either `teamId` or `teamName` but not both.

`-n, --teamName [teamName]`
: The name of the Microsoft Teams team. Specify either `teamId` or `teamName` but not both.


--8<-- "docs/cmd/_global.md"

## Examples

List applications installed in the specified Microsoft Teams team by id

```sh
m365 teams team app list --teamId 2eaf7dcd-7e83-4c3a-94f7-932a1299c844
```

List applications installed in the specified Microsoft Teams team by name

```sh
m365 teams team app list --teamName "Team Name"
```

## Response

=== "JSON"

```json
[
{
"id": "MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMxNGQ2OTYyZC02ZWViLTRmNDgtODg5MC1kZTU1NDU0YmIxMzY=",
"teamsApp": {
"id": "14d6962d-6eeb-4f48-8890-de55454bb136",
"externalId": null,
"displayName": "Activity",
"distributionMethod": "store"
},
"teamsAppDefinition": {
"id": "MTRkNjk2MmQtNmVlYi00ZjQ4LTg4OTAtZGU1NTQ1NGJiMTM2IyMxLjAjI1B1Ymxpc2hlZA==",
"teamsAppId": "14d6962d-6eeb-4f48-8890-de55454bb136",
"displayName": "Activity",
"version": "1.0",
"publishingState": "published",
"shortDescription": "Activity app bar entry.",
"description": "Activity app bar entry.",
"lastModifiedDateTime": null,
"createdBy": null
}
}
]
```

=== "Text"

```text
id displayName distributionMethod
---------------------------------------------------------------------------------------------------- ----------- ------------------
MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMxNGQ2OTYyZC02ZWViLTRmNDgtODg5MC1kZTU1NDU0YmIxMzY= Activity store
```

=== "CSV"

```csv
id,displayName,distributionMethod
MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMxNGQ2OTYyZC02ZWViLTRmNDgtODg5MC1kZTU1NDU0YmIxMzY=,Activity,store
```
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Expand Up @@ -696,6 +696,7 @@ nav:
- team remove: cmd/teams/team/team-remove.md
- team set: cmd/teams/team/team-set.md
- team unarchive: cmd/teams/team/team-unarchive.md
- team app list: cmd/teams/team/team-app-list.md
- user:
- user add: cmd/aad/o365group/o365group-user-add.md
- user list: cmd/teams/user/user-list.md
Expand Down
1 change: 1 addition & 0 deletions src/m365/teams/commands.ts
Expand Up @@ -48,6 +48,7 @@ export default {
TAB_LIST: `${prefix} tab list`,
TAB_REMOVE: `${prefix} tab remove`,
TEAM_ADD: `${prefix} team add`,
TEAM_APP_LIST: `${prefix} team app list`,
TEAM_ARCHIVE: `${prefix} team archive`,
TEAM_CLONE: `${prefix} team clone`,
TEAM_GET: `${prefix} team get`,
Expand Down
131 changes: 131 additions & 0 deletions src/m365/teams/commands/team/team-app-list.spec.ts
@@ -0,0 +1,131 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import appInsights from '../../../../appInsights';
import auth from '../../../../Auth';
import { Cli } from '../../../../cli/Cli';
import { CommandInfo } from '../../../../cli/CommandInfo';
import { Logger } from '../../../../cli/Logger';
import Command, { CommandError } from '../../../../Command';
import { odata } from '../../../../utils/odata';
import { pid } from '../../../../utils/pid';
import { sinonUtil } from '../../../../utils/sinonUtil';
import commands from '../../commands';
import * as TeamGetCommand from './team-get';
const command: Command = require('./team-app-list');

describe(commands.TEAM_APP_LIST, () => {
const teamId = '0ad55b5d-6a79-467b-ad21-d4bef7948a79';
const teamName = 'Contoso Team';
const jsonResponse = JSON.parse(`[{"id":"MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMxNGQ2OTYyZC02ZWViLTRmNDgtODg5MC1kZTU1NDU0YmIxMzY=","teamsApp":{"id":"14d6962d-6eeb-4f48-8890-de55454bb136","externalId":null,"displayName":"Activity","distributionMethod":"store"},"teamsAppDefinition":{"id":"MTRkNjk2MmQtNmVlYi00ZjQ4LTg4OTAtZGU1NTQ1NGJiMTM2IyMxLjAjI1B1Ymxpc2hlZA==","teamsAppId":"14d6962d-6eeb-4f48-8890-de55454bb136","displayName":"Activity","version":"1.0","publishingState":"published","shortDescription":"Activity app bar entry.","description":"Activity app bar entry.","lastModifiedDateTime":null,"createdBy":null}},{"id":"MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMyMGMzNDQwZC1jNjdlLTQ0MjAtOWY4MC0wZTUwYzM5NjkzZGY=","teamsApp":{"id":"20c3440d-c67e-4420-9f80-0e50c39693df","externalId":null,"displayName":"Calling","distributionMethod":"store"},"teamsAppDefinition":{"id":"MjBjMzQ0MGQtYzY3ZS00NDIwLTlmODAtMGU1MGMzOTY5M2RmIyMxLjAjI1B1Ymxpc2hlZA==","teamsAppId":"20c3440d-c67e-4420-9f80-0e50c39693df","displayName":"Calling","version":"1.0","publishingState":"published","shortDescription":"Calling app bar entry.","description":"Calling app bar entry.","lastModifiedDateTime":null,"createdBy":null}},{"id":"MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMyYTg0OTE5Zi01OWQ4LTQ0NDEtYTk3NS0yYThjMjY0M2I3NDE=","teamsApp":{"id":"2a84919f-59d8-4441-a975-2a8c2643b741","externalId":null,"displayName":"Teams","distributionMethod":"store"},"teamsAppDefinition":{"id":"MmE4NDkxOWYtNTlkOC00NDQxLWE5NzUtMmE4YzI2NDNiNzQxIyMxLjAjI1B1Ymxpc2hlZA==","teamsAppId":"2a84919f-59d8-4441-a975-2a8c2643b741","displayName":"Teams","version":"1.0","publishingState":"published","shortDescription":"Teams app bar entry.","description":"Teams app bar entry.","lastModifiedDateTime":null,"createdBy":null}}]`);
const friendlyResponse = [{ "id": "MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMxNGQ2OTYyZC02ZWViLTRmNDgtODg5MC1kZTU1NDU0YmIxMzY=", "displayName": "Activity", "distributionMethod": "store" }, { "id": "MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMyMGMzNDQwZC1jNjdlLTQ0MjAtOWY4MC0wZTUwYzM5NjkzZGY=", "displayName": "Calling", "distributionMethod": "store" }, { "id": "MGFkNTViNWQtNmE3OS00NjdiLWFkMjEtZDRiZWY3OTQ4YTc5IyMyYTg0OTE5Zi01OWQ4LTQ0NDEtYTk3NS0yYThjMjY0M2I3NDE=", "displayName": "Teams", "distributionMethod": "store" }];

let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;

before(() => {
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
sinon.stub(appInsights, 'trackEvent').callsFake(() => { });
sinon.stub(pid, 'getProcessName').callsFake(() => '');
auth.service.connected = true;
commandInfo = Cli.getCommandInfo(command);
});

beforeEach(() => {
log = [];
logger = {
log: (msg: string) => {
log.push(msg);
},
logRaw: (msg: string) => {
log.push(msg);
},
logToStderr: (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
(command as any).items = [];
});

afterEach(() => {
sinonUtil.restore([
odata.getAllItems,
Cli.executeCommandWithOutput
]);
});

after(() => {
sinonUtil.restore([
auth.restoreAuth,
appInsights.trackEvent,
pid.getProcessName
]);
auth.service.connected = false;
});

it('has correct name', () => {
assert.strictEqual(command.name.startsWith(commands.TEAM_APP_LIST), true);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

it('fails validation if the teamId is not a valid GUID', async () => {
const actual = await command.validate({ options: { teamId: 'invalid' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation if the teamId is a valid GUID', async () => {
const actual = await command.validate({ options: { teamId: teamId } }, commandInfo);
assert.strictEqual(actual, true);
});

it('fails when team does not exist in tenant', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => {
if (command === TeamGetCommand) {
throw 'The specified team does not exist in the Microsoft Teams';
}
throw 'Invalid request';
});

await assert.rejects(command.action(logger, { options: { teamName: teamName, verbose: true } }),
new CommandError('The specified team does not exist in the Microsoft Teams'));
});

it('lists team apps for team specified by name with output json', async () => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(async (command): Promise<any> => {
if (command === TeamGetCommand) {
return { "stdout": JSON.stringify({ id: teamId }) };
}

throw 'Invalid request';
});

sinon.stub(odata, 'getAllItems').callsFake(async (url: string): Promise<any> => {
if (url === `https://graph.microsoft.com/v1.0/teams/${teamId}/installedApps?$expand=teamsApp,teamsAppDefinition`) {
return jsonResponse;
}

throw 'Invalid response';
});

await command.action(logger, { options: { teamName: teamName, verbose: true, output: 'json' } });
assert(loggerLogSpy.calledWith(jsonResponse));
});

it('lists team apps for team specified by id with output csv', async () => {
sinon.stub(odata, 'getAllItems').callsFake(async (url: string): Promise<any> => {
if (url === `https://graph.microsoft.com/v1.0/teams/${teamId}/installedApps?$expand=teamsApp,teamsAppDefinition`) {
return jsonResponse;
}

throw 'Invalid response';
});

await command.action(logger, { options: { teamId: teamId, verbose: true, output: 'csv' } });
assert(loggerLogSpy.calledWith(friendlyResponse));
});
});
124 changes: 124 additions & 0 deletions src/m365/teams/commands/team/team-app-list.ts
@@ -0,0 +1,124 @@
import { Logger } from '../../../../cli/Logger';
import GlobalOptions from '../../../../GlobalOptions';
import { validation } from '../../../../utils/validation';
import GraphCommand from '../../../base/GraphCommand';
import commands from '../../commands';
import { odata } from '../../../../utils/odata';
import { Cli } from '../../../../cli/Cli';
import { Options as TeamsTeamGetOptions } from './team-get';
import * as TeamGetCommand from './team-get';
import Command from '../../../../Command';

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
teamId?: string;
teamName?: string;
}

class TeamsTeamAppListCommand extends GraphCommand {
public get name(): string {
return commands.TEAM_APP_LIST;
}

public get description(): string {
return 'List apps installed in the specified team';
}

public defaultProperties(): string[] | undefined {
return ['id', 'displayName', 'distributionMethod'];
}

constructor() {
super();

this.#initTelemetry();
this.#initOptions();
this.#initValidators();
this.#initOptionSets();
}

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

#initOptions(): void {
this.options.unshift(
{
option: '-i, --teamId [teamId]'
},
{
option: '-n, --teamName [teamName]'
}
);
}

#initValidators(): void {
this.validators.push(
async (args: CommandArgs) => {
if (args.options.teamId && !validation.isValidGuid(args.options.teamId)) {
return `${args.options.teamId} is not a valid GUID`;
}

return true;
}
);
}

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

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
try {
if (this.verbose) {
logger.logToStderr(`Retrieving installed apps for team '${args.options.teamId || args.options.teamId}'`);
}

const teamId: string = await this.getTeamId(args);
const res = await odata.getAllItems<any>(`${this.resource}/v1.0/teams/${teamId}/installedApps?$expand=teamsApp,teamsAppDefinition`);

if (args.options.output === 'json') {
logger.log(res);
}
else {
//converted to text friendly output
logger.log(res.map(i => {
return {
id: i.id,
displayName: i.teamsApp.displayName,
distributionMethod: i.teamsApp.distributionMethod
};
}));
}
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}

private async getTeamId(args: CommandArgs): Promise<string> {
if (args.options.teamId) {
return args.options.teamId;
}

const teamGetOptions: TeamsTeamGetOptions = {
name: args.options.teamName,
debug: this.debug,
verbose: this.verbose
};

const commandOutput = await Cli.executeCommandWithOutput(TeamGetCommand as Command, { options: { ...teamGetOptions, _: [] } });
const team = JSON.parse(commandOutput.stdout);
return team.id;
}
}

module.exports = new TeamsTeamAppListCommand();
4 changes: 2 additions & 2 deletions src/m365/teams/commands/team/team-get.ts
Expand Up @@ -16,7 +16,7 @@ interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
export interface Options extends GlobalOptions {
id?: string;
name?: string;
}
Expand Down Expand Up @@ -101,7 +101,7 @@ class TeamsTeamGetCommand extends GraphCommand {
},
responseType: 'json'
};
const res: Team = await request.get<Team>(requestOptions);
const res = await request.get<Team>(requestOptions);
logger.log(res);
}
catch (err: any) {
Expand Down

0 comments on commit e6a208f

Please sign in to comment.