@masonator/m365-mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/graph.test.js +91 -1
- package/dist/__tests__/tools/calendar.test.js +162 -0
- package/dist/__tests__/tools/chat.test.js +148 -1
- package/dist/__tests__/tools/files.test.js +114 -0
- package/dist/__tests__/tools/mail.test.js +321 -0
- package/dist/__tests__/tools/profile.test.js +348 -3
- package/dist/__tests__/tools/schedule.test.d.ts +1 -0
- package/dist/__tests__/tools/schedule.test.js +282 -0
- package/dist/__tests__/tools/server-info.test.js +3 -1
- package/dist/__tests__/tools/sharepoint.test.d.ts +1 -0
- package/dist/__tests__/tools/sharepoint.test.js +218 -0
- package/dist/index.js +12 -2
- package/dist/lib/graph.d.ts +7 -1
- package/dist/lib/graph.js +83 -18
- package/dist/lib/tools/calendar.d.ts +11 -2
- package/dist/lib/tools/calendar.js +119 -5
- package/dist/lib/tools/chat.d.ts +5 -0
- package/dist/lib/tools/chat.js +35 -3
- package/dist/lib/tools/files.d.ts +10 -0
- package/dist/lib/tools/files.js +84 -1
- package/dist/lib/tools/mail.d.ts +28 -3
- package/dist/lib/tools/mail.js +185 -8
- package/dist/lib/tools/profile.d.ts +13 -3
- package/dist/lib/tools/profile.js +143 -10
- package/dist/lib/tools/schedule.d.ts +45 -0
- package/dist/lib/tools/schedule.js +126 -0
- package/dist/lib/tools/server-info.js +2 -0
- package/dist/lib/tools/sharepoint.d.ts +35 -0
- package/dist/lib/tools/sharepoint.js +155 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jest } from '@jest/globals';
|
|
2
|
-
import { graphFetch } from '../lib/graph.js';
|
|
2
|
+
import { graphFetch, graphPost } from '../lib/graph.js';
|
|
3
3
|
// Save original fetch
|
|
4
4
|
const originalFetch = globalThis.fetch;
|
|
5
5
|
afterEach(() => {
|
|
@@ -159,4 +159,94 @@ describe('graphFetch', () => {
|
|
|
159
159
|
},
|
|
160
160
|
});
|
|
161
161
|
});
|
|
162
|
+
it('merges custom headers into request', async () => {
|
|
163
|
+
const mock = mockFetch({
|
|
164
|
+
ok: true,
|
|
165
|
+
json: () => Promise.resolve({ value: [] }),
|
|
166
|
+
});
|
|
167
|
+
await graphFetch('/me/messages', 'test-token', {
|
|
168
|
+
timezone: false,
|
|
169
|
+
headers: { ConsistencyLevel: 'eventual' },
|
|
170
|
+
});
|
|
171
|
+
const callHeaders = mock.mock.calls[0][1].headers;
|
|
172
|
+
expect(callHeaders).toHaveProperty('ConsistencyLevel', 'eventual');
|
|
173
|
+
expect(callHeaders).toHaveProperty('Authorization', 'Bearer test-token');
|
|
174
|
+
});
|
|
175
|
+
it('returns error when success response has invalid JSON', async () => {
|
|
176
|
+
mockFetch({
|
|
177
|
+
ok: true,
|
|
178
|
+
status: 200,
|
|
179
|
+
json: () => Promise.reject(new SyntaxError('Unexpected end of JSON input')),
|
|
180
|
+
});
|
|
181
|
+
const result = await graphFetch('/me', 'test-token');
|
|
182
|
+
expect(result.ok).toBe(false);
|
|
183
|
+
if (!result.ok) {
|
|
184
|
+
expect(result.error.status).toBe(200);
|
|
185
|
+
expect(result.error.message).toContain('not valid JSON');
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
it('handles response.text() failure on error path', async () => {
|
|
189
|
+
mockFetch({
|
|
190
|
+
ok: false,
|
|
191
|
+
status: 500,
|
|
192
|
+
text: () => Promise.reject(new Error('stream error')),
|
|
193
|
+
});
|
|
194
|
+
const result = await graphFetch('/me', 'test-token');
|
|
195
|
+
expect(result.ok).toBe(false);
|
|
196
|
+
if (!result.ok) {
|
|
197
|
+
expect(result.error.status).toBe(500);
|
|
198
|
+
expect(result.error.message).toContain('unable to read error response body');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('graphPost', () => {
|
|
203
|
+
it('sends POST request with JSON body', async () => {
|
|
204
|
+
const mock = mockFetch({
|
|
205
|
+
ok: true,
|
|
206
|
+
json: () => Promise.resolve({ value: [{ scheduleId: 'user@example.com' }] }),
|
|
207
|
+
});
|
|
208
|
+
const result = await graphPost('/me/calendar/getSchedule', 'test-token', { schedules: ['user@example.com'] });
|
|
209
|
+
expect(result).toEqual({
|
|
210
|
+
ok: true,
|
|
211
|
+
data: { value: [{ scheduleId: 'user@example.com' }] },
|
|
212
|
+
});
|
|
213
|
+
expect(mock).toHaveBeenCalledWith('https://graph.microsoft.com/v1.0/me/calendar/getSchedule', expect.objectContaining({
|
|
214
|
+
method: 'POST',
|
|
215
|
+
body: JSON.stringify({ schedules: ['user@example.com'] }),
|
|
216
|
+
headers: expect.objectContaining({
|
|
217
|
+
Authorization: 'Bearer test-token',
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
}),
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
222
|
+
it('returns error on failed POST', async () => {
|
|
223
|
+
mockFetch({ ok: false, status: 403 });
|
|
224
|
+
const result = await graphPost('/me/calendar/getSchedule', 'test-token', {});
|
|
225
|
+
expect(result).toEqual({
|
|
226
|
+
ok: false,
|
|
227
|
+
error: {
|
|
228
|
+
status: 403,
|
|
229
|
+
message: 'Insufficient permissions. Check granted scopes with ms_auth_status.',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
it('handles network error on POST', async () => {
|
|
234
|
+
globalThis.fetch = jest.fn().mockRejectedValue(new Error('Network failure'));
|
|
235
|
+
const result = await graphPost('/me/calendar/getSchedule', 'test-token', {});
|
|
236
|
+
expect(result).toEqual({
|
|
237
|
+
ok: false,
|
|
238
|
+
error: { status: 0, message: 'Network error: Network failure' },
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
it('uses beta URL when beta option is true', async () => {
|
|
242
|
+
const mock = mockFetch({ ok: true, json: () => Promise.resolve({}) });
|
|
243
|
+
await graphPost('/me/calendar/getSchedule', 'test-token', {}, { beta: true });
|
|
244
|
+
expect(mock).toHaveBeenCalledWith('https://graph.microsoft.com/beta/me/calendar/getSchedule', expect.any(Object));
|
|
245
|
+
});
|
|
246
|
+
it('merges custom headers into POST request', async () => {
|
|
247
|
+
const mock = mockFetch({ ok: true, json: () => Promise.resolve({}) });
|
|
248
|
+
await graphPost('/test', 'test-token', {}, { timezone: false, headers: { 'X-Custom': 'value' } });
|
|
249
|
+
const callHeaders = mock.mock.calls[0][1].headers;
|
|
250
|
+
expect(callHeaders).toHaveProperty('X-Custom', 'value');
|
|
251
|
+
});
|
|
162
252
|
});
|
|
@@ -106,6 +106,24 @@ describe('executeCalendar', () => {
|
|
|
106
106
|
await executeCalendar('test-token', {});
|
|
107
107
|
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining(`startDateTime=${todayStr}`), 'test-token', { timezone: true });
|
|
108
108
|
});
|
|
109
|
+
it('includes Event ID when event has id field', async () => {
|
|
110
|
+
mockGraphFetch.mockResolvedValue({
|
|
111
|
+
ok: true,
|
|
112
|
+
data: {
|
|
113
|
+
value: [
|
|
114
|
+
{
|
|
115
|
+
id: 'event-abc123',
|
|
116
|
+
subject: 'ID Test',
|
|
117
|
+
isAllDay: false,
|
|
118
|
+
start: { dateTime: '2025-01-15T09:00:00' },
|
|
119
|
+
end: { dateTime: '2025-01-15T10:00:00' },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const result = await executeCalendar('test-token', { date: '2025-01-15' });
|
|
125
|
+
expect(result).toContain('Event ID: event-abc123');
|
|
126
|
+
});
|
|
109
127
|
it('handles events with minimal fields', async () => {
|
|
110
128
|
mockGraphFetch.mockResolvedValue({
|
|
111
129
|
ok: true,
|
|
@@ -194,6 +212,35 @@ describe('executeCalendar', () => {
|
|
|
194
212
|
expect(result).not.toContain('A'.repeat(800));
|
|
195
213
|
expect(result).toContain('A'.repeat(500) + '...');
|
|
196
214
|
});
|
|
215
|
+
it('strips Teams meeting boilerplate from event body', async () => {
|
|
216
|
+
const teamsBody = 'Sprint planning session\n\n' +
|
|
217
|
+
'________________________________________________________________________________\n' +
|
|
218
|
+
'Microsoft Teams meeting\n' +
|
|
219
|
+
'Join on your computer, mobile app or room device\n' +
|
|
220
|
+
'Click here to join the meeting\n' +
|
|
221
|
+
'Meeting ID: 123 456 789\n' +
|
|
222
|
+
'Passcode: abc123\n' +
|
|
223
|
+
'Dial in: +1 555-0100';
|
|
224
|
+
mockGraphFetch.mockResolvedValue({
|
|
225
|
+
ok: true,
|
|
226
|
+
data: {
|
|
227
|
+
value: [
|
|
228
|
+
{
|
|
229
|
+
subject: 'Teams Meeting',
|
|
230
|
+
isAllDay: false,
|
|
231
|
+
start: { dateTime: '2025-01-15T09:00:00' },
|
|
232
|
+
end: { dateTime: '2025-01-15T10:00:00' },
|
|
233
|
+
body: { contentType: 'text', content: teamsBody },
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
const result = await executeCalendar('test-token', { date: '2025-01-15' });
|
|
239
|
+
expect(result).toContain('Sprint planning session');
|
|
240
|
+
expect(result).not.toContain('Microsoft Teams meeting');
|
|
241
|
+
expect(result).not.toContain('Passcode');
|
|
242
|
+
expect(result).not.toContain('Dial in');
|
|
243
|
+
});
|
|
197
244
|
it('requests body field in $select', async () => {
|
|
198
245
|
mockGraphFetch.mockResolvedValue({
|
|
199
246
|
ok: true,
|
|
@@ -204,4 +251,119 @@ describe('executeCalendar', () => {
|
|
|
204
251
|
expect(calledPath).toContain('body');
|
|
205
252
|
expect(calledPath).not.toContain('bodyPreview');
|
|
206
253
|
});
|
|
254
|
+
describe('calendars list mode', () => {
|
|
255
|
+
it('lists calendars with default marker', async () => {
|
|
256
|
+
mockGraphFetch.mockResolvedValue({
|
|
257
|
+
ok: true,
|
|
258
|
+
data: {
|
|
259
|
+
value: [
|
|
260
|
+
{ name: 'Calendar', color: 'auto', isDefaultCalendar: true, canEdit: true },
|
|
261
|
+
{ name: 'Work', color: 'lightBlue', isDefaultCalendar: false, canEdit: false },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
const result = await executeCalendar('test-token', { calendars: true });
|
|
266
|
+
expect(result).toContain('## Calendar (default)');
|
|
267
|
+
expect(result).toContain('Color: auto');
|
|
268
|
+
expect(result).toContain('Can edit: Yes');
|
|
269
|
+
expect(result).toContain('## Work');
|
|
270
|
+
expect(result).toContain('Can edit: No');
|
|
271
|
+
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/calendars'), 'test-token');
|
|
272
|
+
});
|
|
273
|
+
it('returns message when no calendars found', async () => {
|
|
274
|
+
mockGraphFetch.mockResolvedValue({
|
|
275
|
+
ok: true,
|
|
276
|
+
data: { value: [] },
|
|
277
|
+
});
|
|
278
|
+
const result = await executeCalendar('test-token', { calendars: true });
|
|
279
|
+
expect(result).toBe('No calendars found.');
|
|
280
|
+
});
|
|
281
|
+
it('handles error when listing calendars', async () => {
|
|
282
|
+
mockGraphFetch.mockResolvedValue({
|
|
283
|
+
ok: false,
|
|
284
|
+
error: { status: 403, message: 'Insufficient permissions.' },
|
|
285
|
+
});
|
|
286
|
+
const result = await executeCalendar('test-token', { calendars: true });
|
|
287
|
+
expect(result).toBe('Error: Insufficient permissions.');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
describe('event detail mode', () => {
|
|
291
|
+
it('shows full event detail with attendees and Teams URL', async () => {
|
|
292
|
+
mockGraphFetch.mockResolvedValue({
|
|
293
|
+
ok: true,
|
|
294
|
+
data: {
|
|
295
|
+
subject: 'Sprint Planning',
|
|
296
|
+
start: { dateTime: '2025-01-15T09:00:00', timeZone: 'Europe/London' },
|
|
297
|
+
end: { dateTime: '2025-01-15T10:00:00', timeZone: 'Europe/London' },
|
|
298
|
+
organizer: { emailAddress: { name: 'Alice', address: 'alice@example.com' } },
|
|
299
|
+
attendees: [
|
|
300
|
+
{
|
|
301
|
+
emailAddress: { name: 'Bob', address: 'bob@example.com' },
|
|
302
|
+
status: { response: 'accepted' },
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
emailAddress: { name: 'Carol', address: 'carol@example.com' },
|
|
306
|
+
status: { response: 'tentative' },
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
body: {
|
|
310
|
+
contentType: 'html',
|
|
311
|
+
content: '<html><body><p>Sprint planning session</p><p>Please prepare your updates</p></body></html>',
|
|
312
|
+
},
|
|
313
|
+
location: { displayName: 'Conference Room B' },
|
|
314
|
+
onlineMeeting: { joinUrl: 'https://teams.microsoft.com/l/meetup-join/abc123' },
|
|
315
|
+
hasAttachments: true,
|
|
316
|
+
showAs: 'busy',
|
|
317
|
+
importance: 'high',
|
|
318
|
+
categories: ['Work', 'Sprint'],
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const result = await executeCalendar('test-token', { event_id: 'event-123' });
|
|
322
|
+
expect(result).toContain('## Sprint Planning');
|
|
323
|
+
expect(result).toContain('Time: 2025-01-15T09:00:00 - 2025-01-15T10:00:00');
|
|
324
|
+
expect(result).toContain('Location: Conference Room B');
|
|
325
|
+
expect(result).toContain('Organizer: Alice (alice@example.com)');
|
|
326
|
+
expect(result).toContain('Bob (accepted)');
|
|
327
|
+
expect(result).toContain('Carol (tentative)');
|
|
328
|
+
expect(result).toContain('Sprint planning session');
|
|
329
|
+
expect(result).toContain('Please prepare your updates');
|
|
330
|
+
expect(result).not.toContain('<p>');
|
|
331
|
+
expect(result).toContain('https://teams.microsoft.com/l/meetup-join/abc123');
|
|
332
|
+
expect(result).toContain('Categories: Work, Sprint');
|
|
333
|
+
expect(result).toContain('Importance: high');
|
|
334
|
+
expect(result).toContain('Show as: busy');
|
|
335
|
+
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/events/event-123'), 'test-token');
|
|
336
|
+
});
|
|
337
|
+
it('handles event with no online meeting', async () => {
|
|
338
|
+
mockGraphFetch.mockResolvedValue({
|
|
339
|
+
ok: true,
|
|
340
|
+
data: {
|
|
341
|
+
subject: 'In-Person Meeting',
|
|
342
|
+
start: { dateTime: '2025-01-15T14:00:00', timeZone: 'Europe/London' },
|
|
343
|
+
end: { dateTime: '2025-01-15T15:00:00', timeZone: 'Europe/London' },
|
|
344
|
+
organizer: { emailAddress: { name: 'Dave', address: 'dave@example.com' } },
|
|
345
|
+
attendees: [],
|
|
346
|
+
body: { contentType: 'text', content: 'Office meeting' },
|
|
347
|
+
location: { displayName: 'Room C' },
|
|
348
|
+
onlineMeeting: null,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
const result = await executeCalendar('test-token', { event_id: 'event-456' });
|
|
352
|
+
expect(result).toContain('## In-Person Meeting');
|
|
353
|
+
expect(result).toContain('Location: Room C');
|
|
354
|
+
expect(result).not.toContain('Teams');
|
|
355
|
+
expect(result).not.toContain('joinUrl');
|
|
356
|
+
});
|
|
357
|
+
it('handles event_id error', async () => {
|
|
358
|
+
mockGraphFetch.mockResolvedValue({
|
|
359
|
+
ok: false,
|
|
360
|
+
error: {
|
|
361
|
+
status: 404,
|
|
362
|
+
message: 'Event not found.',
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
const result = await executeCalendar('test-token', { event_id: 'bad-id' });
|
|
366
|
+
expect(result).toBe('Error: Event not found.');
|
|
367
|
+
});
|
|
368
|
+
});
|
|
207
369
|
});
|
|
@@ -124,7 +124,7 @@ describe('executeChat', () => {
|
|
|
124
124
|
await executeChat('test-token', { chat_id: '19:abc@thread.v2' });
|
|
125
125
|
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/chats/19%3Aabc%40thread.v2/messages'), 'test-token', { timezone: false });
|
|
126
126
|
});
|
|
127
|
-
it('handles chat listing with no topic', async () => {
|
|
127
|
+
it('handles chat listing with no topic and no members', async () => {
|
|
128
128
|
mockGraphFetch.mockResolvedValue({
|
|
129
129
|
ok: true,
|
|
130
130
|
data: {
|
|
@@ -143,6 +143,88 @@ describe('executeChat', () => {
|
|
|
143
143
|
expect(result).toContain('Type: oneOnOne');
|
|
144
144
|
expect(result).toContain('Chat ID: chat-456');
|
|
145
145
|
});
|
|
146
|
+
it('shows member names for oneOnOne chats without topic', async () => {
|
|
147
|
+
mockGraphFetch.mockResolvedValue({
|
|
148
|
+
ok: true,
|
|
149
|
+
data: {
|
|
150
|
+
value: [
|
|
151
|
+
{
|
|
152
|
+
id: 'chat-789',
|
|
153
|
+
topic: null,
|
|
154
|
+
chatType: 'oneOnOne',
|
|
155
|
+
members: [{ displayName: 'Alice' }, { displayName: 'Bob' }],
|
|
156
|
+
lastMessagePreview: {
|
|
157
|
+
body: { content: 'Hi there' },
|
|
158
|
+
createdDateTime: '2025-06-15T10:00:00Z',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const result = await executeChat('test-token', {});
|
|
165
|
+
expect(result).toContain('## Alice, Bob');
|
|
166
|
+
expect(result).toContain('Type: oneOnOne');
|
|
167
|
+
expect(result).not.toContain('oneOnOne chat');
|
|
168
|
+
});
|
|
169
|
+
it('handles messages with missing from and createdDateTime', async () => {
|
|
170
|
+
mockGraphFetch.mockResolvedValue({
|
|
171
|
+
ok: true,
|
|
172
|
+
data: {
|
|
173
|
+
value: [
|
|
174
|
+
{
|
|
175
|
+
from: undefined,
|
|
176
|
+
createdDateTime: undefined,
|
|
177
|
+
body: { content: 'System message', contentType: 'text' },
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const result = await executeChat('test-token', { chat_id: 'chat-123' });
|
|
183
|
+
expect(result).toContain('**Unknown**');
|
|
184
|
+
expect(result).toContain('(N/A)');
|
|
185
|
+
expect(result).toContain('System message');
|
|
186
|
+
});
|
|
187
|
+
it('falls back to oneOnOne chat when all member displayNames are undefined', async () => {
|
|
188
|
+
mockGraphFetch.mockResolvedValue({
|
|
189
|
+
ok: true,
|
|
190
|
+
data: {
|
|
191
|
+
value: [
|
|
192
|
+
{
|
|
193
|
+
id: 'chat-nonames',
|
|
194
|
+
topic: null,
|
|
195
|
+
chatType: 'oneOnOne',
|
|
196
|
+
members: [{ displayName: undefined }, { displayName: undefined }],
|
|
197
|
+
lastMessagePreview: null,
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
const result = await executeChat('test-token', {});
|
|
203
|
+
expect(result).toContain('## oneOnOne chat');
|
|
204
|
+
});
|
|
205
|
+
it('handles lastMessagePreview with null body content and null createdDateTime', async () => {
|
|
206
|
+
mockGraphFetch.mockResolvedValue({
|
|
207
|
+
ok: true,
|
|
208
|
+
data: {
|
|
209
|
+
value: [
|
|
210
|
+
{
|
|
211
|
+
id: 'chat-preview',
|
|
212
|
+
topic: 'Preview Test',
|
|
213
|
+
chatType: 'group',
|
|
214
|
+
lastMessagePreview: {
|
|
215
|
+
body: { content: null },
|
|
216
|
+
createdDateTime: null,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
const result = await executeChat('test-token', {});
|
|
223
|
+
expect(result).toContain('## Preview Test');
|
|
224
|
+
expect(result).toContain('(no preview)');
|
|
225
|
+
// No timestamp in the last message line since createdDateTime is null
|
|
226
|
+
expect(result).toContain('Last message: (no preview)');
|
|
227
|
+
});
|
|
146
228
|
it('handles messages with empty body', async () => {
|
|
147
229
|
mockGraphFetch.mockResolvedValue({
|
|
148
230
|
ok: true,
|
|
@@ -181,6 +263,71 @@ describe('executeChat', () => {
|
|
|
181
263
|
expect(result).not.toContain('<p>');
|
|
182
264
|
expect(result).not.toContain('<at');
|
|
183
265
|
});
|
|
266
|
+
it('lists chat members when chat_id and members are provided', async () => {
|
|
267
|
+
mockGraphFetch.mockResolvedValue({
|
|
268
|
+
ok: true,
|
|
269
|
+
data: {
|
|
270
|
+
value: [
|
|
271
|
+
{
|
|
272
|
+
displayName: 'Alice Smith',
|
|
273
|
+
email: 'alice@example.com',
|
|
274
|
+
roles: ['owner'],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
displayName: 'Bob Jones',
|
|
278
|
+
email: 'bob@example.com',
|
|
279
|
+
roles: ['guest'],
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const result = await executeChat('test-token', { chat_id: 'chat-123', members: true });
|
|
285
|
+
expect(result).toContain('## Chat Members');
|
|
286
|
+
expect(result).toContain('Alice Smith');
|
|
287
|
+
expect(result).toContain('alice@example.com');
|
|
288
|
+
expect(result).toContain('owner');
|
|
289
|
+
expect(result).toContain('Bob Jones');
|
|
290
|
+
expect(result).toContain('bob@example.com');
|
|
291
|
+
expect(result).toContain('guest');
|
|
292
|
+
});
|
|
293
|
+
it('does not include $select in members URL', async () => {
|
|
294
|
+
mockGraphFetch.mockResolvedValue({
|
|
295
|
+
ok: true,
|
|
296
|
+
data: { value: [] },
|
|
297
|
+
});
|
|
298
|
+
await executeChat('test-token', { chat_id: 'chat-123', members: true });
|
|
299
|
+
const calledPath = mockGraphFetch.mock.calls[0][0];
|
|
300
|
+
expect(calledPath).toContain('/me/chats/chat-123/members');
|
|
301
|
+
expect(calledPath).not.toContain('$select');
|
|
302
|
+
});
|
|
303
|
+
it('handles empty members list', async () => {
|
|
304
|
+
mockGraphFetch.mockResolvedValue({
|
|
305
|
+
ok: true,
|
|
306
|
+
data: { value: [] },
|
|
307
|
+
});
|
|
308
|
+
const result = await executeChat('test-token', { chat_id: 'chat-123', members: true });
|
|
309
|
+
expect(result).toBe('No members found in this chat.');
|
|
310
|
+
});
|
|
311
|
+
it('handles error fetching members', async () => {
|
|
312
|
+
mockGraphFetch.mockResolvedValue({
|
|
313
|
+
ok: false,
|
|
314
|
+
error: {
|
|
315
|
+
status: 403,
|
|
316
|
+
message: 'Insufficient permissions. Check granted scopes with ms_auth_status.',
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
const result = await executeChat('test-token', { chat_id: 'chat-123', members: true });
|
|
320
|
+
expect(result).toBe('Error: Insufficient permissions. Check granted scopes with ms_auth_status.');
|
|
321
|
+
});
|
|
322
|
+
it('does not include $orderby in chat list URL', async () => {
|
|
323
|
+
mockGraphFetch.mockResolvedValue({
|
|
324
|
+
ok: true,
|
|
325
|
+
data: { value: [] },
|
|
326
|
+
});
|
|
327
|
+
await executeChat('test-token', {});
|
|
328
|
+
const calledPath = mockGraphFetch.mock.calls[0][0];
|
|
329
|
+
expect(calledPath).not.toContain('$orderby');
|
|
330
|
+
});
|
|
184
331
|
it('handles emoji tags in messages', async () => {
|
|
185
332
|
mockGraphFetch.mockResolvedValue({
|
|
186
333
|
ok: true,
|
|
@@ -39,6 +39,32 @@ describe('executeFiles', () => {
|
|
|
39
39
|
expect(result).toContain('https://onedrive.example.com/Documents');
|
|
40
40
|
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/drive/root/children'), 'test-token', { timezone: false });
|
|
41
41
|
});
|
|
42
|
+
it('includes item ID when file items have id field', async () => {
|
|
43
|
+
mockGraphFetch.mockResolvedValue({
|
|
44
|
+
ok: true,
|
|
45
|
+
data: {
|
|
46
|
+
value: [
|
|
47
|
+
{
|
|
48
|
+
id: 'file-id-001',
|
|
49
|
+
name: 'notes.txt',
|
|
50
|
+
size: 1024,
|
|
51
|
+
lastModifiedDateTime: '2025-06-15T10:00:00Z',
|
|
52
|
+
file: { mimeType: 'text/plain' },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const result = await executeFiles('test-token', {});
|
|
58
|
+
expect(result).toContain('ID: file-id-001');
|
|
59
|
+
});
|
|
60
|
+
it('handles shared files error', async () => {
|
|
61
|
+
mockGraphFetch.mockResolvedValue({
|
|
62
|
+
ok: false,
|
|
63
|
+
error: { status: 403, message: 'Access denied.' },
|
|
64
|
+
});
|
|
65
|
+
const result = await executeFiles('test-token', { shared: true });
|
|
66
|
+
expect(result).toBe('Error: Access denied.');
|
|
67
|
+
});
|
|
42
68
|
it('handles path param', async () => {
|
|
43
69
|
mockGraphFetch.mockResolvedValue({
|
|
44
70
|
ok: true,
|
|
@@ -117,6 +143,94 @@ describe('executeFiles', () => {
|
|
|
117
143
|
await executeFiles('test-token', { search: 'test', path: '/Documents' });
|
|
118
144
|
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/drive/root/search'), 'test-token', { timezone: false });
|
|
119
145
|
});
|
|
146
|
+
it('fetches file detail with download URL', async () => {
|
|
147
|
+
mockGraphFetch.mockResolvedValue({
|
|
148
|
+
ok: true,
|
|
149
|
+
data: {
|
|
150
|
+
name: 'report.docx',
|
|
151
|
+
size: 25600,
|
|
152
|
+
lastModifiedDateTime: '2026-01-15T10:00:00Z',
|
|
153
|
+
webUrl: 'https://onedrive.example.com/report.docx',
|
|
154
|
+
'@microsoft.graph.downloadUrl': 'https://download.example.com/report.docx?token=abc',
|
|
155
|
+
file: {
|
|
156
|
+
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const result = await executeFiles('test-token', { item_id: 'item-123' });
|
|
161
|
+
expect(result).toContain('# report.docx');
|
|
162
|
+
expect(result).toContain('Type: file');
|
|
163
|
+
expect(result).toContain('25.0 KB');
|
|
164
|
+
expect(result).toContain('Download URL: https://download.example.com/report.docx?token=abc');
|
|
165
|
+
expect(result).toContain('Web URL: https://onedrive.example.com/report.docx');
|
|
166
|
+
expect(mockGraphFetch).toHaveBeenCalledWith('/me/drive/items/item-123', 'test-token', {
|
|
167
|
+
timezone: false,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
it('fetches file detail for folder without download URL', async () => {
|
|
171
|
+
mockGraphFetch.mockResolvedValue({
|
|
172
|
+
ok: true,
|
|
173
|
+
data: {
|
|
174
|
+
name: 'Documents',
|
|
175
|
+
size: 0,
|
|
176
|
+
lastModifiedDateTime: '2026-01-15T10:00:00Z',
|
|
177
|
+
webUrl: 'https://onedrive.example.com/Documents',
|
|
178
|
+
folder: { childCount: 12 },
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
const result = await executeFiles('test-token', { item_id: 'folder-456' });
|
|
182
|
+
expect(result).toContain('# Documents');
|
|
183
|
+
expect(result).toContain('Type: folder');
|
|
184
|
+
expect(result).toContain('Children: 12');
|
|
185
|
+
expect(result).not.toContain('Download URL');
|
|
186
|
+
expect(mockGraphFetch).toHaveBeenCalledWith('/me/drive/items/folder-456', 'test-token', {
|
|
187
|
+
timezone: false,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
it('fetches shared files with sharer name', async () => {
|
|
191
|
+
mockGraphFetch.mockResolvedValue({
|
|
192
|
+
ok: true,
|
|
193
|
+
data: {
|
|
194
|
+
value: [
|
|
195
|
+
{
|
|
196
|
+
name: 'shared-doc.pdf',
|
|
197
|
+
size: 51200,
|
|
198
|
+
lastModifiedDateTime: '2026-01-20T14:00:00Z',
|
|
199
|
+
webUrl: 'https://onedrive.example.com/shared-doc.pdf',
|
|
200
|
+
file: { mimeType: 'application/pdf' },
|
|
201
|
+
remoteItem: {
|
|
202
|
+
shared: {
|
|
203
|
+
sharedBy: { user: { displayName: 'Jane Smith' } },
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const result = await executeFiles('test-token', { shared: true });
|
|
211
|
+
expect(result).toContain('shared-doc.pdf');
|
|
212
|
+
expect(result).toContain('Shared by: Jane Smith');
|
|
213
|
+
expect(mockGraphFetch).toHaveBeenCalledWith(expect.stringContaining('/me/drive/sharedWithMe'), 'test-token', { timezone: false });
|
|
214
|
+
});
|
|
215
|
+
it('handles empty shared files', async () => {
|
|
216
|
+
mockGraphFetch.mockResolvedValue({
|
|
217
|
+
ok: true,
|
|
218
|
+
data: { value: [] },
|
|
219
|
+
});
|
|
220
|
+
const result = await executeFiles('test-token', { shared: true });
|
|
221
|
+
expect(result).toBe('No shared files found.');
|
|
222
|
+
});
|
|
223
|
+
it('handles file detail error', async () => {
|
|
224
|
+
mockGraphFetch.mockResolvedValue({
|
|
225
|
+
ok: false,
|
|
226
|
+
error: {
|
|
227
|
+
status: 404,
|
|
228
|
+
message: 'Resource not found. The item may not exist or you may lack access.',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const result = await executeFiles('test-token', { item_id: 'nonexistent' });
|
|
232
|
+
expect(result).toBe('Error: Resource not found. The item may not exist or you may lack access.');
|
|
233
|
+
});
|
|
120
234
|
});
|
|
121
235
|
describe('formatFileSize', () => {
|
|
122
236
|
it('formats bytes', () => {
|