Skip to content

Commit 3da5852

Browse files
Add outlook calendargroup get command. Closes #7111
Made-with: Cursor
1 parent bc374af commit 3da5852

5 files changed

Lines changed: 509 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import Global from '../../_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# outlook calendargroup get
6+
7+
Retrieves a calendar group for a user.
8+
9+
## Usage
10+
11+
```sh
12+
m365 outlook calendargroup get [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`--id [id]`
19+
: ID of the calendar group. Specify either `id` or `name`, but not both.
20+
21+
`--name [name]`
22+
: Name of the calendar group. Specify either `id` or `name`, but not both.
23+
24+
`--userId [userId]`
25+
: ID of the user. Specify either `userId` or `userName`, but not both. This option is required when using application permissions.
26+
27+
`--userName [userName]`
28+
: UPN of the user. Specify either `userId` or `userName`, but not both. This option is required when using application permissions.
29+
```
30+
31+
<Global />
32+
33+
## Permissions
34+
35+
<Tabs>
36+
<TabItem value="Delegated">
37+
38+
| Resource | Permissions |
39+
|-----------------|----------------------------------------------------------------------------------------|
40+
| Microsoft Graph | Calendars.ReadBasic, Calendars.Read, Calendars.Read.Shared, Calendars.ReadWrite.Shared |
41+
42+
</TabItem>
43+
<TabItem value="Application">
44+
45+
| Resource | Permissions |
46+
|-----------------|-------------------------------------|
47+
| Microsoft Graph | Calendars.ReadBasic, Calendars.Read |
48+
49+
</TabItem>
50+
</Tabs>
51+
52+
::::note
53+
54+
When using delegated permissions, specifying `userId` or `userName` for a different user requires the `Calendars.Read.Shared` or `Calendars.ReadWrite.Shared` scope. When the specified user matches the signed-in user, no shared scope is needed.
55+
56+
::::
57+
58+
## Examples
59+
60+
Get the calendar group specified by name for the signed-in user.
61+
62+
```sh
63+
m365 outlook calendargroup get --name "Personal Events"
64+
```
65+
66+
Get the calendar group specified by name for a user using application permissions.
67+
68+
```sh
69+
m365 outlook calendargroup get --name "Personal Events" --userId "44288f7d-7710-4293-8c8e-36f310ed2e6a"
70+
```
71+
72+
Get the calendar group specified by id for a user using application permissions.
73+
74+
```sh
75+
m365 outlook calendargroup get --id "AAMkAGVmMDEzMTM4LTZmYWUtNDdkNC1hMDZiLTU1OGY5OTZhYmY4OABGAAAAAAAiQ8W967B7TKBjgx9rVEURBwAiIsqMbYjsT5e-T7KzowPTAAAAAAEMAAAiIsqMbYjsT5e-T7KzowPTAAABuC34AAA=" --userId "44288f7d-7710-4293-8c8e-36f310ed2e6a"
76+
```
77+
78+
## Response
79+
80+
<Tabs>
81+
<TabItem value="JSON">
82+
83+
```json
84+
{
85+
"id": "id-value",
86+
"name": "name-value",
87+
"changeKey": "changeKey-value",
88+
"classId": "classId-value"
89+
}
90+
```
91+
92+
</TabItem>
93+
<TabItem value="Text">
94+
95+
```text
96+
id | name
97+
------------------------------------------------------------------ ----------------
98+
id-value name-value
99+
```
100+
101+
</TabItem>
102+
<TabItem value="CSV">
103+
104+
```csv
105+
id,name
106+
id-value,name-value
107+
```
108+
109+
</TabItem>
110+
<TabItem value="Markdown">
111+
112+
```md
113+
# outlook calendargroup get
114+
115+
Date: 3/20/2026
116+
117+
Property | Value
118+
---------|-------
119+
id | id-value
120+
name | name-value
121+
```
122+
123+
</TabItem>
124+
</Tabs>
125+

docs/src/config/sidebars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,11 @@ const sidebars: SidebarsConfig = {
13081308
type: 'doc',
13091309
label: 'calendargroup list',
13101310
id: 'cmd/outlook/calendargroup/calendargroup-list'
1311+
},
1312+
{
1313+
type: 'doc',
1314+
label: 'calendargroup get',
1315+
id: 'cmd/outlook/calendargroup/calendargroup-get'
13111316
}
13121317
]
13131318
},

src/m365/outlook/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const prefix: string = 'outlook';
22

33
export default {
44
CALENDARGROUP_LIST: `${prefix} calendargroup list`,
5+
CALENDARGROUP_GET: `${prefix} calendargroup get`,
56
MAIL_SEARCHFOLDER_ADD: `${prefix} mail searchfolder add`,
67
MAIL_SEND: `${prefix} mail send`,
78
MAILBOX_SETTINGS_GET: `${prefix} mailbox settings get`,
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import assert from 'assert';
2+
import sinon from 'sinon';
3+
import auth from '../../../../Auth.js';
4+
import { CommandError } from '../../../../Command.js';
5+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
6+
import { Logger } from '../../../../cli/Logger.js';
7+
import { cli } from '../../../../cli/cli.js';
8+
import request from '../../../../request.js';
9+
import { telemetry } from '../../../../telemetry.js';
10+
import { accessToken } from '../../../../utils/accessToken.js';
11+
import { pid } from '../../../../utils/pid.js';
12+
import { session } from '../../../../utils/session.js';
13+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
14+
import commands from '../../commands.js';
15+
import command, { options } from './calendargroup-get.js';
16+
17+
describe(commands.CALENDARGROUP_GET, () => {
18+
const calendarGroupId = 'AAMkAGE0MGM1Y2M5LWEzMmUtNGVlNy05MjRlLTk0YmYyY2I5NTM3ZAAuAAAAAAC_0WfqSjt_SqLtNkuO-bj1AQAbfYq5lmBxQ6a4t1fGbeYAAAAAAEOAAA=';
19+
const calendarGroupName = 'Personal Events';
20+
const resolvedCalendarGroupId = 'AAMkAGE0MGM1Y2M5LWEzMmUtNGVlNy05MjRlLTk0YmYyY2I5NTM3ZAAuAAAAAAC_0WfqSjt_SqLtNkuO-bj1AQAbfYq5lmBxQ6a4t1fGbeYAAAAAAEPAAA=';
21+
const otherUserId = '44288f7d-7710-4293-8c8e-36f310ed2e6a';
22+
const userId = 'b743445a-112c-4fda-9afd-05943f9c7b36';
23+
const userName = 'john.doe@contoso.com';
24+
const currentUserId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
25+
const currentUserName = 'current.user@contoso.com';
26+
27+
const calendarGroupResponse = {
28+
id: calendarGroupId,
29+
name: 'My Calendars',
30+
changeKey: 'nfZyf7VcrEKLNoU37KWlkQAAA0x0+w==',
31+
classId: '0006f0b7-0000-0000-c000-000000000046'
32+
};
33+
34+
const calendarGroupsResponseForFilter = {
35+
value: [
36+
{
37+
id: resolvedCalendarGroupId,
38+
name: calendarGroupName
39+
}
40+
]
41+
};
42+
43+
let logger: Logger;
44+
let commandInfo: CommandInfo;
45+
let loggerLogSpy: sinon.SinonSpy;
46+
let commandOptionsSchema: typeof options;
47+
48+
before(() => {
49+
sinon.stub(auth, 'restoreAuth').resolves();
50+
sinon.stub(telemetry, 'trackEvent').resolves();
51+
sinon.stub(pid, 'getProcessName').returns('');
52+
sinon.stub(session, 'getId').returns('');
53+
54+
auth.connection.active = true;
55+
if (!auth.connection.accessTokens[auth.defaultResource]) {
56+
auth.connection.accessTokens[auth.defaultResource] = {
57+
expiresOn: 'abc',
58+
accessToken: 'abc'
59+
};
60+
}
61+
62+
commandInfo = cli.getCommandInfo(command);
63+
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
64+
});
65+
66+
beforeEach(() => {
67+
logger = {
68+
log: async () => undefined,
69+
logRaw: async () => undefined,
70+
logToStderr: async () => undefined
71+
};
72+
73+
loggerLogSpy = sinon.spy(logger, 'log');
74+
75+
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(false);
76+
sinon.stub(accessToken, 'getScopesFromAccessToken').returns([]);
77+
sinon.stub(accessToken, 'getUserIdFromAccessToken').returns(currentUserId);
78+
sinon.stub(accessToken, 'getUserNameFromAccessToken').returns(currentUserName);
79+
});
80+
81+
afterEach(() => {
82+
sinonUtil.restore([
83+
accessToken.isAppOnlyAccessToken,
84+
accessToken.getScopesFromAccessToken,
85+
accessToken.getUserIdFromAccessToken,
86+
accessToken.getUserNameFromAccessToken,
87+
request.get
88+
]);
89+
});
90+
91+
after(() => {
92+
sinon.restore();
93+
auth.connection.active = false;
94+
});
95+
96+
it('has correct name', () => {
97+
assert.strictEqual(command.name, commands.CALENDARGROUP_GET);
98+
});
99+
100+
it('has a description', () => {
101+
assert.notStrictEqual(command.description, null);
102+
});
103+
104+
it('defines correct properties for the default output', () => {
105+
assert.deepStrictEqual(command.defaultProperties(), ['id', 'name']);
106+
});
107+
108+
it('passes validation with id', () => {
109+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId });
110+
assert.strictEqual(actual.success, true);
111+
});
112+
113+
it('passes validation with name', () => {
114+
const actual = commandOptionsSchema.safeParse({ name: calendarGroupName });
115+
assert.strictEqual(actual.success, true);
116+
});
117+
118+
it('fails validation if both id and name are specified', () => {
119+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId, name: calendarGroupName });
120+
assert.notStrictEqual(actual.success, true);
121+
});
122+
123+
it('fails validation if neither id nor name is specified', () => {
124+
const actual = commandOptionsSchema.safeParse({});
125+
assert.notStrictEqual(actual.success, true);
126+
});
127+
128+
it('fails validation if id is empty', () => {
129+
const actual = commandOptionsSchema.safeParse({ id: '' });
130+
assert.notStrictEqual(actual.success, true);
131+
});
132+
133+
it('fails validation if name is empty', () => {
134+
const actual = commandOptionsSchema.safeParse({ name: '' });
135+
assert.notStrictEqual(actual.success, true);
136+
});
137+
138+
it('fails validation if userId is not a valid GUID', () => {
139+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId, userId: 'foo' });
140+
assert.notStrictEqual(actual.success, true);
141+
});
142+
143+
it('fails validation if userName is not a valid UPN', () => {
144+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId, userName: 'foo' });
145+
assert.notStrictEqual(actual.success, true);
146+
});
147+
148+
it('fails validation if both userId and userName are specified', () => {
149+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId, userId: userId, userName: userName });
150+
assert.notStrictEqual(actual.success, true);
151+
});
152+
153+
it('fails validation with unknown options', () => {
154+
const actual = commandOptionsSchema.safeParse({ id: calendarGroupId, unknownOption: 'value' });
155+
assert.notStrictEqual(actual.success, true);
156+
});
157+
158+
it('retrieves calendar group for the signed-in user by id using delegated permissions', async () => {
159+
sinon.stub(request, 'get').callsFake(async (opts) => {
160+
if (opts.url === `https://graph.microsoft.com/v1.0/me/calendarGroups/${calendarGroupId}`) {
161+
return calendarGroupResponse;
162+
}
163+
164+
throw 'Invalid request';
165+
});
166+
167+
await command.action(logger, { options: commandOptionsSchema.parse({ id: calendarGroupId }) });
168+
assert(loggerLogSpy.calledOnceWith(calendarGroupResponse));
169+
});
170+
171+
it('retrieves calendar group for the signed-in user by name using delegated permissions', async () => {
172+
const expectedFilterUrl = `https://graph.microsoft.com/v1.0/me/calendarGroups?$select=id,name&$filter=name eq 'Personal%20Events'`;
173+
sinon.stub(request, 'get').callsFake(async (opts) => {
174+
if (opts.url === expectedFilterUrl) {
175+
return calendarGroupsResponseForFilter;
176+
}
177+
178+
if (opts.url === `https://graph.microsoft.com/v1.0/me/calendarGroups/${resolvedCalendarGroupId}`) {
179+
return calendarGroupResponse;
180+
}
181+
182+
throw 'Invalid request';
183+
});
184+
185+
await command.action(logger, { options: commandOptionsSchema.parse({ name: calendarGroupName }) });
186+
assert(loggerLogSpy.calledOnceWith(calendarGroupResponse));
187+
});
188+
189+
it('retrieves calendar group for a user specified by id using app-only permissions', async () => {
190+
sinonUtil.restore(accessToken.isAppOnlyAccessToken);
191+
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
192+
193+
sinon.stub(request, 'get').callsFake(async (opts) => {
194+
if (opts.url === `https://graph.microsoft.com/v1.0/users('${userId}')/calendarGroups/${calendarGroupId}`) {
195+
return calendarGroupResponse;
196+
}
197+
198+
throw 'Invalid request';
199+
});
200+
201+
await command.action(logger, { options: commandOptionsSchema.parse({ id: calendarGroupId, userId }) });
202+
assert(loggerLogSpy.calledOnceWith(calendarGroupResponse));
203+
});
204+
205+
it('throws error when running with app-only permissions without userId or userName', async () => {
206+
sinonUtil.restore(accessToken.isAppOnlyAccessToken);
207+
sinon.stub(accessToken, 'isAppOnlyAccessToken').returns(true);
208+
209+
await assert.rejects(
210+
command.action(logger, { options: commandOptionsSchema.parse({ id: calendarGroupId }) }),
211+
new CommandError('When running with application permissions either userId or userName is required.')
212+
);
213+
});
214+
215+
it('throws error when using delegated permissions for other users without shared scope', async () => {
216+
await assert.rejects(
217+
command.action(logger, { options: commandOptionsSchema.parse({ id: calendarGroupId, userId: otherUserId }) }),
218+
new CommandError(`To retrieve calendar groups of other users, the Entra ID application used for authentication must have either the Calendars.Read.Shared or Calendars.ReadWrite.Shared delegated permission assigned.`)
219+
);
220+
});
221+
222+
it('retrieves calendar group for a user specified by id using delegated permissions with shared scope', async () => {
223+
sinonUtil.restore(accessToken.getScopesFromAccessToken);
224+
sinon.stub(accessToken, 'getScopesFromAccessToken').returns(['Calendars.Read.Shared']);
225+
226+
sinon.stub(request, 'get').callsFake(async (opts) => {
227+
if (opts.url === `https://graph.microsoft.com/v1.0/users('${otherUserId}')/calendarGroups/${calendarGroupId}`) {
228+
return calendarGroupResponse;
229+
}
230+
231+
throw 'Invalid request';
232+
});
233+
234+
await command.action(logger, { options: commandOptionsSchema.parse({ id: calendarGroupId, userId: otherUserId }) });
235+
assert(loggerLogSpy.calledOnceWith(calendarGroupResponse));
236+
});
237+
});
238+

0 commit comments

Comments
 (0)