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];
+ }
+};