From ae05d33c54c920fdc35e3c772b930fb7ddb3bcf5 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Thu, 29 Aug 2024 15:51:29 +0200 Subject: [PATCH 1/3] New command: viva engage community set --- .../cmd/viva/engage/engage-community-set.mdx | 52 ++++++ docs/src/config/sidebars.ts | 5 + src/m365/viva/commands.ts | 1 + .../engage/engage-community-set.spec.ts | 152 ++++++++++++++++++ .../commands/engage/engage-community-set.ts | 140 ++++++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 docs/docs/cmd/viva/engage/engage-community-set.mdx create mode 100644 src/m365/viva/commands/engage/engage-community-set.spec.ts create mode 100644 src/m365/viva/commands/engage/engage-community-set.ts diff --git a/docs/docs/cmd/viva/engage/engage-community-set.mdx b/docs/docs/cmd/viva/engage/engage-community-set.mdx new file mode 100644 index 00000000000..09ecf9bb73b --- /dev/null +++ b/docs/docs/cmd/viva/engage/engage-community-set.mdx @@ -0,0 +1,52 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# viva engage community set + +Updates an existing Viva Engage community + +## Usage + +```sh +m365 viva engage community set [options] +``` + +## Options + +```md definition-list +`-i, --id [id]` +: The id of the community. Specify either `id` or `displayName`, but not both. + +`-d, --displayName [displayName]` +: The name of the community. Specify either `id` or `displayName`, but not both. + +`--newDisplayName [newDisplayName]` +: New name for the community. The maximum length is 255 characters. + +`--description [description]` +: The description of the community. The maximum length is 1024 characters. + +`--privacy [privacy]` +: Defines the privacy level of the community. The possible values are: `public`, and `private`. +``` + + + +## Examples + +Update info about the community specified by id + +```sh +m365 viva engage community set --id eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9 --newDisplayName 'Developers' --description 'Community for all devs' --privacy public +``` + +Update info about the community specified by name + +```sh +m365 viva engage community set --displayName 'Developrs' --newDisplayName 'Developers' +``` + +## Response + +The command won't return a response on success. \ No newline at end of file diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index a9c08be2d4d..b7d10935849 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -4504,6 +4504,11 @@ const sidebars: SidebarsConfig = { label: 'engage community list', id: 'cmd/viva/engage/engage-community-list' }, + { + type: 'doc', + label: 'engage community set', + id: 'cmd/viva/engage/engage-community-set' + }, { type: 'doc', label: 'engage community user list', diff --git a/src/m365/viva/commands.ts b/src/m365/viva/commands.ts index 4c431736913..3061b0d2b6d 100644 --- a/src/m365/viva/commands.ts +++ b/src/m365/viva/commands.ts @@ -5,6 +5,7 @@ export default { ENGAGE_COMMUNITY_ADD: `${prefix} engage community add`, ENGAGE_COMMUNITY_GET: `${prefix} engage community get`, ENGAGE_COMMUNITY_LIST: `${prefix} engage community list`, + ENGAGE_COMMUNITY_SET: `${prefix} engage community set`, ENGAGE_COMMUNITY_USER_LIST: `${prefix} engage community user list`, ENGAGE_GROUP_LIST: `${prefix} engage group list`, ENGAGE_GROUP_USER_ADD: `${prefix} engage group user add`, diff --git a/src/m365/viva/commands/engage/engage-community-set.spec.ts b/src/m365/viva/commands/engage/engage-community-set.spec.ts new file mode 100644 index 00000000000..4e65d675902 --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-set.spec.ts @@ -0,0 +1,152 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.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 './engage-community-set.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { vivaEngage } from '../../../../utils/vivaEngage.js'; +import { cli } from '../../../../cli/cli.js'; + +describe(commands.ENGAGE_COMMUNITY_SET, () => { + const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; + const displayName = 'Software Engineers'; + let log: string[]; + let logger: Logger; + let commandInfo: CommandInfo; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + }); + + afterEach(() => { + sinonUtil.restore([ + request.patch + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ENGAGE_COMMUNITY_SET); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('passes validation when id is specified', async () => { + const actual = await command.validate({ options: { id: communityId, description: 'Community for all devs' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('passes validation when displayName is specified', async () => { + const actual = await command.validate({ options: { displayName: 'Software Engineers', description: 'Community for all devs' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation when newDisplayName, description or privacy is not specified', async () => { + const actual = await command.validate({ options: { displayName: 'Software Engineers' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if newDisplayName is more than 255 characters', async () => { + const actual = await command.validate({ + options: { + id: communityId, + newDisplayName: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries." + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation if description is more than 1024 characters', async () => { + const actual = await command.validate({ + options: { + displayName: 'Software engineers', + description: `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text.All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.` + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('fails validation when invalid privacy option is provided', async () => { + const actual = await command.validate({ + options: { + displayName: 'Software engineers', + privacy: 'invalid' + } + }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('updates info about a community specified by id', async () => { + const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { id: communityId, newDisplayName: 'Software Engineers', verbose: true } }); + assert(patchRequestStub.called); + }); + + it('updates info about a community specified by displayName', async () => { + sinon.stub(vivaEngage, 'getCommunityIdByDisplayName').resolves(communityId); + const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { displayName: displayName, description: 'Community for all devs', privacy: 'Public', verbose: true } }); + assert(patchRequestStub.called); + }); + + it('handles error when updating Viva Engage community failed', async () => { + sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + throw { error: { message: 'An error has occurred' } }; + } + throw `Invalid request`; + }); + + await assert.rejects( + command.action(logger, { options: { id: communityId } } as any), + new CommandError('An error has occurred') + ); + }); +}); \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-set.ts b/src/m365/viva/commands/engage/engage-community-set.ts new file mode 100644 index 00000000000..9d858494cd3 --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-set.ts @@ -0,0 +1,140 @@ +import GlobalOptions from '../../../../GlobalOptions.js'; +import { Logger } from '../../../../cli/Logger.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { vivaEngage } from '../../../../utils/vivaEngage.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + id?: string; + displayName?: string; + newDisplayName?: string; + description?: string; + privacy?: string; +} + +class VivaEngageCommunitySetCommand extends GraphCommand { + private privacyOptions: string[] = ['public', 'private']; + + public get name(): string { + return commands.ENGAGE_COMMUNITY_SET; + } + + public get description(): string { + return 'Updates an existing Viva Engage community'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initTypes(); + this.#initOptionSets(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + id: typeof args.options.id !== 'undefined', + displayName: typeof args.options.displayName !== 'undefined', + newDisplayName: typeof args.options.newDisplayName !== 'undefined', + description: typeof args.options.description !== 'undefined', + privacy: typeof args.options.privacy !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-i, --id [id]' + }, + { + option: '-d, --displayName [displayName]' + }, + { + option: '--newDisplayName [newDisplayName]' + }, + { + option: '--description [description]' + }, + { + option: '--privacy [privacy]', + autocomplete: this.privacyOptions + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.newDisplayName && args.options.newDisplayName.length > 255) { + return `The maximum amount of characters for 'newDisplayName' is 255.`; + } + + if (args.options.description && args.options.description.length > 1024) { + return `The maximum amount of characters for 'description' is 1024.`; + } + + if (args.options.privacy && this.privacyOptions.map(x => x.toLowerCase()).indexOf(args.options.privacy.toLowerCase()) === -1) { + return `${args.options.privacy} is not a valid privacy. Allowed values are ${this.privacyOptions.join(', ')}`; + } + + if (!args.options.newDisplayName && !args.options.description && !args.options.privacy) { + return 'Specify at least newDisplayName, description, or privacy.'; + } + + return true; + } + ); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName', 'newDisplayName', 'description', 'privacy'); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['id', 'displayName'] }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + + let communityId = args.options.id; + + if (args.options.displayName) { + communityId = await vivaEngage.getCommunityIdByDisplayName(args.options.displayName); + } + + if (this.verbose) { + await logger.logToStderr(`Updating Viva Engage community with ID ${communityId}...`); + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/employeeExperience/communities/${communityId}`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json', + data: { + description: args.options.description, + displayName: args.options.newDisplayName, + privacy: args.options.privacy + } + }; + + try { + await request.patch(requestOptions); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new VivaEngageCommunitySetCommand(); \ No newline at end of file From 838bd10a6bf482b730b76bc2bab461266f1fbf49 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 07:25:14 +0200 Subject: [PATCH 2/3] New command: viva engage community set --- .../cmd/viva/engage/engage-community-set.mdx | 13 +++++++-- .../engage/engage-community-set.spec.ts | 27 ++++++++++++++++++- .../commands/engage/engage-community-set.ts | 17 +++++++++--- src/utils/vivaEngage.ts | 2 +- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/docs/docs/cmd/viva/engage/engage-community-set.mdx b/docs/docs/cmd/viva/engage/engage-community-set.mdx index 09ecf9bb73b..57bf76a5fff 100644 --- a/docs/docs/cmd/viva/engage/engage-community-set.mdx +++ b/docs/docs/cmd/viva/engage/engage-community-set.mdx @@ -16,10 +16,13 @@ m365 viva engage community set [options] ```md definition-list `-i, --id [id]` -: The id of the community. Specify either `id` or `displayName`, but not both. +: The id of the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. `-d, --displayName [displayName]` -: The name of the community. Specify either `id` or `displayName`, but not both. +: The name of the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. + +`--entraGroupId [entraGroupId]` +: The id of the Microsoft Entra group associated with the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. `--newDisplayName [newDisplayName]` : New name for the community. The maximum length is 255 characters. @@ -47,6 +50,12 @@ Update info about the community specified by name m365 viva engage community set --displayName 'Developrs' --newDisplayName 'Developers' ``` +Update info about the community specified by Entra group id + +```sh +m365 viva engage community set --entraGroupId '0bed8b86-5026-4a93-ac7d-56750cc099f1' --newDisplayName 'Developers' +``` + ## Response The command won't return a response on success. \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-set.spec.ts b/src/m365/viva/commands/engage/engage-community-set.spec.ts index 4e65d675902..7f73364a19d 100644 --- a/src/m365/viva/commands/engage/engage-community-set.spec.ts +++ b/src/m365/viva/commands/engage/engage-community-set.spec.ts @@ -17,6 +17,7 @@ import { cli } from '../../../../cli/cli.js'; describe(commands.ENGAGE_COMMUNITY_SET, () => { const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; const displayName = 'Software Engineers'; + const entraGroupId = '0bed8b86-5026-4a93-ac7d-56750cc099f1'; let log: string[]; let logger: Logger; let commandInfo: CommandInfo; @@ -74,6 +75,11 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert.strictEqual(actual, true); }); + it('passes validation when entraGroupId is specified', async () => { + const actual = await command.validate({ options: { entraGroupId: '0bed8b86-5026-4a93-ac7d-56750cc099f1', description: 'Community for all devs' } }, commandInfo); + assert.strictEqual(actual, true); + }); + it('fails validation when newDisplayName, description or privacy is not specified', async () => { const actual = await command.validate({ options: { displayName: 'Software Engineers' } }, commandInfo); assert.notStrictEqual(actual, true); @@ -109,6 +115,11 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert.notStrictEqual(actual, true); }); + it('fails validation when entraGroupId is not a valid GUID', async () => { + const actual = await command.validate({ options: { entraGroupId: 'foo', description: 'Community for all devs' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('updates info about a community specified by id', async () => { const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { @@ -123,7 +134,7 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { }); it('updates info about a community specified by displayName', async () => { - sinon.stub(vivaEngage, 'getCommunityIdByDisplayName').resolves(communityId); + sinon.stub(vivaEngage, 'getCommunityByDisplayName').resolves({ id: communityId }); const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { return; @@ -136,6 +147,20 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert(patchRequestStub.called); }); + it('updates info about a community specified by entraGroupId', async () => { + sinon.stub(vivaEngage, 'getCommunityByEntraGroupId').resolves({ id: communityId }); + const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { entraGroupId: entraGroupId, description: 'Community for all devs', privacy: 'Public', verbose: true } }); + assert(patchRequestStub.called); + }); + it('handles error when updating Viva Engage community failed', async () => { sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { diff --git a/src/m365/viva/commands/engage/engage-community-set.ts b/src/m365/viva/commands/engage/engage-community-set.ts index 9d858494cd3..c6061ae9a23 100644 --- a/src/m365/viva/commands/engage/engage-community-set.ts +++ b/src/m365/viva/commands/engage/engage-community-set.ts @@ -1,6 +1,7 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; +import { validation } from '../../../../utils/validation.js'; import { vivaEngage } from '../../../../utils/vivaEngage.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; @@ -12,6 +13,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; displayName?: string; + entraGroupId?: string; newDisplayName?: string; description?: string; privacy?: string; @@ -58,6 +60,9 @@ class VivaEngageCommunitySetCommand extends GraphCommand { { option: '-d, --displayName [displayName]' }, + { + option: '--entraGroupId [entraGroupId]' + }, { option: '--newDisplayName [newDisplayName]' }, @@ -74,6 +79,10 @@ class VivaEngageCommunitySetCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { + if (args.options.entraGroupId && !validation.isValidGuid(args.options.entraGroupId)) { + return `${args.options.entraGroupId} is not a valid GUID for the option 'entraGroupId'.`; + } + if (args.options.newDisplayName && args.options.newDisplayName.length > 255) { return `The maximum amount of characters for 'newDisplayName' is 255.`; } @@ -100,15 +109,17 @@ class VivaEngageCommunitySetCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'displayName'] }); + this.optionSets.push({ options: ['id', 'displayName', 'entraGroupId'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { - let communityId = args.options.id; if (args.options.displayName) { - communityId = await vivaEngage.getCommunityIdByDisplayName(args.options.displayName); + communityId = (await vivaEngage.getCommunityByDisplayName(args.options.displayName, ['id'])).id!; + } + else if (args.options.entraGroupId) { + communityId = (await vivaEngage.getCommunityByEntraGroupId(args.options.entraGroupId, ['id'])).id!; } if (this.verbose) { diff --git a/src/utils/vivaEngage.ts b/src/utils/vivaEngage.ts index 40ba84ddc2d..0efcbfe39a1 100644 --- a/src/utils/vivaEngage.ts +++ b/src/utils/vivaEngage.ts @@ -70,4 +70,4 @@ export const vivaEngage = { return filteredCommunity; } -}; \ No newline at end of file +}; From d2c885399c454e6e5e4a339afdda0a3394747085 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 10:15:34 +0200 Subject: [PATCH 3/3] New command: viva engage community set --- src/m365/viva/commands/engage/engage-community-set.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/m365/viva/commands/engage/engage-community-set.ts b/src/m365/viva/commands/engage/engage-community-set.ts index c6061ae9a23..e4bc07965ae 100644 --- a/src/m365/viva/commands/engage/engage-community-set.ts +++ b/src/m365/viva/commands/engage/engage-community-set.ts @@ -45,6 +45,7 @@ class VivaEngageCommunitySetCommand extends GraphCommand { Object.assign(this.telemetryProperties, { id: typeof args.options.id !== 'undefined', displayName: typeof args.options.displayName !== 'undefined', + entraGroupId: typeof args.options.entraGroupId !== 'undefined', newDisplayName: typeof args.options.newDisplayName !== 'undefined', description: typeof args.options.description !== 'undefined', privacy: typeof args.options.privacy !== 'undefined' @@ -105,7 +106,7 @@ class VivaEngageCommunitySetCommand extends GraphCommand { } #initTypes(): void { - this.types.string.push('id', 'displayName', 'newDisplayName', 'description', 'privacy'); + this.types.string.push('id', 'displayName', 'entraGroupId', 'newDisplayName', 'description', 'privacy'); } #initOptionSets(): void {