diff --git a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx index 1cf4dbcdde7..a9b0cfe1627 100644 --- a/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-conversation-list.mdx @@ -15,20 +15,29 @@ m365 entra m365group conversation list [options] ## Options ```md definition-list -`-i, --groupId ` -: The ID of the Microsoft 365 group +`-i, --groupId [groupId]` +: The ID of the Microsoft 365 Group. Specify either `groupId` or `groupName`, but not both. + +`-n, --groupName [groupName]` +: Display name of the Microsoft 365 Group. Specify either `groupId` or `groupName`, but not both. ``` ## Examples -Lists conversations for the specified Microsoft 365 group +Lists conversations for the Microsoft 365 group specified by id. ```sh m365 entra m365group conversation list --groupId '00000000-0000-0000-0000-000000000000' ``` +Lists conversations for the Microsoft 365 group specified by displayName. + +```sh +m365 entra m365group conversation list --groupName Finance +``` + ## Response diff --git a/docs/docs/cmd/entra/m365group/m365group-get.mdx b/docs/docs/cmd/entra/m365group/m365group-get.mdx index f7663c58ca3..c1fd7f619a8 100644 --- a/docs/docs/cmd/entra/m365group/m365group-get.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-get.mdx @@ -15,8 +15,11 @@ m365 entra m365group get [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 Group or Microsoft Teams team to retrieve information for +`-i, --id [id]` +: The ID of the Microsoft 365 Group or Microsoft Teams team to retrieve information for. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group or Microsoft Teams team to retrieve information for. Specify either `id` or `displayName`, but not both. `--includeSiteUrl` : Set to retrieve the site URL for the group @@ -32,6 +35,12 @@ Get information about the Microsoft 365 Group with id _1caf7dcd-7e83-4c3a-94f7-9 m365 entra m365group get --id 1caf7dcd-7e83-4c3a-94f7-932a1299c844 ``` +Get information about the Microsoft 365 Group with displayName _Finance_ + +```sh +m365 entra m365group get --displayName Finance +``` + Get information about the Microsoft 365 Group with id _1caf7dcd-7e83-4c3a-94f7-932a1299c844_ and also retrieve the URL of the corresponding SharePoint site ```sh diff --git a/docs/docs/cmd/entra/m365group/m365group-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-remove.mdx index 3b65202a7a2..0ebd7c05596 100644 --- a/docs/docs/cmd/entra/m365group/m365group-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-remove.mdx @@ -13,8 +13,11 @@ m365 entra m365group remove [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 Group to remove +`-i, --id [id]` +: The ID of the Microsoft 365 Group to remove. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to remove. Specify either `id` or `displayName`, but not both. `-f, --force` : Don't prompt for confirming removing the group @@ -47,10 +50,10 @@ Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_. Will prompt for con m365 entra m365group remove --id 28beab62-7540-4db1-a23f-29a6018a3848 ``` -Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_ without prompting for confirmation +Remove group with displayName _Finance_ without prompting for confirmation ```sh -m365 entra m365group remove --id 28beab62-7540-4db1-a23f-29a6018a3848 --force +m365 entra m365group remove --displayName Finance --force ``` Remove group with id _28beab62-7540-4db1-a23f-29a6018a3848_ without prompting for confirmation and without moving it to the Recycle Bin diff --git a/docs/docs/cmd/entra/m365group/m365group-renew.mdx b/docs/docs/cmd/entra/m365group/m365group-renew.mdx index bafec1f8263..c67fe162964 100644 --- a/docs/docs/cmd/entra/m365group/m365group-renew.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-renew.mdx @@ -13,8 +13,11 @@ m365 entra m365group renew [options] ## Options ```md definition-list -`-i, --id ` -: The ID of the Microsoft 365 group to renew +`-i, --id [id]` +: The ID of the Microsoft 365 group to renew. Specify either `id` or `displayName`, but not both. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to renew. Specify either `id` or `displayName`, but not both. ``` @@ -31,6 +34,12 @@ Renew the Microsoft 365 group with id _28beab62-7540-4db1-a23f-29a6018a3848_ m365 entra m365group renew --id 28beab62-7540-4db1-a23f-29a6018a3848 ``` +Renew the Microsoft 365 group with displayName _Finance_ + +```sh +m365 entra m365group renew --displayName Finance +``` + ## Response The command won't return a response on success. diff --git a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx index 4f31bb9f458..093eec6476e 100644 --- a/docs/docs/cmd/entra/m365group/m365group-teamify.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-teamify.mdx @@ -14,10 +14,13 @@ m365 entra m365group teamify [options] ```md definition-list `-i, --id [id]` -: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. +: The ID of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. + +`-n, --displayName [displayName]` +: Display name of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. `--mailNickname [mailNickname]` -: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id` or `mailNickname` but not both. +: The mail alias of the Microsoft 365 Group to connect to Microsoft Teams. Specify either `id`, `displayName` or `mailNickname`, but not multiple. ``` @@ -30,7 +33,13 @@ Creates a new Microsoft Teams team under existing Microsoft 365 group with the s m365 entra m365group teamify --id e3f60f99-0bad-481f-9e9f-ff0f572fbd03 ``` -Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified mailNickname. +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified displayName. + +```sh +m365 entra m365group teamify --displayName Finance +``` + +Creates a new Microsoft Teams team under existing Microsoft 365 group with the specified mailNickname._ ```sh m365 entra m365group teamify --mailNickname GroupName diff --git a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx index 8f708e970a7..b90ea8e9f77 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-add.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-add.mdx @@ -13,7 +13,7 @@ m365 entra m365group user add [options] ## Alias ```sh -m365 teams user add +m365 teams user add [options] ``` ## Options @@ -38,7 +38,7 @@ m365 teams user add : The user principal names of users. You can also pass a comma-separated list of UPNs. Specify either `ids` or `userNames` but not both. `-r, --role [role]` -: The role to be assigned to the new user: `Owner,Member`. Default `Member`. +: The role to be assigned to the new user. Allowed values: `Owner`, `Member`. Default `Member`. ``` diff --git a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx index 74524fe8469..173ae648e97 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-list.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-list.mdx @@ -57,6 +57,12 @@ List specific properties for all group users from a group specified by ID. m365 entra m365group user list --groupId 03cba9da-3974-46c1-afaf-79caa2e45bbe --properties "id,jobTitle,companyName,accountEnabled" ``` +List all group members that are guest users. + +```sh +m365 entra m365group user list --groupDisplayName Developers --filter "userType eq 'Guest'" +``` + ## Response diff --git a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx index 930fc5df09f..f4c34f757c2 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-remove.mdx @@ -17,7 +17,7 @@ m365 entra m365group user remove [options] : The ID of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. `--groupName [groupName]` -: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`.. +: The display name of the Microsoft 365 group. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. `--teamId [teamId]` : The ID of the Microsoft Teams team. Specify only one of the following: `groupId`, `groupName`, `teamId`, or `teamName`. diff --git a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx index b5b60faf6b6..32deed6bba5 100644 --- a/docs/docs/cmd/entra/m365group/m365group-user-set.mdx +++ b/docs/docs/cmd/entra/m365group/m365group-user-set.mdx @@ -32,7 +32,7 @@ m365 entra m365group user set [options] : The user principal names of users. You can also pass a comma-separated list of UPNs. Specify only one of the following `ids` or `userNames`. `-r, --role ` -: Role to set for the given user in the specified Microsoft 365 Group or Microsoft Teams team. Allowed values: `Owner`, `Member` +: Role to set for the given user in the specified Microsoft 365 Group or Microsoft Teams team. Allowed values: `Owner`, `Member`. ``` @@ -70,13 +70,13 @@ m365 entra m365group user set --groupId '00000000-0000-0000-0000-000000000000' - Demote multiple users specified by the userNames parameter from Owner to Member of the given Microsoft Teams team ```sh -m365 entra teams user set --teamId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com,john.doe@contoso.onmicrosoft.com' --role Member +m365 teams user set --teamId '00000000-0000-0000-0000-000000000000' --userNames 'anne.matthews@contoso.onmicrosoft.com,john.doe@contoso.onmicrosoft.com' --role Member ``` Demote multiple users specified by the ids parameter from Owner to Member in the given Microsoft Teams team ```sh -m365 entra teams user set --teamName 'Engineering' --ids '74a3b772-3122-447b-b9da-10895e238219,dd3d21e4-a142-46b9-8482-bca8fe9596b3' --role Member +m365 teams user set --teamName 'Engineering' --ids '74a3b772-3122-447b-b9da-10895e238219,dd3d21e4-a142-46b9-8482-bca8fe9596b3' --role Member ``` ## Response diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts index c9e5ae6745f..f8ac0902fd7 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.spec.ts @@ -50,6 +50,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -102,7 +103,7 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { assert.strictEqual(actual, true); }); - it('Retrieve conversations for the specified group by groupId in the tenant (verbose)', async () => { + it('Retrieve conversations for the group specified by groupId in the tenant (verbose)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/conversations`) { return jsonOutput; @@ -119,6 +120,25 @@ describe(commands.M365GROUP_CONVERSATION_LIST, () => { jsonOutput.value )); }); + + it('Retrieve conversations for the group specified by groupName in the tenant (verbose)', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/conversations`) { + return jsonOutput; + } + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { + verbose: true, groupName: "Finance" + } + }); + assert(loggerLogSpy.calledWith( + jsonOutput.value + )); + }); + it('correctly handles error when listing conversations', async () => { sinon.stub(request, 'get').rejects(new Error('An error has occurred')); diff --git a/src/m365/entra/commands/m365group/m365group-conversation-list.ts b/src/m365/entra/commands/m365group/m365group-conversation-list.ts index 813dd35fe37..2db25b06e01 100644 --- a/src/m365/entra/commands/m365group/m365group-conversation-list.ts +++ b/src/m365/entra/commands/m365group/m365group-conversation-list.ts @@ -12,7 +12,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - groupId: string; + groupId?: string; + groupName?: string; } class EntraM365GroupConversationListCommand extends GraphCommand { @@ -33,12 +34,17 @@ class EntraM365GroupConversationListCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --groupId ' + option: '-i, --groupId [groupId]' + }, + { + option: '-n, --groupName [groupName]' } ); } @@ -46,7 +52,7 @@ class EntraM365GroupConversationListCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.groupId as string)) { + if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { return `${args.options.groupId} is not a valid GUID`; } @@ -55,15 +61,33 @@ class EntraM365GroupConversationListCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['groupId', 'groupName'] }); + } + + #initTypes(): void { + this.types.string.push('groupId', 'groupName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { + if (this.verbose) { + await logger.logToStderr(`Retrieving conversations for Microsoft 365 Group: ${args.options.groupId || args.options.groupName}...`); + } + try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.groupId); + let groupId = args.options.groupId; + + if (args.options.groupName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.groupName); + } + + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.groupId}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } - const conversations = await odata.getAllItems(`${this.resource}/v1.0/groups/${args.options.groupId}/conversations`); + const conversations = await odata.getAllItems(`${this.resource}/v1.0/groups/${groupId}/conversations`); await logger.log(conversations); } catch (err: any) { diff --git a/src/m365/entra/commands/m365group/m365group-get.spec.ts b/src/m365/entra/commands/m365group/m365group-get.spec.ts index 962e5824ade..5a322a5c5a5 100644 --- a/src/m365/entra/commands/m365group/m365group-get.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-get.spec.ts @@ -65,7 +65,7 @@ describe(commands.M365GROUP_GET, () => { assert.notStrictEqual(command.description, null); }); - it('retrieves information about the specified Microsoft 365 Group', async () => { + it('retrieves information about the Microsoft 365 Group specified by id', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { return { @@ -126,6 +126,71 @@ describe(commands.M365GROUP_GET, () => { })); }); + it('retrieves information about the Microsoft 365 Group specified by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Finance'`) { + return { + "value": [ + { + "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [ + "Unified" + ], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + } + ] + }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: 'Finance' } }); + assert(loggerLogSpy.calledWith({ + "id": "1caf7dcd-7e83-4c3a-94f7-932a1299c844", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [ + "Unified" + ], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + })); + }); + it('retrieves information about the specified Microsoft 365 Group (debug)', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/1caf7dcd-7e83-4c3a-94f7-932a1299c844`) { @@ -414,12 +479,35 @@ describe(commands.M365GROUP_GET, () => { }); it('shows error when the group is not a unified group', async () => { + sinon.stub(entraGroup, 'getGroupById').resolves({ + "id": "3f04e370-cbc6-4091-80fe-1d038be2ad06", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2017-11-29T03:27:05Z", + "description": "This is the Contoso Finance Group. Please come here and check out the latest news, posts, files, and more.", + "displayName": "Finance", + "groupTypes": [], + "mail": "finance@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "finance", + "onPremisesLastSyncDateTime": null, + "onPremisesProvisioningErrors": [], + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "proxyAddresses": [ + "SMTP:finance@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2017-11-29T03:27:05Z", + "securityEnabled": false, + "visibility": "Public" + }); const groupId = '3f04e370-cbc6-4091-80fe-1d038be2ad06'; sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-get.ts b/src/m365/entra/commands/m365group/m365group-get.ts index 6b4dc99bfe6..36d848673b0 100644 --- a/src/m365/entra/commands/m365group/m365group-get.ts +++ b/src/m365/entra/commands/m365group/m365group-get.ts @@ -12,7 +12,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; includeSiteUrl: boolean; } @@ -30,12 +31,17 @@ class EntraM365GroupGetCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' }, { option: '--includeSiteUrl' @@ -46,7 +52,7 @@ class EntraM365GroupGetCommand 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`; } @@ -55,18 +61,31 @@ class EntraM365GroupGetCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { let group: GroupExtended; try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + if (args.options.id) { + group = await entraGroup.getGroupById(args.options.id); + } + else { + group = await entraGroup.getGroupByDisplayName(args.options.displayName!); + } + + const isUnifiedGroup = await entraGroup.isUnifiedGroup(group.id!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.id}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${group.id}' is not a Microsoft 365 group.`); } - group = await entraGroup.getGroupById(args.options.id); - if (args.options.includeSiteUrl) { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/groups/${group.id}/drive?$select=webUrl`, @@ -77,7 +96,7 @@ class EntraM365GroupGetCommand extends GraphCommand { }; const res = await request.get<{ webUrl: string }>(requestOptions); - group.siteUrl = res.webUrl ? res.webUrl.substr(0, res.webUrl.lastIndexOf('/')) : ''; + group.siteUrl = res.webUrl ? res.webUrl.substring(0, res.webUrl.lastIndexOf('/')) : ''; } await logger.log(group); diff --git a/src/m365/entra/commands/m365group/m365group-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-remove.spec.ts index b2f5cdba427..2b4c08e26de 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.spec.ts @@ -102,6 +102,7 @@ describe(commands.M365GROUP_REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId); auth.connection.active = true; auth.connection.spoUrl = 'https://contoso.sharepoint.com'; commandInfo = cli.getCommandInfo(command); @@ -182,7 +183,7 @@ describe(commands.M365GROUP_REMOVE, () => { assert(getGroupSpy.notCalled); }); - it('deletes the group site for the sepcified group id when prompt confirmed', async () => { + it('deletes the group site for the group specified by id when prompt confirmed', async () => { defaultGetStub(); const deletedGroupSpy: sinon.SinonStub = defaultPostStub(); @@ -194,6 +195,18 @@ describe(commands.M365GROUP_REMOVE, () => { assert(loggerLogToStderrSpy.calledWith(`Deleting the group site: 'https://contoso.sharepoint.com/teams/sales'...`)); }); + it('deletes the group site for the group specified by displayName when prompt confirmed', async () => { + defaultGetStub(); + const deletedGroupSpy: sinon.SinonStub = defaultPostStub(); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { displayName: 'Finance', verbose: true } }); + assert(deletedGroupSpy.calledOnce); + assert(loggerLogToStderrSpy.calledWith(`Deleting the group site: 'https://contoso.sharepoint.com/teams/sales'...`)); + }); + it('deletes the group without moving it to the Recycle Bin', async () => { defaultGetStub(); defaultPostStub(); @@ -303,7 +316,7 @@ describe(commands.M365GROUP_REMOVE, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId, force: true } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId, force: true } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-remove.ts b/src/m365/entra/commands/m365group/m365group-remove.ts index c6384714180..6d0987617d8 100644 --- a/src/m365/entra/commands/m365group/m365group-remove.ts +++ b/src/m365/entra/commands/m365group/m365group-remove.ts @@ -16,7 +16,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; force?: boolean; skipRecycleBin: boolean; } @@ -39,6 +40,8 @@ class EntraM365GroupRemoveCommand extends GraphCommand { this.#initTelemetry(); this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initTelemetry(): void { @@ -53,7 +56,10 @@ class EntraM365GroupRemoveCommand extends GraphCommand { #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' }, { option: '-f, --force' @@ -67,7 +73,7 @@ class EntraM365GroupRemoveCommand 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`; } @@ -76,27 +82,40 @@ class EntraM365GroupRemoveCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { const removeGroup = async (): Promise => { if (this.verbose) { - await logger.logToStderr(`Removing Microsoft 365 Group: ${args.options.id}...`); + await logger.logToStderr(`Removing Microsoft 365 Group: ${args.options.id || args.options.displayName}...`); } try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + let groupId = args.options.id; + + if (args.options.displayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.displayName); + } + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.id}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } - const siteUrl = await this.getM365GroupSiteUrl(logger, args.options.id); + const siteUrl = await this.getM365GroupSiteUrl(logger, groupId!); const spoAdminUrl = await spo.getSpoAdminUrl(logger, this.debug); // Delete the Microsoft 365 group site. This operation will also delete the group. await this.deleteM365GroupSite(logger, siteUrl, spoAdminUrl); if (args.options.skipRecycleBin) { - await this.deleteM365GroupFromRecycleBin(logger, args.options.id); + await this.deleteM365GroupFromRecycleBin(logger, groupId!); await this.deleteSiteFromRecycleBin(logger, siteUrl, spoAdminUrl); } } @@ -109,7 +128,7 @@ class EntraM365GroupRemoveCommand extends GraphCommand { await removeGroup(); } else { - const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the group ${args.options.id}?` }); + const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the group ${args.options.id || args.options.displayName}?` }); if (result) { await removeGroup(); diff --git a/src/m365/entra/commands/m365group/m365group-renew.spec.ts b/src/m365/entra/commands/m365group/m365group-renew.spec.ts index 1eb96a99173..2d3bf71784c 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.spec.ts @@ -21,12 +21,15 @@ describe(commands.M365GROUP_RENEW, () => { let commandInfo: CommandInfo; let loggerLogToStderrSpy: sinon.SinonSpy; + const groupId = '28beab62-7540-4db1-a23f-29a6018a3848'; + before(() => { sinon.stub(auth, 'restoreAuth').resolves(); sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(true); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves(groupId); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -68,7 +71,20 @@ describe(commands.M365GROUP_RENEW, () => { assert.notStrictEqual(command.description, null); }); - it('renews expiration the specified group', async () => { + it('renews expiration of a group specified by id', async () => { + sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848/renew/') { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: groupId, verbose: true } }); + assert(loggerLogSpy.notCalled); + }); + + it('renews expiration of a group specified by displayName', async () => { sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848/renew/') { return; @@ -77,7 +93,7 @@ describe(commands.M365GROUP_RENEW, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } }); + await command.action(logger, { options: { displayName: 'Finance', verbose: true } }); assert(loggerLogSpy.notCalled); }); @@ -90,14 +106,14 @@ describe(commands.M365GROUP_RENEW, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } }); + await command.action(logger, { options: { debug: true, id: groupId } }); assert(loggerLogToStderrSpy.called); }); it('correctly handles error when group is not found', async () => { sinon.stub(request, 'post').rejects({ error: { 'odata.error': { message: { value: 'The remote server returned an error: (404) Not Found.' } } } }); - await assert.rejects(command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError('The remote server returned an error: (404) Not Found.')); }); @@ -128,7 +144,7 @@ describe(commands.M365GROUP_RENEW, () => { sinonUtil.restore(entraGroup.isUnifiedGroup); sinon.stub(entraGroup, 'isUnifiedGroup').resolves(false); - await assert.rejects(command.action(logger, { options: { id: groupId } } as any), + await assert.rejects(command.action(logger, { options: { id: groupId } }), new CommandError(`Specified group with id '${groupId}' is not a Microsoft 365 group.`)); }); }); diff --git a/src/m365/entra/commands/m365group/m365group-renew.ts b/src/m365/entra/commands/m365group/m365group-renew.ts index aa251b235ca..36f47f6df0a 100644 --- a/src/m365/entra/commands/m365group/m365group-renew.ts +++ b/src/m365/entra/commands/m365group/m365group-renew.ts @@ -11,7 +11,8 @@ interface CommandArgs { } interface Options extends GlobalOptions { - id: string; + id?: string; + displayName?: string; } class EntraM365GroupRenewCommand extends GraphCommand { @@ -28,12 +29,17 @@ class EntraM365GroupRenewCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); } #initOptions(): void { this.options.unshift( { - option: '-i, --id ' + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' } ); } @@ -41,7 +47,7 @@ class EntraM365GroupRenewCommand 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`; } @@ -50,20 +56,33 @@ class EntraM365GroupRenewCommand extends GraphCommand { ); } + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { if (this.verbose) { - await logger.logToStderr(`Renewing Microsoft 365 group's expiration: ${args.options.id}...`); + await logger.logToStderr(`Renewing Microsoft 365 group's expiration: ${args.options.id || args.options.displayName}...`); } try { - const isUnifiedGroup = await entraGroup.isUnifiedGroup(args.options.id); + let groupId = args.options.id; + + if (args.options.displayName) { + groupId = await entraGroup.getGroupIdByDisplayName(args.options.displayName); + } + const isUnifiedGroup = await entraGroup.isUnifiedGroup(groupId!); if (!isUnifiedGroup) { - throw Error(`Specified group with id '${args.options.id}' is not a Microsoft 365 group.`); + throw Error(`Specified group with id '${groupId}' is not a Microsoft 365 group.`); } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups/${args.options.id}/renew/`, + url: `${this.resource}/v1.0/groups/${groupId}/renew/`, headers: { 'accept': 'application/json;odata.metadata=none' } diff --git a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts index 727be4534ed..473c66a68c2 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.spec.ts @@ -68,7 +68,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if both id and mailNickname options are not passed', async () => { + it('fails validation if id, displayName and mailNickname options are not passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -84,7 +84,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.notStrictEqual(actual, true); }); - it('fails validation if both id and mailNickname options are passed', async () => { + it('fails validation if id, displayName and mailNickname options are passed', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -124,7 +124,7 @@ describe(commands.M365GROUP_TEAMIFY, () => { debug: true, mailNickname: 'GroupName' } - }), new CommandError(`The specified Microsoft 365 Group does not exist`)); + }), new CommandError(`The specified group 'GroupName' does not exist.`)); }); it('fails when multiple groups with same name exists', async () => { @@ -236,12 +236,12 @@ describe(commands.M365GROUP_TEAMIFY, () => { debug: true, mailNickname: 'GroupName' } - }), new CommandError("Multiple Microsoft 365 Groups with name 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.")); + }), new CommandError("Multiple groups with mail nickname 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.")); }); it('handles selecting single result when multiple groups with the specified name found and cli is set to prompt', async () => { sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === "https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq 'groupname'") { + if (opts.url === "https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq 'groupname'&$select=id") { return { "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups", "value": [ @@ -593,6 +593,114 @@ describe(commands.M365GROUP_TEAMIFY, () => { assert.strictEqual(requestStub.lastCall.args[0].url, 'https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team'); }); + it('Teamify M365 group by displayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if ((opts.url as string).indexOf(`/v1.0/groups?$filter=displayName eq `) > -1) { + return { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#groups", + "value": [ + { + "@odata.id": "https://graph.microsoft.com/v2/00000000-0000-0000-0000-000000000000/directoryObjects/00000000-0000-0000-0000-000000000000/Microsoft.DirectoryServices.Group", + "id": "00000000-0000-0000-0000-000000000000", + "deletedDateTime": null, + "classification": null, + "createdDateTime": "2021-09-05T09:01:19Z", + "creationOptions": [], + "description": "GroupName", + "displayName": "GroupName", + "expirationDateTime": null, + "groupTypes": [ + "Unified" + ], + "isAssignableToRole": null, + "mail": "groupname@contoso.onmicrosoft.com", + "mailEnabled": true, + "mailNickname": "groupname", + "membershipRule": null, + "membershipRuleProcessingState": null, + "onPremisesDomainName": null, + "onPremisesLastSyncDateTime": null, + "onPremisesNetBiosName": null, + "onPremisesSamAccountName": null, + "onPremisesSecurityIdentifier": null, + "onPremisesSyncEnabled": null, + "preferredDataLocation": null, + "preferredLanguage": null, + "proxyAddresses": [ + "SPO:SPO_00000000-0000-0000-0000-000000000000@SPO_00000000-0000-0000-0000-000000000000", + "SMTP:groupname@contoso.onmicrosoft.com" + ], + "renewedDateTime": "2021-09-05T09:01:19Z", + "resourceBehaviorOptions": [], + "resourceProvisioningOptions": [ + "Team" + ], + "securityEnabled": false, + "securityIdentifier": "S-1-12-1-71288816-1279290235-2033184675-371261341", + "theme": null, + "visibility": "Public", + "onPremisesProvisioningErrors": [] + } + ] + }; + } + + throw 'Invalid request'; + }); + + const requestStub: sinon.SinonStub = sinon.stub(request, 'put').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team`) { + return { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams/$entity", + "id": "00000000-0000-0000-0000-000000000000", + "createdDateTime": null, + "displayName": "Group Team", + "description": "Group Team description", + "internalId": "19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01@thread.tacv2", + "classification": null, + "specialization": null, + "mailNickname": "groupname", + "visibility": "public", + "webUrl": "https://teams.microsoft.com/l/team/19:ASjdflg-xKFnjueOwbm3es6HF2zx3Ki57MyfDFrjeg01%40thread.tacv2/conversations?groupId=00000000-0000-0000-0000-000000000000&tenantId=3a7a651b-2620-433b-a1a3-42de27ae94e8", + "isArchived": null, + "isMembershipLimitedToOwners": false, + "discoverySettings": null, + "memberSettings": { + "allowCreateUpdateChannels": true, + "allowCreatePrivateChannels": true, + "allowDeleteChannels": true, + "allowAddRemoveApps": true, + "allowCreateUpdateRemoveTabs": true, + "allowCreateUpdateRemoveConnectors": true + }, + "guestSettings": { + "allowCreateUpdateChannels": false, + "allowDeleteChannels": false + }, + "messagingSettings": { + "allowUserEditMessages": true, + "allowUserDeleteMessages": true, + "allowOwnerDeleteMessages": true, + "allowTeamMentions": true, + "allowChannelMentions": true + }, + "funSettings": { + "allowGiphy": true, + "giphyContentRating": "moderate", + "allowStickersAndMemes": true, + "allowCustomMemes": true + } + }; + } + throw 'Invalid request'; + }); + + await command.action(logger, { + options: { displayName: 'GroupName' } + }); + assert.strictEqual(requestStub.lastCall.args[0].url, 'https://graph.microsoft.com/v1.0/groups/00000000-0000-0000-0000-000000000000/team'); + }); + it('should handle Microsoft graph error response', async () => { sinon.stub(request, 'put').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/8231f9f2-701f-4c6e-93ce-ecb563e3c1ee/team`) { diff --git a/src/m365/entra/commands/m365group/m365group-teamify.ts b/src/m365/entra/commands/m365group/m365group-teamify.ts index de91436791a..39831cc5db3 100644 --- a/src/m365/entra/commands/m365group/m365group-teamify.ts +++ b/src/m365/entra/commands/m365group/m365group-teamify.ts @@ -1,4 +1,3 @@ -import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; @@ -14,6 +13,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; + displayName?: string; mailNickname?: string; } @@ -33,13 +33,15 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { this.#initOptions(); this.#initValidators(); this.#initOptionSets(); + this.#initTypes(); } #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { id: typeof args.options.id !== 'undefined', - mailNickname: typeof args.options.mailNickname !== 'undefined' + mailNickname: typeof args.options.mailNickname !== 'undefined', + displayName: typeof args.options.displayName !== 'undefined' }); }); } @@ -49,6 +51,9 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { { option: '-i, --id [id]' }, + { + option: '-n, --displayName [displayName]' + }, { option: '--mailNickname [mailNickname]' } @@ -68,7 +73,11 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'mailNickname'] }); + this.optionSets.push({ options: ['id', 'displayName', 'mailNickname'] }); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName', 'mailNickname'); } private async getGroupId(options: Options): Promise { @@ -76,28 +85,11 @@ class EntraM365GroupTeamifyCommand extends GraphCommand { return options.id; } - const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(options.mailNickname as string)}'`, - headers: { - accept: 'application/json;odata.metadata=none' - }, - responseType: 'json' - }; - - const response = await request.get<{ value: [{ id: string }] }>(requestOptions); - const groupItem: { id: string } | undefined = response.value[0]; - - if (!groupItem) { - throw `The specified Microsoft 365 Group does not exist`; - } - - if (response.value.length > 1) { - const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', response.value); - const result = await cli.handleMultipleResultsFound<{ id: string }>(`Multiple Microsoft 365 Groups with name '${options.mailNickname}' found.`, resultAsKeyValuePair); - return result.id; + if (options.displayName) { + return await entraGroup.getGroupIdByDisplayName(options.displayName); } - return groupItem.id; + return await entraGroup.getGroupIdByMailNickname(options.mailNickname!); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts index 5c9345823fd..2d7787c8695 100644 --- a/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts +++ b/src/m365/entra/commands/m365group/m365group-user-remove.spec.ts @@ -31,6 +31,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); + sinon.stub(entraGroup, 'getGroupIdByDisplayName').resolves('00000000-0000-0000-0000-000000000000'); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); }); @@ -193,7 +194,7 @@ describe(commands.M365GROUP_USER_REMOVE, () => { assert(postSpy.notCalled); }); - it('removes the specified owner from owners and members endpoint of the specified Microsoft 365 Group with accepted prompt', async () => { + it('removes the specified owner from owners and members endpoint of the Microsoft 365 Group specified by id with accepted prompt', async () => { let memberDeleteCallIssued = false; sinon.stub(request, 'delete').callsFake(async (opts) => { diff --git a/src/utils/entraGroup.spec.ts b/src/utils/entraGroup.spec.ts index 97d60fc8f2f..2ed095c8dbe 100644 --- a/src/utils/entraGroup.spec.ts +++ b/src/utils/entraGroup.spec.ts @@ -10,6 +10,7 @@ import { settingsNames } from '../settingsNames.js'; const validGroupName = 'Group name'; const validGroupId = '00000000-0000-0000-0000-000000000000'; +const validGroupMailNickname = 'GroupName'; const singleGroupResponse = { id: validGroupId, @@ -185,6 +186,80 @@ describe('utils/entraGroup', () => { assert.deepStrictEqual(actual, validGroupId); }); + it('correctly get single group id by name using getGroupIdByMailNickname', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: singleGroupResponse.id } + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await entraGroup.getGroupIdByMailNickname(validGroupMailNickname); + assert.deepStrictEqual(actual, singleGroupResponse.id); + }); + + it('throws error message when no group was found using getGroupIdByMailNickname', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { value: [] }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(entraGroup.getGroupIdByMailNickname(validGroupMailNickname), Error(`The specified group '${validGroupMailNickname}' does not exist.`)); + }); + + it('throws error message when multiple groups were found using getGroupIdByMailNickname', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: validGroupId } + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(entraGroup.getGroupIdByMailNickname(validGroupMailNickname), Error(`Multiple groups with mail nickname 'GroupName' found. Found: 00000000-0000-0000-0000-000000000000.`)); + }); + + it('handles selecting single result when multiple groups with the specified name found using getGroupIdByMailNickname and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(validGroupMailNickname)}'&$select=id`) { + return { + value: [ + { id: validGroupId }, + { id: validGroupId } + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: validGroupId }); + + const actual = await entraGroup.getGroupIdByMailNickname(validGroupMailNickname); + assert.deepStrictEqual(actual, validGroupId); + }); + it('updates a group to public successfully', async () => { const patchStub = sinon.stub(request, 'patch').callsFake(async opts => { if (opts.url === `https://graph.microsoft.com/v1.0/groups/${validGroupId}`) { diff --git a/src/utils/entraGroup.ts b/src/utils/entraGroup.ts index fb516e48f39..92e0a372a79 100644 --- a/src/utils/entraGroup.ts +++ b/src/utils/entraGroup.ts @@ -75,6 +75,28 @@ export const entraGroup = { return groups[0].id!; }, + /** + * Get id of a group by its mail nickname. + * @param mailNickname Group mail nickname. + * @throws Error when group was not found. + * @throws Error when multiple groups with the same name were found. + */ + async getGroupIdByMailNickname(mailNickname: string): Promise { + const groups = await odata.getAllItems(`${graphResource}/v1.0/groups?$filter=mailNickname eq '${formatting.encodeQueryParameter(mailNickname)}'&$select=id`); + + if (!groups.length) { + throw Error(`The specified group '${mailNickname}' does not exist.`); + } + + if (groups.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', groups); + const result = await cli.handleMultipleResultsFound(`Multiple groups with mail nickname '${mailNickname}' found.`, resultAsKeyValuePair); + return result.id!; + } + + return groups[0].id!; + }, + async setGroup(id: string, isPrivate: boolean, logger?: Logger, verbose?: boolean): Promise { if (verbose && logger) { await logger.logToStderr(`Updating Microsoft 365 Group ${id}...`);