@openclaw/msteams 2026.3.13 → 2026.5.1-beta.1
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/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
package/src/attachments.test.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import type { PluginRuntime, SsrFPolicy } from "openclaw/plugin-sdk/msteams";
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
buildMSTeamsMediaPayload,
|
|
8
|
-
downloadMSTeamsAttachments,
|
|
9
|
-
downloadMSTeamsGraphMedia,
|
|
10
|
-
} from "./attachments.js";
|
|
2
|
+
import type { PluginRuntime, SsrFPolicy } from "../runtime-api.js";
|
|
3
|
+
import { readRemoteMediaResponse } from "./attachments.test-helpers.js";
|
|
4
|
+
import { downloadMSTeamsAttachments } from "./attachments/download.js";
|
|
5
|
+
import { resolveRequestUrl } from "./attachments/shared.js";
|
|
11
6
|
import { setMSTeamsRuntime } from "./runtime.js";
|
|
12
7
|
|
|
13
8
|
const GRAPH_HOST = "graph.microsoft.com";
|
|
14
|
-
const
|
|
9
|
+
const _SHAREPOINT_HOST = "contoso.sharepoint.com";
|
|
15
10
|
const AZUREEDGE_HOST = "azureedge.net";
|
|
16
11
|
const TEST_HOST = "x";
|
|
17
12
|
const createUrlForHost = (host: string, pathSegment: string) => `https://${host}/${pathSegment}`;
|
|
@@ -19,14 +14,14 @@ const createTestUrl = (pathSegment: string) => createUrlForHost(TEST_HOST, pathS
|
|
|
19
14
|
const SAVED_PNG_PATH = "/tmp/saved.png";
|
|
20
15
|
const SAVED_PDF_PATH = "/tmp/saved.pdf";
|
|
21
16
|
const TEST_URL_IMAGE = createTestUrl("img");
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
17
|
+
const _TEST_URL_IMAGE_PNG = createTestUrl("img.png");
|
|
18
|
+
const _TEST_URL_IMAGE_1_PNG = createTestUrl("1.png");
|
|
19
|
+
const _TEST_URL_IMAGE_2_JPG = createTestUrl("2.jpg");
|
|
20
|
+
const _TEST_URL_PDF = createTestUrl("x.pdf");
|
|
21
|
+
const _TEST_URL_PDF_1 = createTestUrl("1.pdf");
|
|
22
|
+
const _TEST_URL_PDF_2 = createTestUrl("2.pdf");
|
|
23
|
+
const _TEST_URL_HTML_A = createTestUrl("a.png");
|
|
24
|
+
const _TEST_URL_HTML_B = createTestUrl("b.png");
|
|
30
25
|
const TEST_URL_INLINE_IMAGE = createTestUrl("inline.png");
|
|
31
26
|
const TEST_URL_DOC_PDF = createTestUrl("doc.pdf");
|
|
32
27
|
const TEST_URL_FILE_DOWNLOAD = createTestUrl("dl");
|
|
@@ -35,7 +30,7 @@ const CONTENT_TYPE_IMAGE_PNG = "image/png";
|
|
|
35
30
|
const CONTENT_TYPE_APPLICATION_PDF = "application/pdf";
|
|
36
31
|
const CONTENT_TYPE_TEXT_HTML = "text/html";
|
|
37
32
|
const CONTENT_TYPE_TEAMS_FILE_DOWNLOAD_INFO = "application/vnd.microsoft.teams.file.download.info";
|
|
38
|
-
const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308];
|
|
33
|
+
const REDIRECT_STATUS_CODES = new Set([301, 302, 303, 307, 308]);
|
|
39
34
|
const MAX_REDIRECT_HOPS = 5;
|
|
40
35
|
type RemoteMediaFetchParams = {
|
|
41
36
|
url: string;
|
|
@@ -52,24 +47,6 @@ const saveMediaBufferMock = vi.fn(async () => ({
|
|
|
52
47
|
size: Buffer.byteLength(PNG_BUFFER),
|
|
53
48
|
contentType: CONTENT_TYPE_IMAGE_PNG,
|
|
54
49
|
}));
|
|
55
|
-
const readRemoteMediaResponse = async (
|
|
56
|
-
res: Response,
|
|
57
|
-
params: Pick<RemoteMediaFetchParams, "maxBytes" | "filePathHint">,
|
|
58
|
-
) => {
|
|
59
|
-
if (!res.ok) {
|
|
60
|
-
throw new Error(`HTTP ${res.status}`);
|
|
61
|
-
}
|
|
62
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
63
|
-
if (typeof params.maxBytes === "number" && buffer.byteLength > params.maxBytes) {
|
|
64
|
-
throw new Error(`payload exceeds maxBytes ${params.maxBytes}`);
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
buffer,
|
|
68
|
-
contentType: res.headers.get("content-type") ?? undefined,
|
|
69
|
-
fileName: params.filePathHint,
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
|
|
73
50
|
function isHostnameAllowedByPattern(hostname: string, pattern: string): boolean {
|
|
74
51
|
if (pattern.startsWith("*.")) {
|
|
75
52
|
const suffix = pattern.slice(2);
|
|
@@ -99,7 +76,7 @@ async function fetchRemoteMediaWithRedirects(
|
|
|
99
76
|
throw new Error(`Blocked hostname (not in allowlist): ${currentUrl}`);
|
|
100
77
|
}
|
|
101
78
|
const res = await fetchFn(currentUrl, { redirect: "manual", ...requestInit });
|
|
102
|
-
if (REDIRECT_STATUS_CODES.
|
|
79
|
+
if (REDIRECT_STATUS_CODES.has(res.status)) {
|
|
103
80
|
const location = res.headers.get("location");
|
|
104
81
|
if (!location) {
|
|
105
82
|
throw new Error("redirect missing location");
|
|
@@ -116,7 +93,7 @@ const fetchRemoteMediaMock = vi.fn(async (params: RemoteMediaFetchParams) => {
|
|
|
116
93
|
return await fetchRemoteMediaWithRedirects(params);
|
|
117
94
|
});
|
|
118
95
|
|
|
119
|
-
const runtimeStub
|
|
96
|
+
const runtimeStub = {
|
|
120
97
|
media: {
|
|
121
98
|
detectMime: detectMimeMock,
|
|
122
99
|
},
|
|
@@ -126,12 +103,10 @@ const runtimeStub: PluginRuntime = createPluginRuntimeMock({
|
|
|
126
103
|
saveMediaBuffer: saveMediaBufferMock,
|
|
127
104
|
},
|
|
128
105
|
},
|
|
129
|
-
}
|
|
106
|
+
} as unknown as PluginRuntime;
|
|
130
107
|
|
|
131
108
|
type DownloadAttachmentsParams = Parameters<typeof downloadMSTeamsAttachments>[0];
|
|
132
|
-
type DownloadGraphMediaParams = Parameters<typeof downloadMSTeamsGraphMedia>[0];
|
|
133
109
|
type DownloadedMedia = Awaited<ReturnType<typeof downloadMSTeamsAttachments>>;
|
|
134
|
-
type MSTeamsMediaPayload = ReturnType<typeof buildMSTeamsMediaPayload>;
|
|
135
110
|
type DownloadAttachmentsBuildOverrides = Partial<
|
|
136
111
|
Omit<DownloadAttachmentsParams, "attachments" | "maxBytes" | "allowHosts">
|
|
137
112
|
> &
|
|
@@ -140,31 +115,17 @@ type DownloadAttachmentsNoFetchOverrides = Partial<
|
|
|
140
115
|
Omit<DownloadAttachmentsParams, "attachments" | "maxBytes" | "allowHosts" | "fetchFn">
|
|
141
116
|
> &
|
|
142
117
|
Pick<DownloadAttachmentsParams, "allowHosts">;
|
|
143
|
-
type DownloadGraphMediaOverrides = Partial<
|
|
144
|
-
Omit<DownloadGraphMediaParams, "messageUrl" | "tokenProvider" | "maxBytes">
|
|
145
|
-
>;
|
|
146
118
|
type FetchFn = typeof fetch;
|
|
147
119
|
type MSTeamsAttachments = DownloadAttachmentsParams["attachments"];
|
|
148
|
-
type AttachmentPlaceholderInput = Parameters<typeof buildMSTeamsAttachmentPlaceholder>[0];
|
|
149
|
-
type GraphMessageUrlParams = Parameters<typeof buildMSTeamsGraphMessageUrls>[0];
|
|
150
120
|
type LabeledCase = { label: string };
|
|
151
121
|
type FetchCallExpectation = { expectFetchCalled?: boolean };
|
|
152
122
|
type DownloadedMediaExpectation = { path?: string; placeholder?: string };
|
|
153
|
-
type MSTeamsMediaPayloadExpectation = {
|
|
154
|
-
firstPath: string;
|
|
155
|
-
paths: string[];
|
|
156
|
-
types: string[];
|
|
157
|
-
};
|
|
158
123
|
|
|
159
|
-
const DEFAULT_MESSAGE_URL = `https://${GRAPH_HOST}/v1.0/chats/19%3Achat/messages/123`;
|
|
160
|
-
const GRAPH_SHARES_URL_PREFIX = `https://${GRAPH_HOST}/v1.0/shares/`;
|
|
161
124
|
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
162
125
|
const DEFAULT_ALLOW_HOSTS = [TEST_HOST];
|
|
163
|
-
const DEFAULT_SHAREPOINT_ALLOW_HOSTS = [GRAPH_HOST, SHAREPOINT_HOST];
|
|
164
|
-
const DEFAULT_SHARE_REFERENCE_URL = createUrlForHost(SHAREPOINT_HOST, "site/file");
|
|
165
126
|
const MEDIA_PLACEHOLDER_IMAGE = "<media:image>";
|
|
166
127
|
const MEDIA_PLACEHOLDER_DOCUMENT = "<media:document>";
|
|
167
|
-
const
|
|
128
|
+
const _formatImagePlaceholder = (count: number) =>
|
|
168
129
|
count > 1 ? `${MEDIA_PLACEHOLDER_IMAGE} (${count} images)` : MEDIA_PLACEHOLDER_IMAGE;
|
|
169
130
|
const formatDocumentPlaceholder = (count: number) =>
|
|
170
131
|
count > 1 ? `${MEDIA_PLACEHOLDER_DOCUMENT} (${count} files)` : MEDIA_PLACEHOLDER_DOCUMENT;
|
|
@@ -208,19 +169,16 @@ const createTeamsFileDownloadInfoAttachments = (
|
|
|
208
169
|
content: { downloadUrl, fileType },
|
|
209
170
|
}),
|
|
210
171
|
);
|
|
211
|
-
const createMediaEntriesWithType = (contentType: string, ...paths: string[]) =>
|
|
212
|
-
paths.map((path) => ({ path, contentType }));
|
|
213
172
|
const createHostedContentsWithType = (contentType: string, ...ids: string[]) =>
|
|
214
173
|
ids.map((id) => ({ id, contentType, contentBytes: PNG_BASE64 }));
|
|
215
|
-
const
|
|
216
|
-
createMediaEntriesWithType(CONTENT_TYPE_IMAGE_PNG, ...paths);
|
|
217
|
-
const createHostedImageContents = (...ids: string[]) =>
|
|
174
|
+
const _createHostedImageContents = (...ids: string[]) =>
|
|
218
175
|
createHostedContentsWithType(CONTENT_TYPE_IMAGE_PNG, ...ids);
|
|
219
|
-
|
|
176
|
+
type BinaryPayload = Uint8Array | string;
|
|
177
|
+
const _createPdfResponse = (payload: BinaryPayload = PDF_BUFFER) => {
|
|
220
178
|
return createBufferResponse(payload, CONTENT_TYPE_APPLICATION_PDF);
|
|
221
179
|
};
|
|
222
|
-
const createBufferResponse = (payload:
|
|
223
|
-
const raw =
|
|
180
|
+
const createBufferResponse = (payload: BinaryPayload, contentType: string, status = 200) => {
|
|
181
|
+
const raw = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
224
182
|
return new Response(new Uint8Array(raw), {
|
|
225
183
|
status,
|
|
226
184
|
headers: { "content-type": contentType },
|
|
@@ -229,13 +187,16 @@ const createBufferResponse = (payload: Buffer | string, contentType: string, sta
|
|
|
229
187
|
const createJsonResponse = (payload: unknown, status = 200) =>
|
|
230
188
|
new Response(JSON.stringify(payload), { status });
|
|
231
189
|
const createTextResponse = (body: string, status = 200) => new Response(body, { status });
|
|
232
|
-
const
|
|
190
|
+
const _createGraphCollectionResponse = (value: unknown[]) => createJsonResponse({ value });
|
|
233
191
|
const createNotFoundResponse = () => new Response("not found", { status: 404 });
|
|
234
192
|
const createRedirectResponse = (location: string, status = 302) =>
|
|
235
193
|
new Response(null, { status, headers: { location } });
|
|
194
|
+
const publicResolve = async () => ({ address: "13.107.136.10" });
|
|
236
195
|
|
|
237
196
|
const createOkFetchMock = (contentType: string, payload = "png") =>
|
|
238
|
-
vi.fn(async (
|
|
197
|
+
vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
198
|
+
createBufferResponse(payload, contentType),
|
|
199
|
+
);
|
|
239
200
|
const asFetchFn = (fetchFn: unknown): FetchFn => fetchFn as FetchFn;
|
|
240
201
|
|
|
241
202
|
const buildDownloadParams = (
|
|
@@ -246,6 +207,7 @@ const buildDownloadParams = (
|
|
|
246
207
|
attachments,
|
|
247
208
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
248
209
|
allowHosts: DEFAULT_ALLOW_HOSTS,
|
|
210
|
+
resolveFn: publicResolve,
|
|
249
211
|
...overrides,
|
|
250
212
|
};
|
|
251
213
|
};
|
|
@@ -283,25 +245,6 @@ const expectMockCallState = (mockFn: unknown, shouldCall: boolean) => {
|
|
|
283
245
|
}
|
|
284
246
|
};
|
|
285
247
|
|
|
286
|
-
const DEFAULT_CHANNEL_TEAM_ID = "team-id";
|
|
287
|
-
const DEFAULT_CHANNEL_ID = "chan-id";
|
|
288
|
-
const createChannelGraphMessageUrlParams = (params: {
|
|
289
|
-
messageId: string;
|
|
290
|
-
replyToId?: string;
|
|
291
|
-
conversationId?: string;
|
|
292
|
-
}) => ({
|
|
293
|
-
conversationType: "channel" as const,
|
|
294
|
-
...params,
|
|
295
|
-
channelData: {
|
|
296
|
-
team: { id: DEFAULT_CHANNEL_TEAM_ID },
|
|
297
|
-
channel: { id: DEFAULT_CHANNEL_ID },
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
const buildExpectedChannelMessagePath = (params: { messageId: string; replyToId?: string }) =>
|
|
301
|
-
params.replyToId
|
|
302
|
-
? `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.replyToId}/replies/${params.messageId}`
|
|
303
|
-
: `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.messageId}`;
|
|
304
|
-
|
|
305
248
|
const expectAttachmentMediaLength = (media: DownloadedMedia, expectedLength: number) => {
|
|
306
249
|
expect(media).toHaveLength(expectedLength);
|
|
307
250
|
};
|
|
@@ -321,25 +264,6 @@ const expectFirstMedia = (media: DownloadedMedia, expected: DownloadedMediaExpec
|
|
|
321
264
|
expect(first?.placeholder).toBe(expected.placeholder);
|
|
322
265
|
}
|
|
323
266
|
};
|
|
324
|
-
const expectMSTeamsMediaPayload = (
|
|
325
|
-
payload: MSTeamsMediaPayload,
|
|
326
|
-
expected: MSTeamsMediaPayloadExpectation,
|
|
327
|
-
) => {
|
|
328
|
-
expect(payload.MediaPath).toBe(expected.firstPath);
|
|
329
|
-
expect(payload.MediaUrl).toBe(expected.firstPath);
|
|
330
|
-
expect(payload.MediaPaths).toEqual(expected.paths);
|
|
331
|
-
expect(payload.MediaUrls).toEqual(expected.paths);
|
|
332
|
-
expect(payload.MediaTypes).toEqual(expected.types);
|
|
333
|
-
};
|
|
334
|
-
type AttachmentPlaceholderCase = LabeledCase & {
|
|
335
|
-
attachments: AttachmentPlaceholderInput;
|
|
336
|
-
expected: string;
|
|
337
|
-
};
|
|
338
|
-
type CountedAttachmentPlaceholderCaseDef = LabeledCase & {
|
|
339
|
-
attachments: AttachmentPlaceholderCase["attachments"];
|
|
340
|
-
count: number;
|
|
341
|
-
formatPlaceholder: (count: number) => string;
|
|
342
|
-
};
|
|
343
267
|
type AttachmentDownloadSuccessCase = LabeledCase & {
|
|
344
268
|
attachments: MSTeamsAttachments;
|
|
345
269
|
buildFetchFn?: () => unknown;
|
|
@@ -357,77 +281,6 @@ type AttachmentAuthRetryCase = LabeledCase & {
|
|
|
357
281
|
expectedMediaLength: number;
|
|
358
282
|
expectTokenFetch: boolean;
|
|
359
283
|
};
|
|
360
|
-
type GraphUrlExpectationCase = LabeledCase & {
|
|
361
|
-
params: GraphMessageUrlParams;
|
|
362
|
-
expectedPath: string;
|
|
363
|
-
};
|
|
364
|
-
type ChannelGraphUrlCaseParams = {
|
|
365
|
-
messageId: string;
|
|
366
|
-
replyToId?: string;
|
|
367
|
-
conversationId?: string;
|
|
368
|
-
};
|
|
369
|
-
type GraphMediaDownloadResult = {
|
|
370
|
-
fetchMock: ReturnType<typeof createGraphFetchMock>;
|
|
371
|
-
media: Awaited<ReturnType<typeof downloadMSTeamsGraphMedia>>;
|
|
372
|
-
};
|
|
373
|
-
type GraphMediaSuccessCase = LabeledCase & {
|
|
374
|
-
buildOptions: () => GraphFetchMockOptions;
|
|
375
|
-
expectedLength: number;
|
|
376
|
-
assert?: (params: GraphMediaDownloadResult) => void;
|
|
377
|
-
};
|
|
378
|
-
const EMPTY_ATTACHMENT_PLACEHOLDER_CASES: AttachmentPlaceholderCase[] = [
|
|
379
|
-
withLabel("returns empty string when no attachments", { attachments: undefined, expected: "" }),
|
|
380
|
-
withLabel("returns empty string when attachments are empty", { attachments: [], expected: "" }),
|
|
381
|
-
];
|
|
382
|
-
const COUNTED_ATTACHMENT_PLACEHOLDER_CASE_DEFS: CountedAttachmentPlaceholderCaseDef[] = [
|
|
383
|
-
withLabel("returns image placeholder for one image attachment", {
|
|
384
|
-
attachments: createImageAttachments(TEST_URL_IMAGE_PNG),
|
|
385
|
-
count: 1,
|
|
386
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
387
|
-
}),
|
|
388
|
-
withLabel("returns image placeholder with count for many image attachments", {
|
|
389
|
-
attachments: [
|
|
390
|
-
...createImageAttachments(TEST_URL_IMAGE_1_PNG),
|
|
391
|
-
{ contentType: "image/jpeg", contentUrl: TEST_URL_IMAGE_2_JPG },
|
|
392
|
-
],
|
|
393
|
-
count: 2,
|
|
394
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
395
|
-
}),
|
|
396
|
-
withLabel("treats Teams file.download.info image attachments as images", {
|
|
397
|
-
attachments: createTeamsFileDownloadInfoAttachments(),
|
|
398
|
-
count: 1,
|
|
399
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
400
|
-
}),
|
|
401
|
-
withLabel("returns document placeholder for non-image attachments", {
|
|
402
|
-
attachments: createPdfAttachments(TEST_URL_PDF),
|
|
403
|
-
count: 1,
|
|
404
|
-
formatPlaceholder: formatDocumentPlaceholder,
|
|
405
|
-
}),
|
|
406
|
-
withLabel("returns document placeholder with count for many non-image attachments", {
|
|
407
|
-
attachments: createPdfAttachments(TEST_URL_PDF_1, TEST_URL_PDF_2),
|
|
408
|
-
count: 2,
|
|
409
|
-
formatPlaceholder: formatDocumentPlaceholder,
|
|
410
|
-
}),
|
|
411
|
-
withLabel("counts one inline image in html attachments", {
|
|
412
|
-
attachments: createHtmlImageAttachments([TEST_URL_HTML_A], "<p>hi</p>"),
|
|
413
|
-
count: 1,
|
|
414
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
415
|
-
}),
|
|
416
|
-
withLabel("counts many inline images in html attachments", {
|
|
417
|
-
attachments: createHtmlImageAttachments([TEST_URL_HTML_A, TEST_URL_HTML_B]),
|
|
418
|
-
count: 2,
|
|
419
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
420
|
-
}),
|
|
421
|
-
];
|
|
422
|
-
const ATTACHMENT_PLACEHOLDER_CASES: AttachmentPlaceholderCase[] = [
|
|
423
|
-
...EMPTY_ATTACHMENT_PLACEHOLDER_CASES,
|
|
424
|
-
...COUNTED_ATTACHMENT_PLACEHOLDER_CASE_DEFS.map((testCase) =>
|
|
425
|
-
withLabel(testCase.label, {
|
|
426
|
-
attachments: testCase.attachments,
|
|
427
|
-
expected: testCase.formatPlaceholder(testCase.count),
|
|
428
|
-
}),
|
|
429
|
-
),
|
|
430
|
-
];
|
|
431
284
|
const ATTACHMENT_DOWNLOAD_SUCCESS_CASES: AttachmentDownloadSuccessCase[] = [
|
|
432
285
|
withLabel("downloads and stores image contentUrl attachments", {
|
|
433
286
|
attachments: asSingleItemArray(IMAGE_ATTACHMENT),
|
|
@@ -487,150 +340,6 @@ const ATTACHMENT_AUTH_RETRY_CASES: AttachmentAuthRetryCase[] = [
|
|
|
487
340
|
expectTokenFetch: false,
|
|
488
341
|
}),
|
|
489
342
|
];
|
|
490
|
-
const GRAPH_MEDIA_SUCCESS_CASES: GraphMediaSuccessCase[] = [
|
|
491
|
-
withLabel("downloads hostedContents images", {
|
|
492
|
-
buildOptions: () => ({ hostedContents: createHostedImageContents("1") }),
|
|
493
|
-
expectedLength: 1,
|
|
494
|
-
assert: ({ fetchMock }) => {
|
|
495
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
496
|
-
expectMediaBufferSaved();
|
|
497
|
-
},
|
|
498
|
-
}),
|
|
499
|
-
withLabel("merges SharePoint reference attachments with hosted content", {
|
|
500
|
-
buildOptions: () => {
|
|
501
|
-
return {
|
|
502
|
-
hostedContents: createHostedImageContents("hosted-1"),
|
|
503
|
-
...buildDefaultShareReferenceGraphFetchOptions({
|
|
504
|
-
onShareRequest: () => createPdfResponse(),
|
|
505
|
-
}),
|
|
506
|
-
};
|
|
507
|
-
},
|
|
508
|
-
expectedLength: 2,
|
|
509
|
-
}),
|
|
510
|
-
];
|
|
511
|
-
const CHANNEL_GRAPH_URL_CASES: Array<LabeledCase & ChannelGraphUrlCaseParams> = [
|
|
512
|
-
withLabel("builds channel message urls", {
|
|
513
|
-
conversationId: "19:thread@thread.tacv2",
|
|
514
|
-
messageId: "123",
|
|
515
|
-
}),
|
|
516
|
-
withLabel("builds channel reply urls when replyToId is present", {
|
|
517
|
-
messageId: "reply-id",
|
|
518
|
-
replyToId: "root-id",
|
|
519
|
-
}),
|
|
520
|
-
];
|
|
521
|
-
const GRAPH_URL_EXPECTATION_CASES: GraphUrlExpectationCase[] = [
|
|
522
|
-
...CHANNEL_GRAPH_URL_CASES.map<GraphUrlExpectationCase>(({ label, ...params }) =>
|
|
523
|
-
withLabel(label, {
|
|
524
|
-
params: createChannelGraphMessageUrlParams(params),
|
|
525
|
-
expectedPath: buildExpectedChannelMessagePath(params),
|
|
526
|
-
}),
|
|
527
|
-
),
|
|
528
|
-
withLabel("builds chat message urls", {
|
|
529
|
-
params: {
|
|
530
|
-
conversationType: "groupChat" as const,
|
|
531
|
-
conversationId: "19:chat@thread.v2",
|
|
532
|
-
messageId: "456",
|
|
533
|
-
},
|
|
534
|
-
expectedPath: "/chats/19%3Achat%40thread.v2/messages/456",
|
|
535
|
-
}),
|
|
536
|
-
];
|
|
537
|
-
|
|
538
|
-
type GraphFetchMockOptions = {
|
|
539
|
-
hostedContents?: unknown[];
|
|
540
|
-
attachments?: unknown[];
|
|
541
|
-
messageAttachments?: unknown[];
|
|
542
|
-
onShareRequest?: (url: string) => Response | Promise<Response>;
|
|
543
|
-
onUnhandled?: (url: string) => Response | Promise<Response> | undefined;
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const createReferenceAttachment = (shareUrl = DEFAULT_SHARE_REFERENCE_URL) => ({
|
|
547
|
-
id: "ref-1",
|
|
548
|
-
contentType: "reference",
|
|
549
|
-
contentUrl: shareUrl,
|
|
550
|
-
name: "report.pdf",
|
|
551
|
-
});
|
|
552
|
-
const buildShareReferenceGraphFetchOptions = (params: {
|
|
553
|
-
referenceAttachment: ReturnType<typeof createReferenceAttachment>;
|
|
554
|
-
onShareRequest?: GraphFetchMockOptions["onShareRequest"];
|
|
555
|
-
onUnhandled?: GraphFetchMockOptions["onUnhandled"];
|
|
556
|
-
}) => ({
|
|
557
|
-
attachments: [params.referenceAttachment],
|
|
558
|
-
messageAttachments: [params.referenceAttachment],
|
|
559
|
-
...(params.onShareRequest ? { onShareRequest: params.onShareRequest } : {}),
|
|
560
|
-
...(params.onUnhandled ? { onUnhandled: params.onUnhandled } : {}),
|
|
561
|
-
});
|
|
562
|
-
const buildDefaultShareReferenceGraphFetchOptions = (
|
|
563
|
-
params: Omit<Parameters<typeof buildShareReferenceGraphFetchOptions>[0], "referenceAttachment">,
|
|
564
|
-
) =>
|
|
565
|
-
buildShareReferenceGraphFetchOptions({
|
|
566
|
-
referenceAttachment: createReferenceAttachment(),
|
|
567
|
-
...params,
|
|
568
|
-
});
|
|
569
|
-
type GraphEndpointResponseHandler = {
|
|
570
|
-
suffix: string;
|
|
571
|
-
buildResponse: () => Response;
|
|
572
|
-
};
|
|
573
|
-
const createGraphEndpointResponseHandlers = (params: {
|
|
574
|
-
hostedContents: unknown[];
|
|
575
|
-
attachments: unknown[];
|
|
576
|
-
messageAttachments: unknown[];
|
|
577
|
-
}): GraphEndpointResponseHandler[] => [
|
|
578
|
-
{
|
|
579
|
-
suffix: "/hostedContents",
|
|
580
|
-
buildResponse: () => createGraphCollectionResponse(params.hostedContents),
|
|
581
|
-
},
|
|
582
|
-
{
|
|
583
|
-
suffix: "/attachments",
|
|
584
|
-
buildResponse: () => createGraphCollectionResponse(params.attachments),
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
suffix: "/messages/123",
|
|
588
|
-
buildResponse: () => createJsonResponse({ attachments: params.messageAttachments }),
|
|
589
|
-
},
|
|
590
|
-
];
|
|
591
|
-
const resolveGraphEndpointResponse = (
|
|
592
|
-
url: string,
|
|
593
|
-
handlers: GraphEndpointResponseHandler[],
|
|
594
|
-
): Response | undefined => {
|
|
595
|
-
const handler = handlers.find((entry) => url.endsWith(entry.suffix));
|
|
596
|
-
return handler ? handler.buildResponse() : undefined;
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const createGraphFetchMock = (options: GraphFetchMockOptions = {}) => {
|
|
600
|
-
const hostedContents = options.hostedContents ?? [];
|
|
601
|
-
const attachments = options.attachments ?? [];
|
|
602
|
-
const messageAttachments = options.messageAttachments ?? [];
|
|
603
|
-
const endpointHandlers = createGraphEndpointResponseHandlers({
|
|
604
|
-
hostedContents,
|
|
605
|
-
attachments,
|
|
606
|
-
messageAttachments,
|
|
607
|
-
});
|
|
608
|
-
return vi.fn(async (url: string) => {
|
|
609
|
-
const endpointResponse = resolveGraphEndpointResponse(url, endpointHandlers);
|
|
610
|
-
if (endpointResponse) {
|
|
611
|
-
return endpointResponse;
|
|
612
|
-
}
|
|
613
|
-
if (url.startsWith(GRAPH_SHARES_URL_PREFIX) && options.onShareRequest) {
|
|
614
|
-
return options.onShareRequest(url);
|
|
615
|
-
}
|
|
616
|
-
const unhandled = options.onUnhandled ? await options.onUnhandled(url) : undefined;
|
|
617
|
-
return unhandled ?? createNotFoundResponse();
|
|
618
|
-
});
|
|
619
|
-
};
|
|
620
|
-
const downloadGraphMediaWithMockOptions = async (
|
|
621
|
-
options: GraphFetchMockOptions = {},
|
|
622
|
-
overrides: DownloadGraphMediaOverrides = {},
|
|
623
|
-
): Promise<GraphMediaDownloadResult> => {
|
|
624
|
-
const fetchMock = createGraphFetchMock(options);
|
|
625
|
-
const media = await downloadMSTeamsGraphMedia({
|
|
626
|
-
messageUrl: DEFAULT_MESSAGE_URL,
|
|
627
|
-
tokenProvider: createTokenProvider(),
|
|
628
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
629
|
-
fetchFn: asFetchFn(fetchMock),
|
|
630
|
-
...overrides,
|
|
631
|
-
});
|
|
632
|
-
return { fetchMock, media };
|
|
633
|
-
};
|
|
634
343
|
const runAttachmentDownloadSuccessCase = async ({
|
|
635
344
|
attachments,
|
|
636
345
|
buildFetchFn,
|
|
@@ -661,15 +370,6 @@ const runAttachmentAuthRetryCase = async ({
|
|
|
661
370
|
expectAttachmentMediaLength(media, expectedMediaLength);
|
|
662
371
|
expectMockCallState(tokenProvider.getAccessToken, expectTokenFetch);
|
|
663
372
|
};
|
|
664
|
-
const runGraphMediaSuccessCase = async ({
|
|
665
|
-
buildOptions,
|
|
666
|
-
expectedLength,
|
|
667
|
-
assert,
|
|
668
|
-
}: GraphMediaSuccessCase) => {
|
|
669
|
-
const { fetchMock, media } = await downloadGraphMediaWithMockOptions(buildOptions());
|
|
670
|
-
expectAttachmentMediaLength(media.media, expectedLength);
|
|
671
|
-
assert?.({ fetchMock, media });
|
|
672
|
-
};
|
|
673
373
|
|
|
674
374
|
describe("msteams attachments", () => {
|
|
675
375
|
beforeEach(() => {
|
|
@@ -679,15 +379,6 @@ describe("msteams attachments", () => {
|
|
|
679
379
|
setMSTeamsRuntime(runtimeStub);
|
|
680
380
|
});
|
|
681
381
|
|
|
682
|
-
describe("buildMSTeamsAttachmentPlaceholder", () => {
|
|
683
|
-
it.each<AttachmentPlaceholderCase>(ATTACHMENT_PLACEHOLDER_CASES)(
|
|
684
|
-
"$label",
|
|
685
|
-
({ attachments, expected }) => {
|
|
686
|
-
expect(buildMSTeamsAttachmentPlaceholder(attachments)).toBe(expected);
|
|
687
|
-
},
|
|
688
|
-
);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
382
|
describe("downloadMSTeamsAttachments", () => {
|
|
692
383
|
it.each<AttachmentDownloadSuccessCase>(ATTACHMENT_DOWNLOAD_SUCCESS_CASES)(
|
|
693
384
|
"$label",
|
|
@@ -740,7 +431,7 @@ describe("msteams attachments", () => {
|
|
|
740
431
|
|
|
741
432
|
expectAttachmentMediaLength(media, 1);
|
|
742
433
|
expect(tokenProvider.getAccessToken).toHaveBeenCalledOnce();
|
|
743
|
-
expect(fetchMock.mock.calls.map(([calledUrl]) =>
|
|
434
|
+
expect(fetchMock.mock.calls.map(([calledUrl]) => calledUrl)).toContain(redirectedUrl);
|
|
744
435
|
});
|
|
745
436
|
|
|
746
437
|
it("continues scope fallback after non-auth failure and succeeds on later scope", async () => {
|
|
@@ -827,7 +518,7 @@ describe("msteams attachments", () => {
|
|
|
827
518
|
it("blocks redirects to non-https URLs", async () => {
|
|
828
519
|
const insecureUrl = "http://x/insecure.png";
|
|
829
520
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
830
|
-
const url =
|
|
521
|
+
const url = resolveRequestUrl(input);
|
|
831
522
|
if (url === TEST_URL_IMAGE) {
|
|
832
523
|
return createRedirectResponse(insecureUrl);
|
|
833
524
|
}
|
|
@@ -848,94 +539,148 @@ describe("msteams attachments", () => {
|
|
|
848
539
|
expectAttachmentMediaLength(media, 0);
|
|
849
540
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
850
541
|
});
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
describe("buildMSTeamsGraphMessageUrls", () => {
|
|
854
|
-
it.each(GRAPH_URL_EXPECTATION_CASES)("$label", ({ params, expectedPath }) => {
|
|
855
|
-
const urls = buildMSTeamsGraphMessageUrls(params);
|
|
856
|
-
expect(urls[0]).toContain(expectedPath);
|
|
857
|
-
});
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
describe("downloadMSTeamsGraphMedia", () => {
|
|
861
|
-
it.each<GraphMediaSuccessCase>(GRAPH_MEDIA_SUCCESS_CASES)("$label", runGraphMediaSuccessCase);
|
|
862
|
-
|
|
863
|
-
it("does not forward Authorization for SharePoint redirects outside auth allowlist", async () => {
|
|
864
|
-
const tokenProvider = createTokenProvider("top-secret-token");
|
|
865
|
-
const escapedUrl = "https://example.com/collect";
|
|
866
|
-
const seen: Array<{ url: string; auth: string }> = [];
|
|
867
|
-
const referenceAttachment = createReferenceAttachment();
|
|
868
|
-
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
869
|
-
const url = String(input);
|
|
870
|
-
const auth = new Headers(init?.headers).get("Authorization") ?? "";
|
|
871
|
-
seen.push({ url, auth });
|
|
872
542
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
543
|
+
describe("OneDrive/SharePoint shared links", () => {
|
|
544
|
+
const GRAPH_SHARES_URL_PREFIX = `https://${GRAPH_HOST}/v1.0/shares/`;
|
|
545
|
+
const DEFAULT_GRAPH_ALLOW_HOSTS = [GRAPH_HOST];
|
|
546
|
+
const PDF_PAYLOAD = Buffer.from("pdf-bytes");
|
|
547
|
+
|
|
548
|
+
const createGraphSharesFetchMock = () =>
|
|
549
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
550
|
+
const url = resolveRequestUrl(input);
|
|
551
|
+
const auth = new Headers(init?.headers).get("Authorization");
|
|
552
|
+
if (url.startsWith(GRAPH_SHARES_URL_PREFIX)) {
|
|
553
|
+
if (!auth) {
|
|
554
|
+
return createTextResponse("unauthorized", 401);
|
|
555
|
+
}
|
|
556
|
+
return createBufferResponse(PDF_PAYLOAD, CONTENT_TYPE_APPLICATION_PDF);
|
|
557
|
+
}
|
|
558
|
+
return createNotFoundResponse();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it.each([
|
|
562
|
+
{
|
|
563
|
+
label: "SharePoint URL",
|
|
564
|
+
contentUrl: "https://contoso.sharepoint.com/personal/user/Documents/report.pdf",
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
label: "OneDrive 1drv.ms URL",
|
|
568
|
+
contentUrl: "https://1drv.ms/b/s!AkxYabcdefg",
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
label: "OneDrive onedrive.live.com URL",
|
|
572
|
+
contentUrl: "https://onedrive.live.com/share/file",
|
|
573
|
+
},
|
|
574
|
+
])("routes $label through Graph shares endpoint", async ({ contentUrl }) => {
|
|
575
|
+
const tokenProvider = createTokenProvider();
|
|
576
|
+
const fetchMock = createGraphSharesFetchMock();
|
|
577
|
+
detectMimeMock.mockResolvedValueOnce(CONTENT_TYPE_APPLICATION_PDF);
|
|
578
|
+
saveMediaBufferMock.mockResolvedValueOnce({
|
|
579
|
+
id: "saved.pdf",
|
|
580
|
+
path: SAVED_PDF_PATH,
|
|
581
|
+
size: Buffer.byteLength(PDF_PAYLOAD),
|
|
582
|
+
contentType: CONTENT_TYPE_APPLICATION_PDF,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const media = await downloadMSTeamsAttachments(
|
|
586
|
+
buildDownloadParams(
|
|
587
|
+
[
|
|
588
|
+
{
|
|
589
|
+
contentType: "reference",
|
|
590
|
+
contentUrl,
|
|
591
|
+
name: "report.pdf",
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
{
|
|
595
|
+
tokenProvider,
|
|
596
|
+
allowHosts: DEFAULT_GRAPH_ALLOW_HOSTS,
|
|
597
|
+
authAllowHosts: DEFAULT_GRAPH_ALLOW_HOSTS,
|
|
598
|
+
fetchFn: asFetchFn(fetchMock),
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
expectAttachmentMediaLength(media, 1);
|
|
604
|
+
expect(media[0]?.path).toBe(SAVED_PDF_PATH);
|
|
605
|
+
// The only host that should be fetched is graph.microsoft.com.
|
|
606
|
+
const calledUrls = (fetchMock.mock.calls as Array<[RequestInfo | URL, RequestInit?]>).map(
|
|
607
|
+
([input]) => resolveRequestUrl(input),
|
|
608
|
+
);
|
|
609
|
+
expect(calledUrls.length).toBeGreaterThan(0);
|
|
610
|
+
for (const url of calledUrls) {
|
|
611
|
+
expect(url.startsWith(GRAPH_SHARES_URL_PREFIX)).toBe(true);
|
|
887
612
|
}
|
|
888
|
-
|
|
613
|
+
// Graph scope token was acquired for the shares fetch.
|
|
614
|
+
expect(tokenProvider.getAccessToken).toHaveBeenCalled();
|
|
889
615
|
});
|
|
890
616
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
617
|
+
it("falls through to direct fetch for non-shared-link URLs", async () => {
|
|
618
|
+
const directUrl = createTestUrl("direct.pdf");
|
|
619
|
+
const fetchMock = createOkFetchMock(CONTENT_TYPE_APPLICATION_PDF, "pdf");
|
|
620
|
+
detectMimeMock.mockResolvedValueOnce(CONTENT_TYPE_APPLICATION_PDF);
|
|
621
|
+
saveMediaBufferMock.mockResolvedValueOnce({
|
|
622
|
+
id: "saved.pdf",
|
|
623
|
+
path: SAVED_PDF_PATH,
|
|
624
|
+
size: Buffer.byteLength(PDF_BUFFER),
|
|
625
|
+
contentType: CONTENT_TYPE_APPLICATION_PDF,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const media = await downloadAttachmentsWithFetch(
|
|
629
|
+
createPdfAttachments(directUrl),
|
|
630
|
+
fetchMock,
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
expectAttachmentMediaLength(media, 1);
|
|
634
|
+
const calledUrls = (fetchMock.mock.calls as unknown[]).map((call) => {
|
|
635
|
+
const input = (call as [RequestInfo | URL])[0];
|
|
636
|
+
return resolveRequestUrl(input);
|
|
637
|
+
});
|
|
638
|
+
// Should have hit the original host, NOT graph shares.
|
|
639
|
+
expect(calledUrls.some((url) => url === directUrl)).toBe(true);
|
|
640
|
+
expect(calledUrls.some((url) => url.startsWith(GRAPH_SHARES_URL_PREFIX))).toBe(false);
|
|
898
641
|
});
|
|
899
|
-
|
|
900
|
-
expectAttachmentMediaLength(media.media, 1);
|
|
901
|
-
const redirected = seen.find((entry) => entry.url === escapedUrl);
|
|
902
|
-
expect(redirected).toBeDefined();
|
|
903
|
-
expect(redirected?.auth).toBe("");
|
|
904
642
|
});
|
|
905
643
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
644
|
+
describe("error logging (issue #63396)", () => {
|
|
645
|
+
// Before this fix, fetch failures were swallowed by empty `catch {}`
|
|
646
|
+
// blocks, leaving operators with no signal that SharePoint downloads
|
|
647
|
+
// were silently failing on Node 24+. These tests pin the logger contract
|
|
648
|
+
// so the regression cannot return.
|
|
649
|
+
it("invokes logger.warn when a remote media download fails", async () => {
|
|
650
|
+
const logger = { warn: vi.fn(), error: vi.fn() };
|
|
651
|
+
const fetchMock = vi.fn(async () => createTextResponse("server error", 500));
|
|
652
|
+
|
|
653
|
+
const media = await downloadMSTeamsAttachments(
|
|
654
|
+
buildDownloadParams(createImageAttachments(TEST_URL_IMAGE), {
|
|
655
|
+
fetchFn: asFetchFn(fetchMock),
|
|
656
|
+
logger,
|
|
918
657
|
}),
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
expectAttachmentMediaLength(media, 0);
|
|
661
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
662
|
+
"msteams attachment download failed",
|
|
663
|
+
expect.objectContaining({
|
|
664
|
+
error: expect.stringContaining("HTTP 500"),
|
|
665
|
+
host: expect.any(String),
|
|
666
|
+
}),
|
|
667
|
+
);
|
|
668
|
+
});
|
|
924
669
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
670
|
+
it("does not log when downloads succeed", async () => {
|
|
671
|
+
const logger = { warn: vi.fn(), error: vi.fn() };
|
|
672
|
+
const fetchMock = createOkFetchMock(CONTENT_TYPE_IMAGE_PNG);
|
|
673
|
+
|
|
674
|
+
const media = await downloadMSTeamsAttachments(
|
|
675
|
+
buildDownloadParams(createImageAttachments(TEST_URL_IMAGE), {
|
|
676
|
+
fetchFn: asFetchFn(fetchMock),
|
|
677
|
+
logger,
|
|
678
|
+
}),
|
|
679
|
+
);
|
|
931
680
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
expectMSTeamsMediaPayload(payload, {
|
|
936
|
-
firstPath: "/tmp/a.png",
|
|
937
|
-
paths: ["/tmp/a.png", "/tmp/b.png"],
|
|
938
|
-
types: [CONTENT_TYPE_IMAGE_PNG, CONTENT_TYPE_IMAGE_PNG],
|
|
681
|
+
expectAttachmentMediaLength(media, 1);
|
|
682
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
683
|
+
expect(logger.error).not.toHaveBeenCalled();
|
|
939
684
|
});
|
|
940
685
|
});
|
|
941
686
|
});
|