@openclaw/msteams 2026.3.12 → 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 +161 -9
- 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 +174 -437
- 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 +148 -14
- 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 +258 -0
- package/src/graph-upload.ts +87 -8
- 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 +522 -45
- 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 +477 -174
- 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 +301 -106
- 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 +34 -40
- 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 -101
- 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);
|
|
@@ -88,15 +65,18 @@ function isUrlAllowedBySsrfPolicy(url: string, policy?: SsrFPolicy): boolean {
|
|
|
88
65
|
);
|
|
89
66
|
}
|
|
90
67
|
|
|
91
|
-
|
|
68
|
+
async function fetchRemoteMediaWithRedirects(
|
|
69
|
+
params: RemoteMediaFetchParams,
|
|
70
|
+
requestInit?: RequestInit,
|
|
71
|
+
) {
|
|
92
72
|
const fetchFn = params.fetchImpl ?? fetch;
|
|
93
73
|
let currentUrl = params.url;
|
|
94
74
|
for (let i = 0; i <= MAX_REDIRECT_HOPS; i += 1) {
|
|
95
75
|
if (!isUrlAllowedBySsrfPolicy(currentUrl, params.ssrfPolicy)) {
|
|
96
76
|
throw new Error(`Blocked hostname (not in allowlist): ${currentUrl}`);
|
|
97
77
|
}
|
|
98
|
-
const res = await fetchFn(currentUrl, { redirect: "manual" });
|
|
99
|
-
if (REDIRECT_STATUS_CODES.
|
|
78
|
+
const res = await fetchFn(currentUrl, { redirect: "manual", ...requestInit });
|
|
79
|
+
if (REDIRECT_STATUS_CODES.has(res.status)) {
|
|
100
80
|
const location = res.headers.get("location");
|
|
101
81
|
if (!location) {
|
|
102
82
|
throw new Error("redirect missing location");
|
|
@@ -107,9 +87,13 @@ const fetchRemoteMediaMock = vi.fn(async (params: RemoteMediaFetchParams) => {
|
|
|
107
87
|
return readRemoteMediaResponse(res, params);
|
|
108
88
|
}
|
|
109
89
|
throw new Error("too many redirects");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fetchRemoteMediaMock = vi.fn(async (params: RemoteMediaFetchParams) => {
|
|
93
|
+
return await fetchRemoteMediaWithRedirects(params);
|
|
110
94
|
});
|
|
111
95
|
|
|
112
|
-
const runtimeStub
|
|
96
|
+
const runtimeStub = {
|
|
113
97
|
media: {
|
|
114
98
|
detectMime: detectMimeMock,
|
|
115
99
|
},
|
|
@@ -119,12 +103,10 @@ const runtimeStub: PluginRuntime = createPluginRuntimeMock({
|
|
|
119
103
|
saveMediaBuffer: saveMediaBufferMock,
|
|
120
104
|
},
|
|
121
105
|
},
|
|
122
|
-
}
|
|
106
|
+
} as unknown as PluginRuntime;
|
|
123
107
|
|
|
124
108
|
type DownloadAttachmentsParams = Parameters<typeof downloadMSTeamsAttachments>[0];
|
|
125
|
-
type DownloadGraphMediaParams = Parameters<typeof downloadMSTeamsGraphMedia>[0];
|
|
126
109
|
type DownloadedMedia = Awaited<ReturnType<typeof downloadMSTeamsAttachments>>;
|
|
127
|
-
type MSTeamsMediaPayload = ReturnType<typeof buildMSTeamsMediaPayload>;
|
|
128
110
|
type DownloadAttachmentsBuildOverrides = Partial<
|
|
129
111
|
Omit<DownloadAttachmentsParams, "attachments" | "maxBytes" | "allowHosts">
|
|
130
112
|
> &
|
|
@@ -133,31 +115,17 @@ type DownloadAttachmentsNoFetchOverrides = Partial<
|
|
|
133
115
|
Omit<DownloadAttachmentsParams, "attachments" | "maxBytes" | "allowHosts" | "fetchFn">
|
|
134
116
|
> &
|
|
135
117
|
Pick<DownloadAttachmentsParams, "allowHosts">;
|
|
136
|
-
type DownloadGraphMediaOverrides = Partial<
|
|
137
|
-
Omit<DownloadGraphMediaParams, "messageUrl" | "tokenProvider" | "maxBytes">
|
|
138
|
-
>;
|
|
139
118
|
type FetchFn = typeof fetch;
|
|
140
119
|
type MSTeamsAttachments = DownloadAttachmentsParams["attachments"];
|
|
141
|
-
type AttachmentPlaceholderInput = Parameters<typeof buildMSTeamsAttachmentPlaceholder>[0];
|
|
142
|
-
type GraphMessageUrlParams = Parameters<typeof buildMSTeamsGraphMessageUrls>[0];
|
|
143
120
|
type LabeledCase = { label: string };
|
|
144
121
|
type FetchCallExpectation = { expectFetchCalled?: boolean };
|
|
145
122
|
type DownloadedMediaExpectation = { path?: string; placeholder?: string };
|
|
146
|
-
type MSTeamsMediaPayloadExpectation = {
|
|
147
|
-
firstPath: string;
|
|
148
|
-
paths: string[];
|
|
149
|
-
types: string[];
|
|
150
|
-
};
|
|
151
123
|
|
|
152
|
-
const DEFAULT_MESSAGE_URL = `https://${GRAPH_HOST}/v1.0/chats/19%3Achat/messages/123`;
|
|
153
|
-
const GRAPH_SHARES_URL_PREFIX = `https://${GRAPH_HOST}/v1.0/shares/`;
|
|
154
124
|
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
155
125
|
const DEFAULT_ALLOW_HOSTS = [TEST_HOST];
|
|
156
|
-
const DEFAULT_SHAREPOINT_ALLOW_HOSTS = [GRAPH_HOST, SHAREPOINT_HOST];
|
|
157
|
-
const DEFAULT_SHARE_REFERENCE_URL = createUrlForHost(SHAREPOINT_HOST, "site/file");
|
|
158
126
|
const MEDIA_PLACEHOLDER_IMAGE = "<media:image>";
|
|
159
127
|
const MEDIA_PLACEHOLDER_DOCUMENT = "<media:document>";
|
|
160
|
-
const
|
|
128
|
+
const _formatImagePlaceholder = (count: number) =>
|
|
161
129
|
count > 1 ? `${MEDIA_PLACEHOLDER_IMAGE} (${count} images)` : MEDIA_PLACEHOLDER_IMAGE;
|
|
162
130
|
const formatDocumentPlaceholder = (count: number) =>
|
|
163
131
|
count > 1 ? `${MEDIA_PLACEHOLDER_DOCUMENT} (${count} files)` : MEDIA_PLACEHOLDER_DOCUMENT;
|
|
@@ -201,19 +169,16 @@ const createTeamsFileDownloadInfoAttachments = (
|
|
|
201
169
|
content: { downloadUrl, fileType },
|
|
202
170
|
}),
|
|
203
171
|
);
|
|
204
|
-
const createMediaEntriesWithType = (contentType: string, ...paths: string[]) =>
|
|
205
|
-
paths.map((path) => ({ path, contentType }));
|
|
206
172
|
const createHostedContentsWithType = (contentType: string, ...ids: string[]) =>
|
|
207
173
|
ids.map((id) => ({ id, contentType, contentBytes: PNG_BASE64 }));
|
|
208
|
-
const
|
|
209
|
-
createMediaEntriesWithType(CONTENT_TYPE_IMAGE_PNG, ...paths);
|
|
210
|
-
const createHostedImageContents = (...ids: string[]) =>
|
|
174
|
+
const _createHostedImageContents = (...ids: string[]) =>
|
|
211
175
|
createHostedContentsWithType(CONTENT_TYPE_IMAGE_PNG, ...ids);
|
|
212
|
-
|
|
176
|
+
type BinaryPayload = Uint8Array | string;
|
|
177
|
+
const _createPdfResponse = (payload: BinaryPayload = PDF_BUFFER) => {
|
|
213
178
|
return createBufferResponse(payload, CONTENT_TYPE_APPLICATION_PDF);
|
|
214
179
|
};
|
|
215
|
-
const createBufferResponse = (payload:
|
|
216
|
-
const raw =
|
|
180
|
+
const createBufferResponse = (payload: BinaryPayload, contentType: string, status = 200) => {
|
|
181
|
+
const raw = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
217
182
|
return new Response(new Uint8Array(raw), {
|
|
218
183
|
status,
|
|
219
184
|
headers: { "content-type": contentType },
|
|
@@ -222,13 +187,16 @@ const createBufferResponse = (payload: Buffer | string, contentType: string, sta
|
|
|
222
187
|
const createJsonResponse = (payload: unknown, status = 200) =>
|
|
223
188
|
new Response(JSON.stringify(payload), { status });
|
|
224
189
|
const createTextResponse = (body: string, status = 200) => new Response(body, { status });
|
|
225
|
-
const
|
|
190
|
+
const _createGraphCollectionResponse = (value: unknown[]) => createJsonResponse({ value });
|
|
226
191
|
const createNotFoundResponse = () => new Response("not found", { status: 404 });
|
|
227
192
|
const createRedirectResponse = (location: string, status = 302) =>
|
|
228
193
|
new Response(null, { status, headers: { location } });
|
|
194
|
+
const publicResolve = async () => ({ address: "13.107.136.10" });
|
|
229
195
|
|
|
230
196
|
const createOkFetchMock = (contentType: string, payload = "png") =>
|
|
231
|
-
vi.fn(async (
|
|
197
|
+
vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
198
|
+
createBufferResponse(payload, contentType),
|
|
199
|
+
);
|
|
232
200
|
const asFetchFn = (fetchFn: unknown): FetchFn => fetchFn as FetchFn;
|
|
233
201
|
|
|
234
202
|
const buildDownloadParams = (
|
|
@@ -239,6 +207,7 @@ const buildDownloadParams = (
|
|
|
239
207
|
attachments,
|
|
240
208
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
241
209
|
allowHosts: DEFAULT_ALLOW_HOSTS,
|
|
210
|
+
resolveFn: publicResolve,
|
|
242
211
|
...overrides,
|
|
243
212
|
};
|
|
244
213
|
};
|
|
@@ -276,25 +245,6 @@ const expectMockCallState = (mockFn: unknown, shouldCall: boolean) => {
|
|
|
276
245
|
}
|
|
277
246
|
};
|
|
278
247
|
|
|
279
|
-
const DEFAULT_CHANNEL_TEAM_ID = "team-id";
|
|
280
|
-
const DEFAULT_CHANNEL_ID = "chan-id";
|
|
281
|
-
const createChannelGraphMessageUrlParams = (params: {
|
|
282
|
-
messageId: string;
|
|
283
|
-
replyToId?: string;
|
|
284
|
-
conversationId?: string;
|
|
285
|
-
}) => ({
|
|
286
|
-
conversationType: "channel" as const,
|
|
287
|
-
...params,
|
|
288
|
-
channelData: {
|
|
289
|
-
team: { id: DEFAULT_CHANNEL_TEAM_ID },
|
|
290
|
-
channel: { id: DEFAULT_CHANNEL_ID },
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
const buildExpectedChannelMessagePath = (params: { messageId: string; replyToId?: string }) =>
|
|
294
|
-
params.replyToId
|
|
295
|
-
? `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.replyToId}/replies/${params.messageId}`
|
|
296
|
-
: `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.messageId}`;
|
|
297
|
-
|
|
298
248
|
const expectAttachmentMediaLength = (media: DownloadedMedia, expectedLength: number) => {
|
|
299
249
|
expect(media).toHaveLength(expectedLength);
|
|
300
250
|
};
|
|
@@ -314,25 +264,6 @@ const expectFirstMedia = (media: DownloadedMedia, expected: DownloadedMediaExpec
|
|
|
314
264
|
expect(first?.placeholder).toBe(expected.placeholder);
|
|
315
265
|
}
|
|
316
266
|
};
|
|
317
|
-
const expectMSTeamsMediaPayload = (
|
|
318
|
-
payload: MSTeamsMediaPayload,
|
|
319
|
-
expected: MSTeamsMediaPayloadExpectation,
|
|
320
|
-
) => {
|
|
321
|
-
expect(payload.MediaPath).toBe(expected.firstPath);
|
|
322
|
-
expect(payload.MediaUrl).toBe(expected.firstPath);
|
|
323
|
-
expect(payload.MediaPaths).toEqual(expected.paths);
|
|
324
|
-
expect(payload.MediaUrls).toEqual(expected.paths);
|
|
325
|
-
expect(payload.MediaTypes).toEqual(expected.types);
|
|
326
|
-
};
|
|
327
|
-
type AttachmentPlaceholderCase = LabeledCase & {
|
|
328
|
-
attachments: AttachmentPlaceholderInput;
|
|
329
|
-
expected: string;
|
|
330
|
-
};
|
|
331
|
-
type CountedAttachmentPlaceholderCaseDef = LabeledCase & {
|
|
332
|
-
attachments: AttachmentPlaceholderCase["attachments"];
|
|
333
|
-
count: number;
|
|
334
|
-
formatPlaceholder: (count: number) => string;
|
|
335
|
-
};
|
|
336
267
|
type AttachmentDownloadSuccessCase = LabeledCase & {
|
|
337
268
|
attachments: MSTeamsAttachments;
|
|
338
269
|
buildFetchFn?: () => unknown;
|
|
@@ -350,77 +281,6 @@ type AttachmentAuthRetryCase = LabeledCase & {
|
|
|
350
281
|
expectedMediaLength: number;
|
|
351
282
|
expectTokenFetch: boolean;
|
|
352
283
|
};
|
|
353
|
-
type GraphUrlExpectationCase = LabeledCase & {
|
|
354
|
-
params: GraphMessageUrlParams;
|
|
355
|
-
expectedPath: string;
|
|
356
|
-
};
|
|
357
|
-
type ChannelGraphUrlCaseParams = {
|
|
358
|
-
messageId: string;
|
|
359
|
-
replyToId?: string;
|
|
360
|
-
conversationId?: string;
|
|
361
|
-
};
|
|
362
|
-
type GraphMediaDownloadResult = {
|
|
363
|
-
fetchMock: ReturnType<typeof createGraphFetchMock>;
|
|
364
|
-
media: Awaited<ReturnType<typeof downloadMSTeamsGraphMedia>>;
|
|
365
|
-
};
|
|
366
|
-
type GraphMediaSuccessCase = LabeledCase & {
|
|
367
|
-
buildOptions: () => GraphFetchMockOptions;
|
|
368
|
-
expectedLength: number;
|
|
369
|
-
assert?: (params: GraphMediaDownloadResult) => void;
|
|
370
|
-
};
|
|
371
|
-
const EMPTY_ATTACHMENT_PLACEHOLDER_CASES: AttachmentPlaceholderCase[] = [
|
|
372
|
-
withLabel("returns empty string when no attachments", { attachments: undefined, expected: "" }),
|
|
373
|
-
withLabel("returns empty string when attachments are empty", { attachments: [], expected: "" }),
|
|
374
|
-
];
|
|
375
|
-
const COUNTED_ATTACHMENT_PLACEHOLDER_CASE_DEFS: CountedAttachmentPlaceholderCaseDef[] = [
|
|
376
|
-
withLabel("returns image placeholder for one image attachment", {
|
|
377
|
-
attachments: createImageAttachments(TEST_URL_IMAGE_PNG),
|
|
378
|
-
count: 1,
|
|
379
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
380
|
-
}),
|
|
381
|
-
withLabel("returns image placeholder with count for many image attachments", {
|
|
382
|
-
attachments: [
|
|
383
|
-
...createImageAttachments(TEST_URL_IMAGE_1_PNG),
|
|
384
|
-
{ contentType: "image/jpeg", contentUrl: TEST_URL_IMAGE_2_JPG },
|
|
385
|
-
],
|
|
386
|
-
count: 2,
|
|
387
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
388
|
-
}),
|
|
389
|
-
withLabel("treats Teams file.download.info image attachments as images", {
|
|
390
|
-
attachments: createTeamsFileDownloadInfoAttachments(),
|
|
391
|
-
count: 1,
|
|
392
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
393
|
-
}),
|
|
394
|
-
withLabel("returns document placeholder for non-image attachments", {
|
|
395
|
-
attachments: createPdfAttachments(TEST_URL_PDF),
|
|
396
|
-
count: 1,
|
|
397
|
-
formatPlaceholder: formatDocumentPlaceholder,
|
|
398
|
-
}),
|
|
399
|
-
withLabel("returns document placeholder with count for many non-image attachments", {
|
|
400
|
-
attachments: createPdfAttachments(TEST_URL_PDF_1, TEST_URL_PDF_2),
|
|
401
|
-
count: 2,
|
|
402
|
-
formatPlaceholder: formatDocumentPlaceholder,
|
|
403
|
-
}),
|
|
404
|
-
withLabel("counts one inline image in html attachments", {
|
|
405
|
-
attachments: createHtmlImageAttachments([TEST_URL_HTML_A], "<p>hi</p>"),
|
|
406
|
-
count: 1,
|
|
407
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
408
|
-
}),
|
|
409
|
-
withLabel("counts many inline images in html attachments", {
|
|
410
|
-
attachments: createHtmlImageAttachments([TEST_URL_HTML_A, TEST_URL_HTML_B]),
|
|
411
|
-
count: 2,
|
|
412
|
-
formatPlaceholder: formatImagePlaceholder,
|
|
413
|
-
}),
|
|
414
|
-
];
|
|
415
|
-
const ATTACHMENT_PLACEHOLDER_CASES: AttachmentPlaceholderCase[] = [
|
|
416
|
-
...EMPTY_ATTACHMENT_PLACEHOLDER_CASES,
|
|
417
|
-
...COUNTED_ATTACHMENT_PLACEHOLDER_CASE_DEFS.map((testCase) =>
|
|
418
|
-
withLabel(testCase.label, {
|
|
419
|
-
attachments: testCase.attachments,
|
|
420
|
-
expected: testCase.formatPlaceholder(testCase.count),
|
|
421
|
-
}),
|
|
422
|
-
),
|
|
423
|
-
];
|
|
424
284
|
const ATTACHMENT_DOWNLOAD_SUCCESS_CASES: AttachmentDownloadSuccessCase[] = [
|
|
425
285
|
withLabel("downloads and stores image contentUrl attachments", {
|
|
426
286
|
attachments: asSingleItemArray(IMAGE_ATTACHMENT),
|
|
@@ -480,150 +340,6 @@ const ATTACHMENT_AUTH_RETRY_CASES: AttachmentAuthRetryCase[] = [
|
|
|
480
340
|
expectTokenFetch: false,
|
|
481
341
|
}),
|
|
482
342
|
];
|
|
483
|
-
const GRAPH_MEDIA_SUCCESS_CASES: GraphMediaSuccessCase[] = [
|
|
484
|
-
withLabel("downloads hostedContents images", {
|
|
485
|
-
buildOptions: () => ({ hostedContents: createHostedImageContents("1") }),
|
|
486
|
-
expectedLength: 1,
|
|
487
|
-
assert: ({ fetchMock }) => {
|
|
488
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
489
|
-
expectMediaBufferSaved();
|
|
490
|
-
},
|
|
491
|
-
}),
|
|
492
|
-
withLabel("merges SharePoint reference attachments with hosted content", {
|
|
493
|
-
buildOptions: () => {
|
|
494
|
-
return {
|
|
495
|
-
hostedContents: createHostedImageContents("hosted-1"),
|
|
496
|
-
...buildDefaultShareReferenceGraphFetchOptions({
|
|
497
|
-
onShareRequest: () => createPdfResponse(),
|
|
498
|
-
}),
|
|
499
|
-
};
|
|
500
|
-
},
|
|
501
|
-
expectedLength: 2,
|
|
502
|
-
}),
|
|
503
|
-
];
|
|
504
|
-
const CHANNEL_GRAPH_URL_CASES: Array<LabeledCase & ChannelGraphUrlCaseParams> = [
|
|
505
|
-
withLabel("builds channel message urls", {
|
|
506
|
-
conversationId: "19:thread@thread.tacv2",
|
|
507
|
-
messageId: "123",
|
|
508
|
-
}),
|
|
509
|
-
withLabel("builds channel reply urls when replyToId is present", {
|
|
510
|
-
messageId: "reply-id",
|
|
511
|
-
replyToId: "root-id",
|
|
512
|
-
}),
|
|
513
|
-
];
|
|
514
|
-
const GRAPH_URL_EXPECTATION_CASES: GraphUrlExpectationCase[] = [
|
|
515
|
-
...CHANNEL_GRAPH_URL_CASES.map<GraphUrlExpectationCase>(({ label, ...params }) =>
|
|
516
|
-
withLabel(label, {
|
|
517
|
-
params: createChannelGraphMessageUrlParams(params),
|
|
518
|
-
expectedPath: buildExpectedChannelMessagePath(params),
|
|
519
|
-
}),
|
|
520
|
-
),
|
|
521
|
-
withLabel("builds chat message urls", {
|
|
522
|
-
params: {
|
|
523
|
-
conversationType: "groupChat" as const,
|
|
524
|
-
conversationId: "19:chat@thread.v2",
|
|
525
|
-
messageId: "456",
|
|
526
|
-
},
|
|
527
|
-
expectedPath: "/chats/19%3Achat%40thread.v2/messages/456",
|
|
528
|
-
}),
|
|
529
|
-
];
|
|
530
|
-
|
|
531
|
-
type GraphFetchMockOptions = {
|
|
532
|
-
hostedContents?: unknown[];
|
|
533
|
-
attachments?: unknown[];
|
|
534
|
-
messageAttachments?: unknown[];
|
|
535
|
-
onShareRequest?: (url: string) => Response | Promise<Response>;
|
|
536
|
-
onUnhandled?: (url: string) => Response | Promise<Response> | undefined;
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const createReferenceAttachment = (shareUrl = DEFAULT_SHARE_REFERENCE_URL) => ({
|
|
540
|
-
id: "ref-1",
|
|
541
|
-
contentType: "reference",
|
|
542
|
-
contentUrl: shareUrl,
|
|
543
|
-
name: "report.pdf",
|
|
544
|
-
});
|
|
545
|
-
const buildShareReferenceGraphFetchOptions = (params: {
|
|
546
|
-
referenceAttachment: ReturnType<typeof createReferenceAttachment>;
|
|
547
|
-
onShareRequest?: GraphFetchMockOptions["onShareRequest"];
|
|
548
|
-
onUnhandled?: GraphFetchMockOptions["onUnhandled"];
|
|
549
|
-
}) => ({
|
|
550
|
-
attachments: [params.referenceAttachment],
|
|
551
|
-
messageAttachments: [params.referenceAttachment],
|
|
552
|
-
...(params.onShareRequest ? { onShareRequest: params.onShareRequest } : {}),
|
|
553
|
-
...(params.onUnhandled ? { onUnhandled: params.onUnhandled } : {}),
|
|
554
|
-
});
|
|
555
|
-
const buildDefaultShareReferenceGraphFetchOptions = (
|
|
556
|
-
params: Omit<Parameters<typeof buildShareReferenceGraphFetchOptions>[0], "referenceAttachment">,
|
|
557
|
-
) =>
|
|
558
|
-
buildShareReferenceGraphFetchOptions({
|
|
559
|
-
referenceAttachment: createReferenceAttachment(),
|
|
560
|
-
...params,
|
|
561
|
-
});
|
|
562
|
-
type GraphEndpointResponseHandler = {
|
|
563
|
-
suffix: string;
|
|
564
|
-
buildResponse: () => Response;
|
|
565
|
-
};
|
|
566
|
-
const createGraphEndpointResponseHandlers = (params: {
|
|
567
|
-
hostedContents: unknown[];
|
|
568
|
-
attachments: unknown[];
|
|
569
|
-
messageAttachments: unknown[];
|
|
570
|
-
}): GraphEndpointResponseHandler[] => [
|
|
571
|
-
{
|
|
572
|
-
suffix: "/hostedContents",
|
|
573
|
-
buildResponse: () => createGraphCollectionResponse(params.hostedContents),
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
suffix: "/attachments",
|
|
577
|
-
buildResponse: () => createGraphCollectionResponse(params.attachments),
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
suffix: "/messages/123",
|
|
581
|
-
buildResponse: () => createJsonResponse({ attachments: params.messageAttachments }),
|
|
582
|
-
},
|
|
583
|
-
];
|
|
584
|
-
const resolveGraphEndpointResponse = (
|
|
585
|
-
url: string,
|
|
586
|
-
handlers: GraphEndpointResponseHandler[],
|
|
587
|
-
): Response | undefined => {
|
|
588
|
-
const handler = handlers.find((entry) => url.endsWith(entry.suffix));
|
|
589
|
-
return handler ? handler.buildResponse() : undefined;
|
|
590
|
-
};
|
|
591
|
-
|
|
592
|
-
const createGraphFetchMock = (options: GraphFetchMockOptions = {}) => {
|
|
593
|
-
const hostedContents = options.hostedContents ?? [];
|
|
594
|
-
const attachments = options.attachments ?? [];
|
|
595
|
-
const messageAttachments = options.messageAttachments ?? [];
|
|
596
|
-
const endpointHandlers = createGraphEndpointResponseHandlers({
|
|
597
|
-
hostedContents,
|
|
598
|
-
attachments,
|
|
599
|
-
messageAttachments,
|
|
600
|
-
});
|
|
601
|
-
return vi.fn(async (url: string) => {
|
|
602
|
-
const endpointResponse = resolveGraphEndpointResponse(url, endpointHandlers);
|
|
603
|
-
if (endpointResponse) {
|
|
604
|
-
return endpointResponse;
|
|
605
|
-
}
|
|
606
|
-
if (url.startsWith(GRAPH_SHARES_URL_PREFIX) && options.onShareRequest) {
|
|
607
|
-
return options.onShareRequest(url);
|
|
608
|
-
}
|
|
609
|
-
const unhandled = options.onUnhandled ? await options.onUnhandled(url) : undefined;
|
|
610
|
-
return unhandled ?? createNotFoundResponse();
|
|
611
|
-
});
|
|
612
|
-
};
|
|
613
|
-
const downloadGraphMediaWithMockOptions = async (
|
|
614
|
-
options: GraphFetchMockOptions = {},
|
|
615
|
-
overrides: DownloadGraphMediaOverrides = {},
|
|
616
|
-
): Promise<GraphMediaDownloadResult> => {
|
|
617
|
-
const fetchMock = createGraphFetchMock(options);
|
|
618
|
-
const media = await downloadMSTeamsGraphMedia({
|
|
619
|
-
messageUrl: DEFAULT_MESSAGE_URL,
|
|
620
|
-
tokenProvider: createTokenProvider(),
|
|
621
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
622
|
-
fetchFn: asFetchFn(fetchMock),
|
|
623
|
-
...overrides,
|
|
624
|
-
});
|
|
625
|
-
return { fetchMock, media };
|
|
626
|
-
};
|
|
627
343
|
const runAttachmentDownloadSuccessCase = async ({
|
|
628
344
|
attachments,
|
|
629
345
|
buildFetchFn,
|
|
@@ -654,15 +370,6 @@ const runAttachmentAuthRetryCase = async ({
|
|
|
654
370
|
expectAttachmentMediaLength(media, expectedMediaLength);
|
|
655
371
|
expectMockCallState(tokenProvider.getAccessToken, expectTokenFetch);
|
|
656
372
|
};
|
|
657
|
-
const runGraphMediaSuccessCase = async ({
|
|
658
|
-
buildOptions,
|
|
659
|
-
expectedLength,
|
|
660
|
-
assert,
|
|
661
|
-
}: GraphMediaSuccessCase) => {
|
|
662
|
-
const { fetchMock, media } = await downloadGraphMediaWithMockOptions(buildOptions());
|
|
663
|
-
expectAttachmentMediaLength(media.media, expectedLength);
|
|
664
|
-
assert?.({ fetchMock, media });
|
|
665
|
-
};
|
|
666
373
|
|
|
667
374
|
describe("msteams attachments", () => {
|
|
668
375
|
beforeEach(() => {
|
|
@@ -672,15 +379,6 @@ describe("msteams attachments", () => {
|
|
|
672
379
|
setMSTeamsRuntime(runtimeStub);
|
|
673
380
|
});
|
|
674
381
|
|
|
675
|
-
describe("buildMSTeamsAttachmentPlaceholder", () => {
|
|
676
|
-
it.each<AttachmentPlaceholderCase>(ATTACHMENT_PLACEHOLDER_CASES)(
|
|
677
|
-
"$label",
|
|
678
|
-
({ attachments, expected }) => {
|
|
679
|
-
expect(buildMSTeamsAttachmentPlaceholder(attachments)).toBe(expected);
|
|
680
|
-
},
|
|
681
|
-
);
|
|
682
|
-
});
|
|
683
|
-
|
|
684
382
|
describe("downloadMSTeamsAttachments", () => {
|
|
685
383
|
it.each<AttachmentDownloadSuccessCase>(ATTACHMENT_DOWNLOAD_SUCCESS_CASES)(
|
|
686
384
|
"$label",
|
|
@@ -720,24 +418,9 @@ describe("msteams attachments", () => {
|
|
|
720
418
|
});
|
|
721
419
|
|
|
722
420
|
fetchRemoteMediaMock.mockImplementationOnce(async (params) => {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const res = await fetchFn(currentUrl, {
|
|
727
|
-
redirect: "manual",
|
|
728
|
-
dispatcher: {},
|
|
729
|
-
} as RequestInit);
|
|
730
|
-
if (REDIRECT_STATUS_CODES.includes(res.status)) {
|
|
731
|
-
const location = res.headers.get("location");
|
|
732
|
-
if (!location) {
|
|
733
|
-
throw new Error("redirect missing location");
|
|
734
|
-
}
|
|
735
|
-
currentUrl = new URL(location, currentUrl).toString();
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
return readRemoteMediaResponse(res, params);
|
|
739
|
-
}
|
|
740
|
-
throw new Error("too many redirects");
|
|
421
|
+
return await fetchRemoteMediaWithRedirects(params, {
|
|
422
|
+
dispatcher: {},
|
|
423
|
+
} as RequestInit);
|
|
741
424
|
});
|
|
742
425
|
|
|
743
426
|
const media = await downloadAttachmentsWithFetch(
|
|
@@ -748,7 +431,7 @@ describe("msteams attachments", () => {
|
|
|
748
431
|
|
|
749
432
|
expectAttachmentMediaLength(media, 1);
|
|
750
433
|
expect(tokenProvider.getAccessToken).toHaveBeenCalledOnce();
|
|
751
|
-
expect(fetchMock.mock.calls.map(([calledUrl]) =>
|
|
434
|
+
expect(fetchMock.mock.calls.map(([calledUrl]) => calledUrl)).toContain(redirectedUrl);
|
|
752
435
|
});
|
|
753
436
|
|
|
754
437
|
it("continues scope fallback after non-auth failure and succeeds on later scope", async () => {
|
|
@@ -835,7 +518,7 @@ describe("msteams attachments", () => {
|
|
|
835
518
|
it("blocks redirects to non-https URLs", async () => {
|
|
836
519
|
const insecureUrl = "http://x/insecure.png";
|
|
837
520
|
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
|
838
|
-
const url =
|
|
521
|
+
const url = resolveRequestUrl(input);
|
|
839
522
|
if (url === TEST_URL_IMAGE) {
|
|
840
523
|
return createRedirectResponse(insecureUrl);
|
|
841
524
|
}
|
|
@@ -856,94 +539,148 @@ describe("msteams attachments", () => {
|
|
|
856
539
|
expectAttachmentMediaLength(media, 0);
|
|
857
540
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
858
541
|
});
|
|
859
|
-
});
|
|
860
542
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
const auth = new Headers(init?.headers).get("Authorization") ?? "";
|
|
879
|
-
seen.push({ url, auth });
|
|
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
|
+
});
|
|
880
560
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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);
|
|
895
612
|
}
|
|
896
|
-
|
|
613
|
+
// Graph scope token was acquired for the shares fetch.
|
|
614
|
+
expect(tokenProvider.getAccessToken).toHaveBeenCalled();
|
|
897
615
|
});
|
|
898
616
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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);
|
|
906
641
|
});
|
|
907
|
-
|
|
908
|
-
expectAttachmentMediaLength(media.media, 1);
|
|
909
|
-
const redirected = seen.find((entry) => entry.url === escapedUrl);
|
|
910
|
-
expect(redirected).toBeDefined();
|
|
911
|
-
expect(redirected?.auth).toBe("");
|
|
912
642
|
});
|
|
913
643
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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,
|
|
926
657
|
}),
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
+
});
|
|
932
669
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
+
);
|
|
939
680
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
expectMSTeamsMediaPayload(payload, {
|
|
944
|
-
firstPath: "/tmp/a.png",
|
|
945
|
-
paths: ["/tmp/a.png", "/tmp/b.png"],
|
|
946
|
-
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();
|
|
947
684
|
});
|
|
948
685
|
});
|
|
949
686
|
});
|