Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds 'teams team app list' command. Closes #4129
- Loading branch information
1 parent
dcd63df
commit e6a208f
Showing
6 changed files
with
337 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters