Adds outlook event get command. Closes #7122#7189
Adds outlook event get command. Closes #7122#7189
outlook event get command. Closes #7122#7189Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Microsoft Graph-based CLI command to retrieve a specific Outlook calendar event, plus supporting calendar lookup utilities, tests, and documentation.
Changes:
- Introduces
outlook event getcommand with options for user, calendar, and timezone. - Adds
src/utils/calendarhelper for resolving calendars by id/name (with interactive disambiguation). - Adds tests and Docusaurus docs/sidebar entry for the new command.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/calendar.ts | New utility to resolve a user calendar by id or by name (with multi-result prompting). |
| src/utils/calendar.spec.ts | Unit tests for the new calendar utility. |
| src/m365/outlook/commands/event/event-get.ts | Implements the new outlook event get command (builds Graph request, supports timezone preference). |
| src/m365/outlook/commands/event/event-get.spec.ts | Tests for command validation, happy path, and OData error handling. |
| src/m365/outlook/commands.ts | Registers the EVENT_GET command name. |
| docs/src/config/sidebars.ts | Adds the new command to the Outlook docs sidebar. |
| docs/docs/cmd/outlook/event/event-get.mdx | New documentation page for outlook event get. |
| public getRefinedSchema(schema: typeof options): z.ZodObject<any> | undefined { | ||
| return schema | ||
| .refine(options => [options.userId, options.userName].filter(x => x !== undefined).length === 1, { | ||
| error: 'Specify either userId or userName, but not both' | ||
| }) |
There was a problem hiding this comment.
The refined schema currently enforces that exactly one of userId/userName must be provided. This prevents the common delegated-permissions flow of defaulting to the signed-in user ("me"), which is supported by other Outlook commands (for example src/m365/outlook/commands/calendargroup/calendargroup-list.ts allows omitting both and uses /me). Consider changing validation to disallow specifying both, and enforce the requirement for userId/userName only when running with app-only permissions in commandAction.
| .refine(options => !(options.calendarId && options.calendarName), { | ||
| error: 'Specify either calendarId or calendarName, but not both.' | ||
| }); |
There was a problem hiding this comment.
The schema only prevents specifying both calendarId and calendarName, but it allows specifying neither. In commandAction you always construct a URL that includes /calendars/${calendarId}/..., so when neither option is provided the request URL will contain calendars/undefined and fail. Either require one of calendarId/calendarName in the refined schema, or update commandAction to handle the no-calendar case (for example by calling the /users/{id}/events/{id} endpoint).
| const userIdentifier = args.options.userId ?? args.options.userName; | ||
| if (this.verbose) { | ||
| await logger.logToStderr(`Retrieving event ${args.options.id} for user ${userIdentifier}...`); | ||
| } | ||
|
|
||
| let calendarId = args.options.calendarId; | ||
| if (args.options.calendarName) { | ||
| calendarId = (await calendar.getUserCalendarByName(userIdentifier!, args.options.calendarName))!.id; | ||
| } | ||
|
|
||
| let requestUrl = `${this.resource}/v1.0/users/${userIdentifier}/calendars/${calendarId}/events/${args.options.id}`; | ||
|
|
There was a problem hiding this comment.
When building the request URL, userIdentifier can be a UPN and is inserted into the path without encoding, and the command always uses the /users/... form (no support for /me). In other Outlook commands, UPNs are typically passed via users('...') and/or encoded with formatting.encodeQueryParameter, and delegated calls can default to me (see calendargroup-list.ts and message-list.ts). Updating the user segment construction would improve correctness for special characters and align behavior with existing commands.
| it('retrieves event by id for the user specified with userName and calendarName', async () => { | ||
| sinon.stub(calendar, 'getUserCalendarByName').resolves({ id: calendarId }); | ||
|
|
||
| sinon.stub(request, 'get').callsFake(async (opts) => { | ||
| if (opts.url === `https://graph.microsoft.com/v1.0/users/${userName}/calendars/${calendarId}/events/${id}`) { | ||
| return eventResponse; | ||
| } | ||
|
|
||
| throw 'Invalid request'; | ||
| }); | ||
|
|
||
| await command.action(logger, { | ||
| options: commandOptionsSchema.parse({ | ||
| id: id, | ||
| userName: userName, | ||
| calendarName: calendarName, | ||
| timeZone: 'Pacific Standard Time', | ||
| verbose: true | ||
| }) | ||
| }); | ||
| assert(loggerLogSpy.calledOnceWith(eventResponse)); | ||
| }); |
There was a problem hiding this comment.
The test that passes timeZone doesn't assert that the request includes the Prefer: outlook.timezone="..." header. Since this header controls a key part of the command's behavior, add an assertion in the stubbed request.get to verify opts.headers.Prefer is set when timeZone is provided (and absent when it isn't).
| async getUserCalendarById(userId: string, calendarId: string, calendarGroupId?: string, properties?: string): Promise<Calendar> { | ||
| let url = `https://graph.microsoft.com/v1.0/users('${userId}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars/${calendarId}`; | ||
|
|
There was a problem hiding this comment.
The URL is built using users('${userId}') without encoding userId. UPNs for guest users commonly contain #EXT#, and an unencoded # will be treated as a URL fragment delimiter and won’t be sent to Microsoft Graph. Consider URL-encoding the userId value when composing the request URL (for example using formatting.encodeQueryParameter).
| async getUserCalendarByName(userId: string, name: string, calendarGroupId?: string, properties?: string): Promise<Calendar> { | ||
| let url = `https://graph.microsoft.com/v1.0/users('${userId}')/${calendarGroupId ? `calendarGroups/${calendarGroupId}/` : ''}calendars?$filter=name eq '${formatting.encodeQueryParameter(name)}'`; | ||
|
|
There was a problem hiding this comment.
The URL is built using users('${userId}') without encoding userId. UPNs for guest users commonly contain #EXT#, and an unencoded # will be treated as a URL fragment delimiter and won’t be sent to Microsoft Graph. Consider URL-encoding the userId value when composing the request URL (for example using formatting.encodeQueryParameter).
|
Thanks, we'll try to review it ASAP! |
Closes #7122