diff --git a/docs/docs/cmd/outlook/calendar/calendar-remove.mdx b/docs/docs/cmd/outlook/calendar/calendar-remove.mdx new file mode 100644 index 00000000000..6cb365dd296 --- /dev/null +++ b/docs/docs/cmd/outlook/calendar/calendar-remove.mdx @@ -0,0 +1,86 @@ +import Global from '../../_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# outlook calendar remove + +Removes the calendar of a user. + +## Usage + +```sh +m365 outlook calendar remove [options] +``` + +## Options + +```md definition-list +`-i, --id [id]` +: ID of the calendar. Specify either `id` or `name`, but not both. + +`-n, --name [name]` +: Name of the calendar. Specify either `id` or `name`, but not both. + +`--userId [userId]` +: ID of the user. Specify either `userId` or `userName`, but not both. + +`--userName [userName]` +: UPN of the user. Specify either `userId` or `userName`, but not both. + +`--calendarGroupId [calendarGroupId]` +: ID of the calendar group. Specify either `calendarGroupId` or `calendarGroupName`, but not both. + +`--calendarGroupName [calendarGroupName]` +: Name of the calendar group. Specify either `calendarGroupId` or `calendarGroupName`, but not both. + +`--permanent` +: Permanently remove the calendar, don't send it to the recycle bin. + +`-f, --force` +: Don't prompt for confirmation. +``` + + + +## Permissions + + + + + | Resource | Permissions | + |-----------------|--------------------| + | Microsoft Graph | Calendar.ReadWrite | + + + + + | Resource | Permissions | + |-----------------|--------------------| + | Microsoft Graph | Calendar.ReadWrite | + + + + +## Examples + +Remove the calendar for the current signed-in user by id. + +```sh +m365 outlook calendar remove --userId "@meId" --id "AAMkAGI2TGuLAAA=" +``` + +Permanently remove the calendar from a specific calendar group for the current signed-in user by name. + +```sh +m365 outlook calendar remove --userId "@meId" --calendarGroupName "Colleague calendars" --name "Calendar" --permanent +``` + +Remove the calendar from a specific calendar group for a specific user by name. + +```sh +m365 outlook calendar remove --userId b743445a-112c-4fda-9afd-05943f9c7b36 --calendarGroupId "AAMkADIxYjJiYmIzLTFmNjYtNGNhMy0YOkcEEh3vhfAAAGgdFjAAA=" --name "Calendar" +``` + +## Response + +The command won't return a response on success. diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 4680a85b5c3..7f644ad4fc9 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1303,6 +1303,13 @@ const sidebars: SidebarsConfig = { { 'Outlook (outlook)': [ { + calendar: [ + { + type: 'doc', + label: 'calendar remove', + id: 'cmd/outlook/calendar/calendar-remove' + } + ], mail: [ { type: 'doc', diff --git a/eslint.config.mjs b/eslint.config.mjs index 3ba64be050a..b1274639ce0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -27,6 +27,7 @@ const dictionary = [ 'azure', 'bin', 'builder', + 'calendar', 'call', 'card', 'catalog', diff --git a/src/m365/outlook/commands.ts b/src/m365/outlook/commands.ts index e1cb51b0e07..feb0dab42df 100644 --- a/src/m365/outlook/commands.ts +++ b/src/m365/outlook/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'outlook'; export default { + CALENDAR_REMOVE: `${prefix} calendar remove`, MAIL_SEARCHFOLDER_ADD: `${prefix} mail searchfolder add`, MAIL_SEND: `${prefix} mail send`, MAILBOX_SETTINGS_GET: `${prefix} mailbox settings get`, diff --git a/src/m365/outlook/commands/calendar/calendar-remove.spec.ts b/src/m365/outlook/commands/calendar/calendar-remove.spec.ts new file mode 100644 index 00000000000..efff06d4a5a --- /dev/null +++ b/src/m365/outlook/commands/calendar/calendar-remove.spec.ts @@ -0,0 +1,293 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.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, { options } from './calendar-remove.js'; +import { calendarGroup } from '../../../../utils/calendarGroup.js'; +import { calendar } from '../../../../utils/calendar.js'; + +describe(commands.CALENDAR_REMOVE, () => { + const userId = 'ae0e8388-cd70-427f-9503-c57498ee3337'; + const userName = 'john.doe@contoso.com'; + const calendarId = 'AAMkADJmMVAAA='; + const calendarName = 'Volunteer'; + const calendarGroupId = 'AQMkADJmMVAAA='; + const calendarGroupName = 'My Calendars'; + + let log: any[]; + let logger: Logger; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; + let promptIssued: boolean; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').resolves(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + sinon.stub(cli, 'promptForConfirmation').callsFake(() => { + promptIssued = true; + return Promise.resolve(false); + }); + + promptIssued = false; + }); + + afterEach(() => { + sinonUtil.restore([ + request.delete, + request.post, + calendar.getUserCalendarByName, + calendarGroup.getUserCalendarGroupByName, + cli.handleMultipleResultsFound, + cli.promptForConfirmation + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.CALENDAR_REMOVE); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if neither id nor name is specified', () => { + const actual = commandOptionsSchema.safeParse({ + userId: userId + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if both id and name is specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: calendarId, + name: calendarName, + userId: userId + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + id: calendarId, + userId: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if userName is not a valid user principal name', () => { + const actual = commandOptionsSchema.safeParse({ + id: calendarId, + userName: 'foo' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if both userId and userName is specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: calendarId, + userId: userId, + userName: userName + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if both calendarGroupId and calendarGroupName is specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: calendarId, + userId: userId, + calendarGroupId: calendarGroupId, + calendarGroupName: calendarGroupName + }); + assert.notStrictEqual(actual.success, true); + }); + + it('removes the calendar by id for a user specified by id without prompting for confirmation', async () => { + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars/${calendarId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userId: userId, + force: true, + verbose: true + }) }); + assert(deleteRequestStub.called); + }); + + it('permanently removes the calendar by id for a user specified by id without prompting for confirmation', async () => { + const postRequestStub = sinon.stub(request, 'post').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars/${calendarId}/permanentDelete`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userId: userId, + permanent: true, + force: true, + verbose: true + }) + }); + assert(postRequestStub.called); + }); + + it('removes the calendar by id for a user specified by name from a calendar group specified by name while prompting for confirmation', async () => { + sinon.stub(calendarGroup, 'getUserCalendarGroupByName').withArgs(userName, calendarGroupName, 'id').resolves({ id: calendarGroupId }); + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userName}')/calendarGroups/${calendarGroupId}/calendars/${calendarId}`) { + return; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userName: userName, + calendarGroupName: calendarGroupName, + verbose: true + }) }); + assert(deleteRequestStub.called); + }); + + it('removes the calendar by name for a user specified by name from a calendar group specified by name while prompting for confirmation', async () => { + sinon.stub(calendar, 'getUserCalendarByName').withArgs(userName, calendarName, calendarGroupId, 'id').resolves({ id: calendarId }); + sinon.stub(calendarGroup, 'getUserCalendarGroupByName').withArgs(userName, calendarGroupName, 'id').resolves({ id: calendarGroupId }); + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userName}')/calendarGroups/${calendarGroupId}/calendars/${calendarId}`) { + return; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + name: calendarName, + userName: userName, + calendarGroupName: calendarGroupName, + verbose: true + }) + }); + assert(deleteRequestStub.called); + }); + + it('removes the calendar by name for a user specified by name from a calendar group specified by id without prompting for confirmation', async () => { + sinon.stub(calendar, 'getUserCalendarByName').withArgs(userName, calendarName, calendarGroupId, 'id').resolves({ id: calendarId }); + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userName}')/calendarGroups/${calendarGroupId}/calendars/${calendarId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + name: calendarName, + userName: userName, + calendarGroupId: calendarGroupId, + force: true, + verbose: true + }) }); + assert(deleteRequestStub.called); + }); + + it('prompts before removing the calendar when confirm option not passed', async () => { + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userId: userId + }) + }); + + assert(promptIssued); + }); + + it('aborts removing the calendar when prompt not confirmed', async () => { + const deleteSpy = sinon.stub(request, 'delete').resolves(); + + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userId: userId + }) + }); + assert(deleteSpy.notCalled); + }); + + it('throws an error when the calendar specified by id for a user specified by id cannot be found', async () => { + const error = { + error: { + code: 'ErrorItemNotFound', + message: 'The specified object was not found in the store.' + } + }; + sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars/${calendarId}`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { + options: commandOptionsSchema.parse({ + id: calendarId, + userId: userId, + force: true + }) + }), new CommandError(error.error.message)); + }); +}); diff --git a/src/m365/outlook/commands/calendar/calendar-remove.ts b/src/m365/outlook/commands/calendar/calendar-remove.ts new file mode 100644 index 00000000000..aedadcce561 --- /dev/null +++ b/src/m365/outlook/commands/calendar/calendar-remove.ts @@ -0,0 +1,119 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; +import { validation } from '../../../../utils/validation.js'; +import { calendarGroup } from '../../../../utils/calendarGroup.js'; +import { calendar } from '../../../../utils/calendar.js'; +import { cli } from '../../../../cli/cli.js'; +import request, { CliRequestOptions } from '../../../../request.js'; + +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().alias('i').optional(), + name: z.string().alias('n').optional(), + userId: z.string() + .refine(userId => validation.isValidGuid(userId), { + error: e => `'${e.input}' is not a valid GUID.` + }).optional(), + userName: z.string() + .refine(userName => validation.isValidUserPrincipalName(userName), { + error: e => `'${e.input}' is not a valid UPN.` + }).optional(), + calendarGroupId: z.string().optional(), + calendarGroupName: z.string().optional(), + permanent: z.boolean().optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +class OutlookCalendarRemoveCommand extends GraphCommand { + public get name(): string { + return commands.CALENDAR_REMOVE; + } + + public get description(): string { + return 'Removes the calendar of a user'; + } + + public get schema(): z.ZodType | undefined { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.id, options.name].filter(x => x !== undefined).length === 1, { + error: 'Specify either id or name, but not both' + }) + .refine(options => !(options.userId && options.userName), { + error: 'Specify either userId or userName, but not both' + }) + .refine(options => [options.calendarGroupId, options.calendarGroupName].filter(x => x !== undefined).length !== 2, { + error: 'Do not specify both calendarGroupId and calendarGroupName' + }); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + const removeCalendar = async (): Promise => { + if (this.verbose) { + await logger.logToStderr('Getting calendar...'); + } + + try { + const userIdentifier = args.options.userId ?? args.options.userName; + let calendarGroupId = args.options.calendarGroupId; + + if (args.options.calendarGroupName) { + const group = await calendarGroup.getUserCalendarGroupByName(userIdentifier!, args.options.calendarGroupName, 'id'); + calendarGroupId = group.id; + } + + let calendarId = args.options.id; + if (args.options.name) { + const result = await calendar.getUserCalendarByName(userIdentifier!, args.options.name!, calendarGroupId, 'id'); + calendarId = result.id; + } + + let url = `${this.resource}/v1.0/users('${userIdentifier}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars/${calendarId}`; + if (args.options.permanent) { + url += '/permanentDelete'; + } + const requestOptions: CliRequestOptions = { + url: url, + headers: { + accept: 'application/json;odata.metadata=none' + } + }; + + if (args.options.permanent) { + await request.post(requestOptions); + } + else { + await request.delete(requestOptions); + } + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + }; + + if (args.options.force) { + await removeCalendar(); + } + else { + const calendarIdentifier = args.options.id ?? args.options.name; + const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove calendar '${calendarIdentifier}'?` }); + if (result) { + await removeCalendar(); + } + } + } +} + +export default new OutlookCalendarRemoveCommand(); diff --git a/src/utils/calendar.spec.ts b/src/utils/calendar.spec.ts new file mode 100644 index 00000000000..fed0380396c --- /dev/null +++ b/src/utils/calendar.spec.ts @@ -0,0 +1,189 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { cli } from '../cli/cli.js'; +import request from '../request.js'; +import { sinonUtil } from './sinonUtil.js'; +import { calendar } from './calendar.js'; +import { formatting } from './formatting.js'; +import { settingsNames } from '../settingsNames.js'; + +describe('utils/calendar', () => { + const userId = '729827e3-9c14-49f7-bb1b-9608f156bbb8'; + const calendarId = 'AAMkAGI2TGuLAAA'; + const calendarName = 'My Calendar'; + const invalidCalendarName = 'M Calnedar'; + const calendarGroupId = 'AQMkADIxYjJiYgEzLTFmN_F8AAAIBBgAA_F8AAAJjIQAAAA=='; + const calendarResponse = { + "id": "AAMkAGI2TGuLAAA=", + "name": "Calendar", + "color": "auto", + "isDefaultCalendar": true, + "changeKey": "nfZyf7VcrEKLNoU37KWlkQAAA0x0+w==", + "canShare": true, + "canViewPrivateItems": true, + "hexColor": "", + "canEdit": true, + "allowedOnlineMeetingProviders": [ + "teamsForBusiness" + ], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": true, + "isRemovable": false, + "owner": { + "name": "John Doe", + "address": "john.doe@contoso.com" + } + }; + const anotherCalendarResponse = { + "id": "AAMkAGI2TGuLBBB=", + "name": "Vacation", + "color": "auto", + "isDefaultCalendar": false, + "changeKey": "abcdf7VcrEKLNoU37KWlkQAAA0x0+w==", + "canShare": false, + "canViewPrivateItems": true, + "hexColor": "", + "canEdit": true, + "allowedOnlineMeetingProviders": [ + ], + "defaultOnlineMeetingProvider": "none", + "isTallyingResponses": true, + "isRemovable": false, + "owner": { + "name": "John Doe", + "address": "john.doe@contoso.com" + } + }; + const calendarLimitedResponse = { + "id": "AAMkAGI2TGuLAAA=", + "name": "Calendar", + "color": "auto" + }; + + afterEach(() => { + sinonUtil.restore([ + request.get, + cli.getSettingWithDefaultValue, + cli.handleMultipleResultsFound + ]); + }); + + it('correctly get single calendar by name using getUserCalendarByName', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars?$filter=name eq '${formatting.encodeQueryParameter(calendarName)}'`) { + return { + value: [ + calendarResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + const actual = await calendar.getUserCalendarByName(userId, calendarName); + assert.deepStrictEqual(actual, calendarResponse); + }); + + it('correctly get single calendar by name from a calendar group using getUserCalendarByName with specified properties', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups/${calendarGroupId}/calendars?$filter=name eq '${formatting.encodeQueryParameter(calendarName)}'&$select=id,name`) { + return { + value: [ + calendarLimitedResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + const actual = await calendar.getUserCalendarByName(userId, calendarName, calendarGroupId, 'id,name'); + assert.deepStrictEqual(actual, calendarLimitedResponse); + }); + + it('handles selecting single calendar when multiple calendars with the specified name found using getUserCalendarByName and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars?$filter=name eq '${formatting.encodeQueryParameter(calendarName)}'`) { + return { + value: [ + calendarResponse, + anotherCalendarResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves(calendarResponse); + + const actual = await calendar.getUserCalendarByName(userId, calendarName); + assert.deepStrictEqual(actual, calendarResponse); + }); + + it('throws error message when no calendar was found using getUserCalendarByName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars?$filter=name eq '${formatting.encodeQueryParameter(invalidCalendarName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(calendar.getUserCalendarByName(userId, invalidCalendarName), + new Error(`The specified calendar '${invalidCalendarName}' does not exist.`)); + }); + + it('throws error message when multiple calendars were found using getUserCalendarByName', 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/users('${userId}')/calendars?$filter=name eq '${formatting.encodeQueryParameter(calendarName)}'`) { + return { + value: [ + calendarResponse, + anotherCalendarResponse + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(calendar.getUserCalendarByName(userId, calendarName), + Error(`Multiple calendars with name '${calendarName}' found. Found: ${calendarResponse.id}, ${anotherCalendarResponse.id}.`)); + }); + + it('correctly get single calendar by id using getUserCalendarById', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendars/${calendarId}`) { + return calendarResponse; + } + + throw 'Invalid Request'; + }); + + const actual = await calendar.getUserCalendarById(userId, calendarId); + assert.deepStrictEqual(actual, calendarResponse); + }); + + it('correctly get single calendar by id from a calendar group using getUserCalendarById with specified properties', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups/${calendarGroupId}/calendars/${calendarId}?$select=id,displayName`) { + return calendarLimitedResponse; + } + + throw 'Invalid Request'; + }); + + const actual = await calendar.getUserCalendarById(userId, calendarId, calendarGroupId, 'id,displayName'); + assert.deepStrictEqual(actual, calendarLimitedResponse); + }); +}); diff --git a/src/utils/calendar.ts b/src/utils/calendar.ts new file mode 100644 index 00000000000..b01d81c6cd7 --- /dev/null +++ b/src/utils/calendar.ts @@ -0,0 +1,47 @@ +import { Calendar } from '@microsoft/microsoft-graph-types'; +import { odata } from './odata.js'; +import { formatting } from './formatting.js'; +import { cli } from '../cli/cli.js'; +import request, { CliRequestOptions } from '../request.js'; + +export const calendar = { + async getUserCalendarById(userId: string, calendarId: string, calendarGroupId?: string, properties?: string): Promise { + let url = `https://graph.microsoft.com/v1.0/users('${userId}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars/${calendarId}`; + + if (properties) { + url += `?$select=${properties}`; + } + + const requestOptions: CliRequestOptions = { + url: url, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + return await request.get(requestOptions); + }, + + async getUserCalendarByName(userId: string, name: string, calendarGroupId?: string, properties?: string): Promise { + let url = `https://graph.microsoft.com/v1.0/users('${userId}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars?$filter=name eq '${formatting.encodeQueryParameter(name)}'`; + + if (properties) { + url += `&$select=${properties}`; + } + + const calendars = await odata.getAllItems(url); + + if (calendars.length === 0) { + throw new Error(`The specified calendar '${name}' does not exist.`); + } + + if (calendars.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', calendars); + const selectedCalendar = await cli.handleMultipleResultsFound(`Multiple calendars with name '${name}' found.`, resultAsKeyValuePair); + return selectedCalendar; + } + + return calendars[0]; + } +}; diff --git a/src/utils/calendarGroup.spec.ts b/src/utils/calendarGroup.spec.ts new file mode 100644 index 00000000000..1d811437e06 --- /dev/null +++ b/src/utils/calendarGroup.spec.ts @@ -0,0 +1,131 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { cli } from '../cli/cli.js'; +import request from '../request.js'; +import { sinonUtil } from './sinonUtil.js'; +import { calendarGroup } from './calendarGroup.js'; +import { formatting } from './formatting.js'; +import { settingsNames } from '../settingsNames.js'; + +describe('utils/calendarGroup', () => { + const userId = '729827e3-9c14-49f7-bb1b-9608f156bbb8'; + const groupName = 'My Calendars'; + const invalidGroupName = 'M Calnedar'; + const calendarGroupResponse = { + "name": "My Calendars", + "classId": "0006f0b7-0000-0000-c000-000000000046", + "changeKey": "NreqLYgxdE2DpHBBId74XwAAAAAGZw==", + "id": "AQMkADIxYjJiYgEzLTFmN_F8AAAIBBgAA_F8AAAJjIQAAAA==" + }; + const anotherCalendarGroupResponse = { + "name": "My Calendars", + "classId": "0006f0b7-0000-0000-c000-000000000047", + "changeKey": "MreqLYgxdE2DpHBBId74XwAAAAAGZw==", + "id": "AQMkADIxYjJiYgEzLTFmN_F8AAAIBBgAA_F8AAAJjIQBBB==" + }; + const calendarGroupLimitedResponse = { + "name": "My Calendars", + "id": "AQMkADIxYjJiYgEzLTFmN_F8AAAIBBgAA_F8AAAJjIQAAAA==" + }; + + afterEach(() => { + sinonUtil.restore([ + request.get, + cli.getSettingWithDefaultValue, + cli.handleMultipleResultsFound + ]); + }); + + it('correctly get single calendar group by name using getUserCalendarGroupByName', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(groupName)}'`) { + return { + value: [ + calendarGroupResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + const actual = await calendarGroup.getUserCalendarGroupByName(userId, groupName); + assert.deepStrictEqual(actual, calendarGroupResponse); + }); + + it('correctly get single calendar group by name using getUserCalendarGroupByName with specified properties', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(groupName)}'&$select=id,name`) { + return { + value: [ + calendarGroupLimitedResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + const actual = await calendarGroup.getUserCalendarGroupByName(userId, groupName, 'id,name'); + assert.deepStrictEqual(actual, calendarGroupLimitedResponse); + }); + + it('handles selecting single calendar group when multiple calendar groups with the specified name found using getUserCalendarGroupByName and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(groupName)}'`) { + return { + value: [ + calendarGroupResponse, + anotherCalendarGroupResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves(calendarGroupResponse); + + const actual = await calendarGroup.getUserCalendarGroupByName(userId, groupName); + assert.deepStrictEqual(actual, calendarGroupResponse); + }); + + it('throws error message when no calendar group was found using getUserCalendarGroupByName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(invalidGroupName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(calendarGroup.getUserCalendarGroupByName(userId, invalidGroupName), + new Error(`The specified calendar group '${invalidGroupName}' does not exist.`)); + }); + + it('throws error message when multiple calendar groups were found using getUserCalendarGroupByName', 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/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(groupName)}'`) { + return { + value: [ + calendarGroupResponse, + anotherCalendarGroupResponse + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(calendarGroup.getUserCalendarGroupByName(userId, groupName), + Error(`Multiple calendar groups with name '${groupName}' found. Found: ${calendarGroupResponse.id}, ${anotherCalendarGroupResponse.id}.`)); + }); +}); diff --git a/src/utils/calendarGroup.ts b/src/utils/calendarGroup.ts new file mode 100644 index 00000000000..1f34a8c0132 --- /dev/null +++ b/src/utils/calendarGroup.ts @@ -0,0 +1,28 @@ +import { CalendarGroup } from '@microsoft/microsoft-graph-types'; +import { odata } from './odata.js'; +import { formatting } from './formatting.js'; +import { cli } from '../cli/cli.js'; + +export const calendarGroup = { + async getUserCalendarGroupByName(userId: string, displayName: string, properties?: string): Promise { + let url = `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups?$filter=name eq '${formatting.encodeQueryParameter(displayName)}'`; + + if (properties) { + url += `&$select=${properties}`; + } + + const calendarGroups = await odata.getAllItems(url); + + if (calendarGroups.length === 0) { + throw new Error(`The specified calendar group '${displayName}' does not exist.`); + } + + if (calendarGroups.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', calendarGroups); + const selectedCalendarGroup = await cli.handleMultipleResultsFound(`Multiple calendar groups with name '${displayName}' found.`, resultAsKeyValuePair); + return selectedCalendarGroup; + } + + return calendarGroups[0]; + } +};