m365-agent-cli 1.2.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.
Files changed (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,178 @@
1
+ import type { GraphResponse } from './graph-client.js';
2
+ import { callGraph, GraphApiError, graphError, graphResult } from './graph-client.js';
3
+ import { graphUserPath } from './graph-user-path.js';
4
+
5
+ function inboxRulesBase(user?: string): string {
6
+ return `${graphUserPath(user, 'mailFolders/inbox/messageRules')}`;
7
+ }
8
+
9
+ export interface MessageRuleCondition {
10
+ /** Strings the message body contains (case-insensitive) */
11
+ bodyContains?: string[];
12
+ /** Strings the message subject contains (case-insensitive) */
13
+ subjectContains?: string[];
14
+ /** Strings the message sender contains (case-insensitive) */
15
+ senderContains?: string[];
16
+ /** Strings any recipient contains (case-insensitive) */
17
+ recipientContains?: string[];
18
+ /** Senders to match */
19
+ fromAddresses?: { emailAddress: { name?: string; address: string } }[];
20
+ /** Recipients to match */
21
+ sentToAddresses?: { emailAddress: { name?: string; address: string } }[];
22
+ /** Message has attachments */
23
+ hasAttachments?: boolean;
24
+ /** Importance: low | normal | high */
25
+ importance?: 'Low' | 'Normal' | 'High';
26
+ /** Message is an automatic forward */
27
+ isAutomaticForward?: boolean;
28
+ }
29
+
30
+ export interface MessageRuleAction {
31
+ /** Move message to this folder (name or well-known name) */
32
+ moveToFolder?: string;
33
+ /** Copy message to this folder */
34
+ copyToFolder?: string;
35
+ /** Soft-delete the message */
36
+ delete?: boolean;
37
+ /** Permanently delete the message */
38
+ permanentDelete?: boolean;
39
+ /** Mark as read */
40
+ markAsRead?: boolean;
41
+ /** Set importance: low | normal | high */
42
+ markImportance?: 'Low' | 'Normal' | 'High';
43
+ /** Forward to these recipients */
44
+ forwardToRecipients?: { emailAddress: { name?: string; address: string } }[];
45
+ /** Forward as attachment to these recipients */
46
+ forwardAsAttachmentToRecipients?: { emailAddress: { name?: string; address: string } }[];
47
+ /** Assign categories */
48
+ assignCategories?: string[];
49
+ /** Stop processing more rules */
50
+ stopProcessingRules?: boolean;
51
+ }
52
+
53
+ export interface MessageRule {
54
+ id: string;
55
+ displayName: string;
56
+ priority: number;
57
+ isEnabled: boolean;
58
+ conditions?: MessageRuleCondition;
59
+ actions?: MessageRuleAction;
60
+ exceptionConditions?: MessageRuleCondition;
61
+ /** Human-readable description auto-generated by Exchange */
62
+ ruleDescription?: string;
63
+ }
64
+
65
+ interface MessageRuleListResponse {
66
+ value: MessageRule[];
67
+ }
68
+
69
+ export interface CreateMessageRulePayload {
70
+ displayName: string;
71
+ priority?: number;
72
+ isEnabled?: boolean;
73
+ conditions?: MessageRuleCondition;
74
+ actions: MessageRuleAction;
75
+ exceptionConditions?: MessageRuleCondition;
76
+ }
77
+
78
+ export interface UpdateMessageRulePayload {
79
+ displayName?: string;
80
+ priority?: number;
81
+ isEnabled?: boolean;
82
+ conditions?: MessageRuleCondition;
83
+ actions?: MessageRuleAction;
84
+ exceptionConditions?: MessageRuleCondition;
85
+ }
86
+
87
+ /** List all inbox message rules */
88
+ export async function listMessageRules(token: string, user?: string): Promise<GraphResponse<MessageRule[]>> {
89
+ let result: GraphResponse<MessageRuleListResponse>;
90
+ try {
91
+ result = await callGraph<MessageRuleListResponse>(token, inboxRulesBase(user));
92
+ } catch (err) {
93
+ if (err instanceof GraphApiError) {
94
+ return graphError(err.message, err.code, err.status);
95
+ }
96
+ return graphError(err instanceof Error ? err.message : 'Failed to list message rules');
97
+ }
98
+ if (!result.ok || !result.data) {
99
+ return graphError(
100
+ result.error?.message || 'Failed to list message rules',
101
+ result.error?.code,
102
+ result.error?.status
103
+ );
104
+ }
105
+ return graphResult(result.data.value || []);
106
+ }
107
+
108
+ /** Get a single message rule by ID */
109
+ export async function getMessageRule(
110
+ token: string,
111
+ ruleId: string,
112
+ user?: string
113
+ ): Promise<GraphResponse<MessageRule>> {
114
+ try {
115
+ return await callGraph<MessageRule>(token, `${inboxRulesBase(user)}/${encodeURIComponent(ruleId)}`);
116
+ } catch (err) {
117
+ if (err instanceof GraphApiError) {
118
+ return graphError(err.message, err.code, err.status);
119
+ }
120
+ return graphError(err instanceof Error ? err.message : 'Failed to get message rule');
121
+ }
122
+ }
123
+
124
+ /** Create a new inbox message rule */
125
+ export async function createMessageRule(
126
+ token: string,
127
+ payload: CreateMessageRulePayload,
128
+ user?: string
129
+ ): Promise<GraphResponse<MessageRule>> {
130
+ try {
131
+ return await callGraph<MessageRule>(token, inboxRulesBase(user), {
132
+ method: 'POST',
133
+ body: JSON.stringify(payload)
134
+ });
135
+ } catch (err) {
136
+ if (err instanceof GraphApiError) {
137
+ return graphError(err.message, err.code, err.status);
138
+ }
139
+ return graphError(err instanceof Error ? err.message : 'Failed to create message rule');
140
+ }
141
+ }
142
+
143
+ /** Update an existing inbox message rule */
144
+ export async function updateMessageRule(
145
+ token: string,
146
+ ruleId: string,
147
+ payload: UpdateMessageRulePayload,
148
+ user?: string
149
+ ): Promise<GraphResponse<MessageRule>> {
150
+ try {
151
+ return await callGraph<MessageRule>(token, `${inboxRulesBase(user)}/${encodeURIComponent(ruleId)}`, {
152
+ method: 'PATCH',
153
+ body: JSON.stringify(payload)
154
+ });
155
+ } catch (err) {
156
+ if (err instanceof GraphApiError) {
157
+ return graphError(err.message, err.code, err.status);
158
+ }
159
+ return graphError(err instanceof Error ? err.message : 'Failed to update message rule');
160
+ }
161
+ }
162
+
163
+ /** Delete an inbox message rule */
164
+ export async function deleteMessageRule(token: string, ruleId: string, user?: string): Promise<GraphResponse<void>> {
165
+ try {
166
+ return await callGraph<void>(
167
+ token,
168
+ `${inboxRulesBase(user)}/${encodeURIComponent(ruleId)}`,
169
+ { method: 'DELETE' },
170
+ false
171
+ );
172
+ } catch (err) {
173
+ if (err instanceof GraphApiError) {
174
+ return graphError(err.message, err.code, err.status);
175
+ }
176
+ return graphError(err instanceof Error ? err.message : 'Failed to delete message rule');
177
+ }
178
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ callGraph,
3
+ fetchAllPages,
4
+ GraphApiError,
5
+ type GraphResponse,
6
+ graphError,
7
+ graphResult
8
+ } from './graph-client.js';
9
+
10
+ export interface SharePointList {
11
+ id: string;
12
+ name: string;
13
+ displayName: string;
14
+ description?: string;
15
+ createdDateTime: string;
16
+ lastModifiedDateTime: string;
17
+ webUrl: string;
18
+ }
19
+
20
+ export interface SharePointListItem {
21
+ id: string;
22
+ createdDateTime: string;
23
+ lastModifiedDateTime: string;
24
+ webUrl: string;
25
+ fields: Record<string, any>;
26
+ }
27
+
28
+ export async function getLists(token: string, siteId: string): Promise<GraphResponse<SharePointList[]>> {
29
+ let res: GraphResponse<{ value: SharePointList[] }>;
30
+ try {
31
+ res = await callGraph<{ value: SharePointList[] }>(token, `/sites/${siteId}/lists`);
32
+ } catch (err) {
33
+ if (err instanceof GraphApiError) {
34
+ return graphError(err.message, err.code, err.status);
35
+ }
36
+ return graphError(err instanceof Error ? err.message : 'Failed to get lists');
37
+ }
38
+ if (!res.ok || !res.data?.value) return graphError('Failed to get lists: missing data');
39
+ return graphResult(res.data.value);
40
+ }
41
+
42
+ export async function getListItems(
43
+ token: string,
44
+ siteId: string,
45
+ listId: string
46
+ ): Promise<GraphResponse<SharePointListItem[]>> {
47
+ return fetchAllPages<SharePointListItem>(
48
+ token,
49
+ `/sites/${siteId}/lists/${encodeURIComponent(listId)}/items?$expand=fields`,
50
+ 'Failed to get list items'
51
+ );
52
+ }
53
+
54
+ export async function createListItem(
55
+ token: string,
56
+ siteId: string,
57
+ listId: string,
58
+ fields: Record<string, any>
59
+ ): Promise<GraphResponse<SharePointListItem>> {
60
+ try {
61
+ return await callGraph<SharePointListItem>(token, `/sites/${siteId}/lists/${encodeURIComponent(listId)}/items`, {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Content-Type': 'application/json'
65
+ },
66
+ body: JSON.stringify({ fields })
67
+ });
68
+ } catch (err) {
69
+ if (err instanceof GraphApiError) {
70
+ return graphError(err.message, err.code, err.status);
71
+ }
72
+ return graphError(err instanceof Error ? err.message : 'Failed to create list item');
73
+ }
74
+ }
75
+
76
+ export async function updateListItem(
77
+ token: string,
78
+ siteId: string,
79
+ listId: string,
80
+ itemId: string,
81
+ fields: Record<string, any>
82
+ ): Promise<GraphResponse<Record<string, any>>> {
83
+ try {
84
+ return await callGraph<Record<string, any>>(
85
+ token,
86
+ `/sites/${siteId}/lists/${encodeURIComponent(listId)}/items/${encodeURIComponent(itemId)}/fields`,
87
+ {
88
+ method: 'PATCH',
89
+ headers: {
90
+ 'Content-Type': 'application/json'
91
+ },
92
+ body: JSON.stringify(fields)
93
+ }
94
+ );
95
+ } catch (err) {
96
+ if (err instanceof GraphApiError) {
97
+ return graphError(err.message, err.code, err.status);
98
+ }
99
+ return graphError(err instanceof Error ? err.message : 'Failed to update list item');
100
+ }
101
+ }
@@ -0,0 +1,73 @@
1
+ import { callGraph, fetchAllPages, GraphApiError, type GraphResponse, graphError } from './graph-client.js';
2
+
3
+ export interface SitePage {
4
+ id: string;
5
+ name?: string;
6
+ title?: string;
7
+ pageLayout?: string;
8
+ publishingState?: {
9
+ level: string;
10
+ versionId: string;
11
+ };
12
+ webUrl?: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ export async function listSitePages(token: string, siteId: string): Promise<GraphResponse<SitePage[]>> {
17
+ return fetchAllPages<SitePage>(token, `/sites/${siteId}/pages/microsoft.graph.sitePage`, 'Failed to list site pages');
18
+ }
19
+
20
+ export async function getSitePage(token: string, siteId: string, pageId: string): Promise<GraphResponse<SitePage>> {
21
+ try {
22
+ return await callGraph<SitePage>(
23
+ token,
24
+ `/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`
25
+ );
26
+ } catch (err) {
27
+ if (err instanceof GraphApiError) {
28
+ return graphError(err.message, err.code, err.status);
29
+ }
30
+ return graphError(err instanceof Error ? err.message : 'Failed to get site page');
31
+ }
32
+ }
33
+
34
+ export async function updateSitePage(
35
+ token: string,
36
+ siteId: string,
37
+ pageId: string,
38
+ pageData: Partial<SitePage>
39
+ ): Promise<GraphResponse<SitePage>> {
40
+ try {
41
+ return await callGraph<SitePage>(
42
+ token,
43
+ `/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage`,
44
+ {
45
+ method: 'PATCH',
46
+ body: JSON.stringify({ '@odata.type': '#microsoft.graph.sitePage', ...pageData })
47
+ }
48
+ );
49
+ } catch (err) {
50
+ if (err instanceof GraphApiError) {
51
+ return graphError(err.message, err.code, err.status);
52
+ }
53
+ return graphError(err instanceof Error ? err.message : 'Failed to update site page');
54
+ }
55
+ }
56
+
57
+ export async function publishSitePage(token: string, siteId: string, pageId: string): Promise<GraphResponse<void>> {
58
+ try {
59
+ return await callGraph<void>(
60
+ token,
61
+ `/sites/${siteId}/pages/${encodeURIComponent(pageId)}/microsoft.graph.sitePage/publish`,
62
+ {
63
+ method: 'POST'
64
+ },
65
+ false // might not return JSON, just 204 No Content
66
+ );
67
+ } catch (err) {
68
+ if (err instanceof GraphApiError) {
69
+ return graphError(err.message, err.code, err.status);
70
+ }
71
+ return graphError(err instanceof Error ? err.message : 'Failed to publish site page');
72
+ }
73
+ }
@@ -0,0 +1,298 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ const token = 'test-token';
4
+ const baseUrl = 'https://graph.microsoft.com/v1.0';
5
+
6
+ describe('linkedResourceToGraphPayload', () => {
7
+ it('prefers displayName over description', async () => {
8
+ const { linkedResourceToGraphPayload } = await import('./todo-client.js');
9
+ expect(linkedResourceToGraphPayload({ displayName: 'A', description: 'B', webUrl: 'https://example.com' })).toEqual(
10
+ {
11
+ displayName: 'A',
12
+ webUrl: 'https://example.com'
13
+ }
14
+ );
15
+ });
16
+
17
+ it('uses description when displayName absent', async () => {
18
+ const { linkedResourceToGraphPayload } = await import('./todo-client.js');
19
+ expect(linkedResourceToGraphPayload({ description: 'Only', webUrl: 'https://x' })).toEqual({
20
+ displayName: 'Only',
21
+ webUrl: 'https://x'
22
+ });
23
+ });
24
+
25
+ it('includes optional applicationName, externalId, id', async () => {
26
+ const { linkedResourceToGraphPayload } = await import('./todo-client.js');
27
+ expect(
28
+ linkedResourceToGraphPayload({
29
+ displayName: 'T',
30
+ applicationName: 'App',
31
+ externalId: 'ext-1',
32
+ id: 'lr-id'
33
+ })
34
+ ).toEqual({
35
+ displayName: 'T',
36
+ applicationName: 'App',
37
+ externalId: 'ext-1',
38
+ id: 'lr-id'
39
+ });
40
+ });
41
+
42
+ it('omits empty displayName string', async () => {
43
+ const { linkedResourceToGraphPayload } = await import('./todo-client.js');
44
+ expect(linkedResourceToGraphPayload({ description: '', webUrl: 'https://u' })).toEqual({
45
+ webUrl: 'https://u'
46
+ });
47
+ });
48
+ });
49
+
50
+ describe('getTasks query options', () => {
51
+ it('requests single page with $top, $skip, $expand, $count', async () => {
52
+ process.env.GRAPH_BASE_URL = baseUrl;
53
+ const urls: string[] = [];
54
+ const originalFetch = globalThis.fetch;
55
+
56
+ try {
57
+ globalThis.fetch = (async (input: string | URL | Request) => {
58
+ urls.push(typeof input === 'string' ? input : input.toString());
59
+ return new Response(JSON.stringify({ value: [] }), {
60
+ status: 200,
61
+ headers: { 'content-type': 'application/json' }
62
+ });
63
+ }) as typeof fetch;
64
+
65
+ const { getTasks } = await import('./todo-client.js');
66
+ const r = await getTasks(token, 'list-1', {
67
+ filter: "status eq 'notStarted'",
68
+ top: 10,
69
+ skip: 5,
70
+ expand: 'attachments',
71
+ count: true
72
+ });
73
+
74
+ expect(r.ok).toBe(true);
75
+ expect(r.data).toEqual([]);
76
+ const u = decodeURIComponent(urls[0]);
77
+ expect(u).toContain('$top=10');
78
+ expect(u).toContain('$skip=5');
79
+ expect(u).toContain('$expand=attachments');
80
+ expect(u).toContain('$count=true');
81
+ } finally {
82
+ globalThis.fetch = originalFetch;
83
+ }
84
+ });
85
+ });
86
+
87
+ describe('getTask $select', () => {
88
+ it('appends $select when provided', async () => {
89
+ process.env.GRAPH_BASE_URL = baseUrl;
90
+ const urls: string[] = [];
91
+ const originalFetch = globalThis.fetch;
92
+
93
+ try {
94
+ globalThis.fetch = (async (input: string | URL | Request) => {
95
+ urls.push(typeof input === 'string' ? input : input.toString());
96
+ return new Response(JSON.stringify({ id: 't1', title: 'x' }), {
97
+ status: 200,
98
+ headers: { 'content-type': 'application/json' }
99
+ });
100
+ }) as typeof fetch;
101
+
102
+ const { getTask } = await import('./todo-client.js');
103
+ const r = await getTask(token, 'list-1', 'task-1', undefined, { select: 'id,title' });
104
+
105
+ expect(r.ok).toBe(true);
106
+ expect(urls[0]).toContain('$select=');
107
+ expect(decodeURIComponent(urls[0])).toContain('id,title');
108
+ } finally {
109
+ globalThis.fetch = originalFetch;
110
+ }
111
+ });
112
+ });
113
+
114
+ describe('getTodoLists / getTodoList', () => {
115
+ it('getTodoLists returns lists from value', async () => {
116
+ process.env.GRAPH_BASE_URL = baseUrl;
117
+ const urls: string[] = [];
118
+ const originalFetch = globalThis.fetch;
119
+
120
+ try {
121
+ globalThis.fetch = (async (input: string | URL | Request) => {
122
+ urls.push(typeof input === 'string' ? input : input.toString());
123
+ return new Response(
124
+ JSON.stringify({
125
+ value: [{ id: 'l1', displayName: 'Work', wellknownListName: 'none' }]
126
+ }),
127
+ { status: 200, headers: { 'content-type': 'application/json' } }
128
+ );
129
+ }) as typeof fetch;
130
+
131
+ const { getTodoLists } = await import('./todo-client.js');
132
+ const r = await getTodoLists(token);
133
+
134
+ expect(r.ok).toBe(true);
135
+ expect(r.data?.[0]?.id).toBe('l1');
136
+ expect(urls[0]).toContain('/me/todo/lists');
137
+ } finally {
138
+ globalThis.fetch = originalFetch;
139
+ }
140
+ });
141
+
142
+ it('getTodoList returns one list', async () => {
143
+ process.env.GRAPH_BASE_URL = baseUrl;
144
+ const originalFetch = globalThis.fetch;
145
+
146
+ try {
147
+ globalThis.fetch = (async (_input: string | URL | Request) => {
148
+ return new Response(JSON.stringify({ id: 'l2', displayName: 'Home', wellknownListName: 'none' }), {
149
+ status: 200,
150
+ headers: { 'content-type': 'application/json' }
151
+ });
152
+ }) as typeof fetch;
153
+
154
+ const { getTodoList } = await import('./todo-client.js');
155
+ const r = await getTodoList(token, 'l2');
156
+
157
+ expect(r.ok).toBe(true);
158
+ expect(r.data?.displayName).toBe('Home');
159
+ } finally {
160
+ globalThis.fetch = originalFetch;
161
+ }
162
+ });
163
+ });
164
+
165
+ describe('getTasks string filter (paging)', () => {
166
+ it('uses fetchAllPages for string $filter only', async () => {
167
+ process.env.GRAPH_BASE_URL = baseUrl;
168
+ const urls: string[] = [];
169
+ const originalFetch = globalThis.fetch;
170
+
171
+ try {
172
+ globalThis.fetch = (async (input: string | URL | Request) => {
173
+ urls.push(typeof input === 'string' ? input : input.toString());
174
+ return new Response(JSON.stringify({ value: [{ id: 't1', title: 'a', status: 'completed' }] }), {
175
+ status: 200,
176
+ headers: { 'content-type': 'application/json' }
177
+ });
178
+ }) as typeof fetch;
179
+
180
+ const { getTasks } = await import('./todo-client.js');
181
+ const r = await getTasks(token, 'list-1', "status eq 'completed'");
182
+
183
+ expect(r.ok).toBe(true);
184
+ expect(r.data).toHaveLength(1);
185
+ const q = decodeURIComponent(urls[0].replace(/\+/g, ' '));
186
+ expect(q).toContain("$filter=status eq 'completed'");
187
+ } finally {
188
+ globalThis.fetch = originalFetch;
189
+ }
190
+ });
191
+ });
192
+
193
+ describe('createTask', () => {
194
+ it('POSTs payload with optional fields', async () => {
195
+ process.env.GRAPH_BASE_URL = baseUrl;
196
+ const posts: { url: string; body: string }[] = [];
197
+ const originalFetch = globalThis.fetch;
198
+
199
+ try {
200
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
201
+ const url = typeof input === 'string' ? input : input.toString();
202
+ if (init?.method === 'POST' && init.body) {
203
+ posts.push({ url, body: String(init.body) });
204
+ }
205
+ return new Response(JSON.stringify({ id: 'new-id', title: 'Created', status: 'notStarted' }), {
206
+ status: 201,
207
+ headers: { 'content-type': 'application/json' }
208
+ });
209
+ }) as typeof fetch;
210
+
211
+ const { createTask } = await import('./todo-client.js');
212
+ const r = await createTask(token, 'list-x', {
213
+ title: 'Created',
214
+ body: '<p>x</p>',
215
+ bodyContentType: 'html',
216
+ dueDateTime: '2026-06-01T12:00:00',
217
+ startDateTime: '2026-06-01T09:00:00',
218
+ timeZone: 'Europe/Stockholm',
219
+ importance: 'high',
220
+ status: 'notStarted',
221
+ isReminderOn: true,
222
+ reminderDateTime: '2026-06-01T08:00:00',
223
+ linkedResources: [{ displayName: 'Link', webUrl: 'https://example.com' }],
224
+ categories: ['Cat1'],
225
+ recurrence: { pattern: { type: 'daily', interval: 1 } }
226
+ });
227
+
228
+ expect(r.ok).toBe(true);
229
+ expect(r.data?.id).toBe('new-id');
230
+ expect(posts).toHaveLength(1);
231
+ expect(posts[0].url).toContain('/lists/list-x/tasks');
232
+ const payload = JSON.parse(posts[0].body) as Record<string, unknown>;
233
+ expect(payload.title).toBe('Created');
234
+ expect(payload.body).toEqual({ content: '<p>x</p>', contentType: 'html' });
235
+ expect(payload.importance).toBe('high');
236
+ expect(payload.categories).toEqual(['Cat1']);
237
+ expect(Array.isArray(payload.linkedResources)).toBe(true);
238
+ } finally {
239
+ globalThis.fetch = originalFetch;
240
+ }
241
+ });
242
+ });
243
+
244
+ describe('getChecklistItem', () => {
245
+ it('GETs one checklist item by id', async () => {
246
+ process.env.GRAPH_BASE_URL = baseUrl;
247
+ const urls: string[] = [];
248
+ const originalFetch = globalThis.fetch;
249
+
250
+ try {
251
+ globalThis.fetch = (async (input: string | URL | Request) => {
252
+ urls.push(typeof input === 'string' ? input : input.toString());
253
+ return new Response(
254
+ JSON.stringify({
255
+ id: 'ck1',
256
+ displayName: 'Buy milk',
257
+ isChecked: false,
258
+ createdDateTime: '2026-01-01T12:00:00.000Z'
259
+ }),
260
+ { status: 200, headers: { 'content-type': 'application/json' } }
261
+ );
262
+ }) as typeof fetch;
263
+
264
+ const { getChecklistItem } = await import('./todo-client.js');
265
+ const r = await getChecklistItem(token, 'list-1', 'task-1', 'ck1');
266
+
267
+ expect(r.ok).toBe(true);
268
+ expect(r.data?.displayName).toBe('Buy milk');
269
+ expect(urls[0]).toContain('/checklistItems/ck1');
270
+ } finally {
271
+ globalThis.fetch = originalFetch;
272
+ }
273
+ });
274
+ });
275
+
276
+ describe('getTaskAttachmentContent', () => {
277
+ it('GETs raw bytes from attachments/$value', async () => {
278
+ process.env.GRAPH_BASE_URL = baseUrl;
279
+ const originalFetch = globalThis.fetch;
280
+
281
+ try {
282
+ globalThis.fetch = (async (input: string | URL | Request) => {
283
+ const u = typeof input === 'string' ? input : input.toString();
284
+ expect(u).toContain('$value');
285
+ return new Response(new Uint8Array([7, 8, 9]), { status: 200 });
286
+ }) as typeof fetch;
287
+
288
+ const { getTaskAttachmentContent } = await import('./todo-client.js');
289
+ const r = await getTaskAttachmentContent(token, 'list-1', 'task-1', 'att-1');
290
+
291
+ expect(r.ok).toBe(true);
292
+ expect(r.data?.length).toBe(3);
293
+ expect(r.data?.[0]).toBe(7);
294
+ } finally {
295
+ globalThis.fetch = originalFetch;
296
+ }
297
+ });
298
+ });