mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
6c29fc154f
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
777 lines
26 KiB
TypeScript
777 lines
26 KiB
TypeScript
// Msteams tests cover bot framework plugin behavior.
|
|
import { expectDefined } from "@openclaw/normalization-core";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { setMSTeamsRuntime } from "../runtime.js";
|
|
import {
|
|
downloadMSTeamsBotFrameworkAttachments,
|
|
isBotFrameworkPersonalChatId,
|
|
} from "./bot-framework.js";
|
|
import type { MSTeamsAccessTokenProvider } from "./types.js";
|
|
|
|
type SavedCall = {
|
|
buffer: Buffer;
|
|
contentType?: string;
|
|
direction: string;
|
|
maxBytes: number;
|
|
originalFilename?: string;
|
|
};
|
|
|
|
type MockRuntime = {
|
|
saveCalls: SavedCall[];
|
|
savePath: string;
|
|
savedContentType: string;
|
|
};
|
|
|
|
type DownloadSingleAttachmentParams = Omit<
|
|
Parameters<typeof downloadMSTeamsBotFrameworkAttachments>[0],
|
|
"attachmentIds"
|
|
> & { attachmentId: string };
|
|
|
|
async function downloadMSTeamsBotFrameworkAttachment(params: DownloadSingleAttachmentParams) {
|
|
const { attachmentId, ...rest } = params;
|
|
const result = await downloadMSTeamsBotFrameworkAttachments({
|
|
...rest,
|
|
attachmentIds: [attachmentId],
|
|
});
|
|
return result.media[0];
|
|
}
|
|
|
|
function expectUnavailableMedia(media: unknown, sourceId: string): void {
|
|
expect(media).toEqual({ kind: "document", sourceId });
|
|
}
|
|
|
|
function installRuntime(): MockRuntime {
|
|
const state: MockRuntime = {
|
|
saveCalls: [],
|
|
savePath: "/tmp/bf-attachment.bin",
|
|
savedContentType: "application/pdf",
|
|
};
|
|
setMSTeamsRuntime({
|
|
media: {
|
|
detectMime: async ({ headerMime }: { headerMime?: string }) =>
|
|
headerMime ?? "application/pdf",
|
|
},
|
|
channel: {
|
|
media: {
|
|
saveMediaBuffer: async (
|
|
buffer: Buffer,
|
|
contentType: string | undefined,
|
|
direction: string,
|
|
maxBytes: number,
|
|
originalFilename?: string,
|
|
) => {
|
|
state.saveCalls.push({
|
|
buffer,
|
|
contentType,
|
|
direction,
|
|
maxBytes,
|
|
originalFilename,
|
|
});
|
|
return { path: state.savePath, contentType: state.savedContentType };
|
|
},
|
|
readRemoteMediaBuffer: async () => ({ buffer: Buffer.alloc(0), contentType: undefined }),
|
|
saveRemoteMedia: async () => ({
|
|
path: state.savePath,
|
|
contentType: state.savedContentType,
|
|
}),
|
|
saveResponseMedia: async (
|
|
response: Response,
|
|
options: {
|
|
fallbackContentType?: string;
|
|
subdir?: string;
|
|
maxBytes?: number;
|
|
originalFilename?: string;
|
|
},
|
|
) => {
|
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
state.saveCalls.push({
|
|
buffer,
|
|
contentType: options.fallbackContentType,
|
|
direction: options.subdir ?? "inbound",
|
|
maxBytes: options.maxBytes ?? 0,
|
|
originalFilename: options.originalFilename,
|
|
});
|
|
return { path: state.savePath, contentType: state.savedContentType };
|
|
},
|
|
},
|
|
},
|
|
} as unknown as Parameters<typeof setMSTeamsRuntime>[0]);
|
|
return state;
|
|
}
|
|
|
|
function createMockFetch(entries: Array<{ match: RegExp; response: Response }>): typeof fetch {
|
|
return vi.fn(async (input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
const entry = entries.find((e) => e.match.test(url));
|
|
if (!entry) {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
// Fetch returns one body. Cloning tees it, so canceling the returned branch
|
|
// would wait forever for the untouched fixture branch to be canceled too.
|
|
return entry.response;
|
|
}) as typeof fetch;
|
|
}
|
|
|
|
function buildTokenProvider(): MSTeamsAccessTokenProvider {
|
|
return {
|
|
getAccessToken: vi.fn(async (scope: string) => {
|
|
if (scope.includes("botframework.com")) {
|
|
return "bf-token";
|
|
}
|
|
return "graph-token";
|
|
}),
|
|
};
|
|
}
|
|
|
|
function firstMockCall(mock: ReturnType<typeof vi.fn>, label: string): unknown[] {
|
|
const [call] = mock.mock.calls;
|
|
if (!call) {
|
|
throw new Error(`expected ${label} call`);
|
|
}
|
|
return call;
|
|
}
|
|
|
|
async function resolvePublicHost(): Promise<{ address: string }> {
|
|
return { address: "93.184.216.34" };
|
|
}
|
|
|
|
describe("isBotFrameworkPersonalChatId", () => {
|
|
it("detects a: prefix personal chat IDs", () => {
|
|
expect(isBotFrameworkPersonalChatId("a:1dRsHCobZ1AxURzY05Dc")).toBe(true);
|
|
});
|
|
|
|
it("detects 8:orgid: prefix chat IDs", () => {
|
|
expect(isBotFrameworkPersonalChatId("8:orgid:12345678-1234-1234-1234-123456789abc")).toBe(true);
|
|
});
|
|
|
|
it("returns false for Graph-compatible 19: thread IDs", () => {
|
|
expect(isBotFrameworkPersonalChatId("19:abc@thread.tacv2")).toBe(false);
|
|
});
|
|
|
|
it("returns false for synthetic DM Graph IDs", () => {
|
|
expect(isBotFrameworkPersonalChatId("19:aad-user-id_bot-app-id@unq.gbl.spaces")).toBe(false);
|
|
});
|
|
|
|
it("returns false for null/undefined/empty", () => {
|
|
expect(isBotFrameworkPersonalChatId(null)).toBe(false);
|
|
expect(isBotFrameworkPersonalChatId(undefined)).toBe(false);
|
|
expect(isBotFrameworkPersonalChatId("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("downloadMSTeamsBotFrameworkAttachment", () => {
|
|
let runtime: MockRuntime;
|
|
beforeEach(() => {
|
|
runtime = installRuntime();
|
|
});
|
|
|
|
it("fetches attachment info then view and saves media", async () => {
|
|
const info = {
|
|
name: "report.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: 1024 }],
|
|
};
|
|
const fileBytes = Buffer.from("PDFBYTES", "utf-8");
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/att-1$/,
|
|
response: new Response(JSON.stringify(info), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/att-1\/views\/original$/,
|
|
response: new Response(fileBytes, {
|
|
status: 200,
|
|
headers: { "content-length": String(fileBytes.byteLength) },
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer/",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expect(media?.path).toBe(runtime.savePath);
|
|
expect(media?.contentType).toBe(runtime.savedContentType);
|
|
expect(runtime.saveCalls).toHaveLength(1);
|
|
expect(expectDefined(runtime.saveCalls[0], "MSTeams save call").buffer.toString("utf-8")).toBe(
|
|
"PDFBYTES",
|
|
);
|
|
});
|
|
|
|
it("skips malformed attachment view content-length before saving media", async () => {
|
|
const info = {
|
|
name: "report.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: 3 }],
|
|
};
|
|
const warn = vi.fn();
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/att-1$/,
|
|
response: new Response(JSON.stringify(info), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/att-1\/views\/original$/,
|
|
response: new Response("PDFBYTES", {
|
|
status: 200,
|
|
headers: { "content-length": "0x3" },
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer/",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
logger: { warn },
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(runtime.saveCalls).toHaveLength(0);
|
|
expect(warn).toHaveBeenCalledWith(
|
|
"msteams botFramework attachmentView invalid content-length",
|
|
{ error: "invalid content-length header: 0x3" },
|
|
);
|
|
});
|
|
|
|
it("returns undefined when attachment info fetch fails", async () => {
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\//,
|
|
response: new Response("unauthorized", { status: 401 }),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(runtime.saveCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("does not send Bot Framework service tokens to non-auth-allowlisted media hosts", async () => {
|
|
const seenAuth: Array<string | null> = [];
|
|
const fetchFn: typeof fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
seenAuth.push(new Headers(init?.headers).get("authorization"));
|
|
return new Response("unauthorized", { status: 401 });
|
|
}) as typeof fetch;
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://attacker.trafficmanager.net",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(seenAuth).toEqual([null]);
|
|
expect(runtime.saveCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("sends Bot Framework service tokens to auth-allowlisted service hosts", async () => {
|
|
const seenAuth: Array<string | null> = [];
|
|
const fileBytes = Buffer.from("BFBYTES", "utf-8");
|
|
const fetchFn: typeof fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
seenAuth.push(new Headers(init?.headers).get("authorization"));
|
|
if (url.endsWith("/v3/attachments/att-1")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
name: "doc.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: fileBytes.byteLength }],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
);
|
|
}
|
|
if (url.endsWith("/v3/attachments/att-1/views/original")) {
|
|
return new Response(fileBytes, {
|
|
status: 200,
|
|
headers: { "content-length": String(fileBytes.byteLength) },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
}) as typeof fetch;
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expect(media?.path).toBe(runtime.savePath);
|
|
expect(seenAuth).toEqual(["Bearer bf-token", "Bearer bf-token"]);
|
|
});
|
|
|
|
it("skips when attachment view size exceeds maxBytes", async () => {
|
|
const info = {
|
|
name: "huge.bin",
|
|
type: "application/octet-stream",
|
|
views: [{ viewId: "original", size: 50_000_000 }],
|
|
};
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/big-1$/,
|
|
response: new Response(JSON.stringify(info), { status: 200 }),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "big-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "big-1");
|
|
expect(runtime.saveCalls).toHaveLength(0);
|
|
});
|
|
|
|
it("returns undefined when no views are returned", async () => {
|
|
const info = { name: "nothing", type: "application/pdf", views: [] };
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/empty-1$/,
|
|
response: new Response(JSON.stringify(info), { status: 200 }),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "empty-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "empty-1");
|
|
});
|
|
|
|
it("returns undefined without a tokenProvider", async () => {
|
|
const fetchFn = vi.fn();
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: undefined,
|
|
maxBytes: 10_000_000,
|
|
fetchFn: fetchFn as unknown as typeof fetch,
|
|
});
|
|
expect(media).toBeUndefined();
|
|
expect(fetchFn).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe("guarded attachment fetches", () => {
|
|
it("drives dispatcher-aware caller fetchFn hooks through a pinned dispatcher", async () => {
|
|
const fileBytes = Buffer.from("BFBYTES", "utf-8");
|
|
const fetchCalls: Array<{ url: string; init?: RequestInit }> = [];
|
|
const fetchFn: typeof fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
fetchCalls.push({ url, init });
|
|
if (url.endsWith("/v3/attachments/att-1")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
name: "doc.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: fileBytes.byteLength }],
|
|
}),
|
|
{ status: 200, headers: { "content-type": "application/json" } },
|
|
);
|
|
}
|
|
if (url.endsWith("/v3/attachments/att-1/views/original")) {
|
|
return new Response(fileBytes, {
|
|
status: 200,
|
|
headers: { "content-length": String(fileBytes.byteLength) },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
}) as typeof fetch;
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expect(media?.path).toBe(runtime.savePath);
|
|
expect(media?.contentType).toBe(runtime.savedContentType);
|
|
// Both the attachment info call and the view call should be observed,
|
|
// confirming the guarded fetch path still preserves caller fetch hooks.
|
|
expect(fetchCalls).toHaveLength(2);
|
|
expect(expectDefined(fetchCalls[0], "attachment info fetch").url).toMatch(
|
|
/\/v3\/attachments\/att-1$/,
|
|
);
|
|
expect(expectDefined(fetchCalls[1], "attachment view fetch").url).toMatch(
|
|
/\/v3\/attachments\/att-1\/views\/original$/,
|
|
);
|
|
for (const call of fetchCalls) {
|
|
const init = call.init as RequestInit & { dispatcher?: unknown };
|
|
expect(init?.dispatcher).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("logs a warning when the attachmentInfo fetch throws (no longer silently swallowed)", async () => {
|
|
const warn = vi.fn();
|
|
const logger = { warn };
|
|
const error = new TypeError("fetch failed | invalid onRequestStart method");
|
|
const fetchFn: typeof fetch = (async () => {
|
|
throw error;
|
|
}) as typeof fetch;
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
logger,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(firstMockCall(warn, "logger.warn")).toStrictEqual([
|
|
"msteams botFramework attachmentInfo fetch failed",
|
|
{ error: "fetch failed | invalid onRequestStart method" },
|
|
]);
|
|
});
|
|
|
|
it("logs a warning when the attachmentView fetch throws", async () => {
|
|
const warn = vi.fn();
|
|
const logger = { warn };
|
|
const fetchFn: typeof fetch = (async (input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
if (url.endsWith("/v3/attachments/att-1")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
name: "doc.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: 10 }],
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
throw new TypeError("fetch failed");
|
|
}) as typeof fetch;
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
logger,
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(firstMockCall(warn, "logger.warn")).toStrictEqual([
|
|
"msteams botFramework attachmentView fetch failed",
|
|
{ error: "fetch failed" },
|
|
]);
|
|
});
|
|
|
|
it("logs a warning on non-ok attachmentInfo response", async () => {
|
|
const warn = vi.fn();
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/att-1$/,
|
|
response: new Response("server error", { status: 500 }),
|
|
},
|
|
]);
|
|
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
resolveFn: resolvePublicHost,
|
|
logger: { warn },
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
expect(firstMockCall(warn, "logger.warn")).toStrictEqual([
|
|
"msteams botFramework attachmentInfo non-ok",
|
|
{ status: 500 },
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "attachment info is unavailable",
|
|
stage: "info",
|
|
init: { status: 500 },
|
|
warning: "msteams botFramework attachmentInfo non-ok",
|
|
},
|
|
{
|
|
name: "attachment view is unavailable",
|
|
stage: "view",
|
|
init: { status: 500 },
|
|
warning: "msteams botFramework attachmentView non-ok",
|
|
},
|
|
{
|
|
name: "attachment view content-length is invalid",
|
|
stage: "view",
|
|
init: { status: 200, headers: { "content-length": "0x3" } },
|
|
warning: "msteams botFramework attachmentView invalid content-length",
|
|
},
|
|
{
|
|
name: "attachment view exceeds maxBytes",
|
|
stage: "view",
|
|
init: { status: 200, headers: { "content-length": "11" } },
|
|
},
|
|
] as const)("preserves the stable outcome when $name cleanup rejects", async (scenario) => {
|
|
const unhandledRejections: unknown[] = [];
|
|
const onUnhandledRejection = (reason: unknown) => {
|
|
unhandledRejections.push(reason);
|
|
};
|
|
const cancel = vi.fn(() => {
|
|
throw new Error("discarded Teams response cancellation failed");
|
|
});
|
|
const body = new ReadableStream<Uint8Array>({ cancel });
|
|
const discardedResponse = new Response(body, scenario.init);
|
|
const warn = vi.fn();
|
|
const fetchFn: typeof fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
const url =
|
|
typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
if (scenario.stage === "info" || url.endsWith("/views/original")) {
|
|
return discardedResponse;
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
name: "doc.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: 1 }],
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
});
|
|
process.on("unhandledRejection", onUnhandledRejection);
|
|
|
|
try {
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
logger: { warn },
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(runtime.saveCalls).toHaveLength(0);
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
if (scenario.warning) {
|
|
expect(warn).toHaveBeenCalledWith(scenario.warning, expect.any(Object));
|
|
} else {
|
|
expect(warn).not.toHaveBeenCalled();
|
|
}
|
|
await new Promise<void>((resolve) => {
|
|
setImmediate(resolve);
|
|
});
|
|
expect(unhandledRejections).toStrictEqual([]);
|
|
} finally {
|
|
process.off("unhandledRejection", onUnhandledRejection);
|
|
expect(process.listeners("unhandledRejection")).not.toContain(onUnhandledRejection);
|
|
}
|
|
});
|
|
|
|
it("bounds an unbounded attachmentInfo JSON body and cancels the stream", async () => {
|
|
const state = { canceled: false, enqueued: 0 };
|
|
const chunkBytes = 1024 * 1024;
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
pull(controller) {
|
|
if (state.enqueued >= 64) {
|
|
controller.close();
|
|
return;
|
|
}
|
|
state.enqueued += 1;
|
|
controller.enqueue(new Uint8Array(chunkBytes).fill(0x61));
|
|
},
|
|
cancel() {
|
|
state.canceled = true;
|
|
},
|
|
});
|
|
const jsonSpy = vi.spyOn(Response.prototype, "json").mockImplementation(async () => {
|
|
throw new Error("raw response.json() should not be used");
|
|
});
|
|
const fetchFn: typeof fetch = vi.fn(async () => {
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
});
|
|
|
|
try {
|
|
const warn = vi.fn();
|
|
const media = await downloadMSTeamsBotFrameworkAttachment({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentId: "att-1",
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000_000,
|
|
fetchFn,
|
|
fetchFnSupportsDispatcher: true,
|
|
resolveFn: resolvePublicHost,
|
|
logger: { warn },
|
|
});
|
|
|
|
expectUnavailableMedia(media, "att-1");
|
|
expect(jsonSpy).not.toHaveBeenCalled();
|
|
// Enforced well before the 64 MiB test ceiling; an unbounded reader would keep pulling.
|
|
expect(state.enqueued).toBeLessThan(32);
|
|
expect(state.canceled).toBe(true);
|
|
expect(warn).toHaveBeenCalledWith(
|
|
"msteams botFramework attachmentInfo parse failed",
|
|
expect.objectContaining({
|
|
error: expect.stringMatching(/JSON response exceeds 16777216 bytes/),
|
|
}),
|
|
);
|
|
} finally {
|
|
jsonSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("downloadMSTeamsBotFrameworkAttachments", () => {
|
|
beforeEach(() => {
|
|
installRuntime();
|
|
});
|
|
|
|
it("fetches every unique attachment id and returns combined media", async () => {
|
|
const mkInfo = (viewId: string) => ({
|
|
name: `file-${viewId}.pdf`,
|
|
type: "application/pdf",
|
|
views: [{ viewId, size: 10 }],
|
|
});
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/att-1$/,
|
|
response: new Response(JSON.stringify(mkInfo("original")), { status: 200 }),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/att-1\/views\/original$/,
|
|
response: new Response(Buffer.from("A"), { status: 200 }),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/att-2$/,
|
|
response: new Response(JSON.stringify(mkInfo("original")), { status: 200 }),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/att-2\/views\/original$/,
|
|
response: new Response(Buffer.from("B"), { status: 200 }),
|
|
},
|
|
]);
|
|
|
|
const result = await downloadMSTeamsBotFrameworkAttachments({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentIds: ["att-1", "att-2", "att-1"],
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000,
|
|
fetchFn,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expect(result.media).toHaveLength(2);
|
|
expect(result.media.map((media) => media.sourceId)).toEqual(["att-1", "att-2"]);
|
|
expect(result.attachmentCount).toBe(2);
|
|
});
|
|
|
|
it("returns empty when no valid attachment ids", async () => {
|
|
const result = await downloadMSTeamsBotFrameworkAttachments({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentIds: [],
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000,
|
|
fetchFn: vi.fn() as unknown as typeof fetch,
|
|
});
|
|
expect(result.media).toStrictEqual([]);
|
|
});
|
|
|
|
it("continues past a per-attachment failure", async () => {
|
|
const fetchFn = createMockFetch([
|
|
{
|
|
match: /\/v3\/attachments\/ok$/,
|
|
response: new Response(
|
|
JSON.stringify({
|
|
name: "ok.pdf",
|
|
type: "application/pdf",
|
|
views: [{ viewId: "original", size: 1 }],
|
|
}),
|
|
{ status: 200 },
|
|
),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/ok\/views\/original$/,
|
|
response: new Response(Buffer.from("OK"), { status: 200 }),
|
|
},
|
|
{
|
|
match: /\/v3\/attachments\/bad$/,
|
|
response: new Response("nope", { status: 500 }),
|
|
},
|
|
]);
|
|
|
|
const result = await downloadMSTeamsBotFrameworkAttachments({
|
|
serviceUrl: "https://smba.trafficmanager.net/amer",
|
|
attachmentIds: ["bad", "ok"],
|
|
tokenProvider: buildTokenProvider(),
|
|
maxBytes: 10_000,
|
|
fetchFn,
|
|
resolveFn: resolvePublicHost,
|
|
});
|
|
|
|
expect(result.media).toEqual([
|
|
{ kind: "document", sourceId: "bad" },
|
|
expect.objectContaining({ path: expect.any(String), sourceId: "ok" }),
|
|
]);
|
|
expect(result.attachmentCount).toBe(2);
|
|
});
|
|
});
|