Files
openclaw/extensions/msteams/src/attachments/bot-framework.test.ts
Peter Steinberger 6c29fc154f test(msteams): avoid hanging response clones (#118096)
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
2026-08-02 11:04:38 -07:00

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);
});
});