Skip to content

Commit 23c8af3

Browse files
committed
fix(test): split msteams attachment helpers
1 parent d140401 commit 23c8af3

2 files changed

Lines changed: 213 additions & 222 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { beforeEach, describe, expect, it } from "vitest";
2+
import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js";
3+
import type { PluginRuntime } from "../runtime-api.js";
4+
import {
5+
buildMSTeamsAttachmentPlaceholder,
6+
buildMSTeamsGraphMessageUrls,
7+
buildMSTeamsMediaPayload,
8+
} from "./attachments.js";
9+
import { setMSTeamsRuntime } from "./runtime.js";
10+
11+
const GRAPH_HOST = "graph.microsoft.com";
12+
const SHAREPOINT_HOST = "contoso.sharepoint.com";
13+
const TEST_HOST = "x";
14+
const createUrlForHost = (host: string, pathSegment: string) => `https://${host}/${pathSegment}`;
15+
const createTestUrl = (pathSegment: string) => createUrlForHost(TEST_HOST, pathSegment);
16+
const TEST_URL_IMAGE = createTestUrl("img");
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");
25+
const CONTENT_TYPE_IMAGE_PNG = "image/png";
26+
const CONTENT_TYPE_APPLICATION_PDF = "application/pdf";
27+
const CONTENT_TYPE_TEXT_HTML = "text/html";
28+
const CONTENT_TYPE_TEAMS_FILE_DOWNLOAD_INFO = "application/vnd.microsoft.teams.file.download.info";
29+
type AttachmentPlaceholderInput = Parameters<typeof buildMSTeamsAttachmentPlaceholder>[0];
30+
type GraphMessageUrlParams = Parameters<typeof buildMSTeamsGraphMessageUrls>[0];
31+
type MSTeamsMediaPayload = ReturnType<typeof buildMSTeamsMediaPayload>;
32+
33+
const runtimeStub: PluginRuntime = createPluginRuntimeMock();
34+
const MEDIA_PLACEHOLDER_IMAGE = "<media:image>";
35+
const MEDIA_PLACEHOLDER_DOCUMENT = "<media:document>";
36+
const formatImagePlaceholder = (count: number) =>
37+
count > 1 ? `${MEDIA_PLACEHOLDER_IMAGE} (${count} images)` : MEDIA_PLACEHOLDER_IMAGE;
38+
const formatDocumentPlaceholder = (count: number) =>
39+
count > 1 ? `${MEDIA_PLACEHOLDER_DOCUMENT} (${count} files)` : MEDIA_PLACEHOLDER_DOCUMENT;
40+
const withLabel = <T extends object>(label: string, fields: T): T & { label: string } => ({
41+
label,
42+
...fields,
43+
});
44+
const buildAttachment = <T extends Record<string, unknown>>(contentType: string, props: T) => ({
45+
contentType,
46+
...props,
47+
});
48+
const createHtmlAttachment = (content: string) =>
49+
buildAttachment(CONTENT_TYPE_TEXT_HTML, { content });
50+
const buildHtmlImageTag = (src: string) => `<img src="${src}" />`;
51+
const createHtmlImageAttachments = (sources: string[], prefix = "") => [
52+
createHtmlAttachment(`${prefix}${sources.map(buildHtmlImageTag).join("")}`),
53+
];
54+
const createContentUrlAttachments = (contentType: string, ...contentUrls: string[]) =>
55+
contentUrls.map((contentUrl) => buildAttachment(contentType, { contentUrl }));
56+
const createImageAttachments = (...contentUrls: string[]) =>
57+
createContentUrlAttachments(CONTENT_TYPE_IMAGE_PNG, ...contentUrls);
58+
const createPdfAttachments = (...contentUrls: string[]) =>
59+
createContentUrlAttachments(CONTENT_TYPE_APPLICATION_PDF, ...contentUrls);
60+
const createTeamsFileDownloadInfoAttachments = (
61+
downloadUrl = createTestUrl("dl"),
62+
fileType = "png",
63+
) => [
64+
buildAttachment(CONTENT_TYPE_TEAMS_FILE_DOWNLOAD_INFO, {
65+
content: { downloadUrl, fileType },
66+
}),
67+
];
68+
const createMediaEntriesWithType = (contentType: string, ...paths: string[]) =>
69+
paths.map((path) => ({ path, contentType }));
70+
const createImageMediaEntries = (...paths: string[]) =>
71+
createMediaEntriesWithType(CONTENT_TYPE_IMAGE_PNG, ...paths);
72+
const DEFAULT_CHANNEL_TEAM_ID = "team-id";
73+
const DEFAULT_CHANNEL_ID = "chan-id";
74+
const createChannelGraphMessageUrlParams = (params: {
75+
messageId: string;
76+
replyToId?: string;
77+
conversationId?: string;
78+
}) => ({
79+
conversationType: "channel" as const,
80+
...params,
81+
channelData: {
82+
team: { id: DEFAULT_CHANNEL_TEAM_ID },
83+
channel: { id: DEFAULT_CHANNEL_ID },
84+
},
85+
});
86+
const buildExpectedChannelMessagePath = (params: { messageId: string; replyToId?: string }) =>
87+
params.replyToId
88+
? `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.replyToId}/replies/${params.messageId}`
89+
: `/teams/${DEFAULT_CHANNEL_TEAM_ID}/channels/${DEFAULT_CHANNEL_ID}/messages/${params.messageId}`;
90+
91+
const expectMSTeamsMediaPayload = (
92+
payload: MSTeamsMediaPayload,
93+
expected: { firstPath: string; paths: string[]; types: string[] },
94+
) => {
95+
expect(payload.MediaPath).toBe(expected.firstPath);
96+
expect(payload.MediaUrl).toBe(expected.firstPath);
97+
expect(payload.MediaPaths).toEqual(expected.paths);
98+
expect(payload.MediaUrls).toEqual(expected.paths);
99+
expect(payload.MediaTypes).toEqual(expected.types);
100+
};
101+
102+
const ATTACHMENT_PLACEHOLDER_CASES = [
103+
withLabel("returns empty string when no attachments", {
104+
attachments: undefined as AttachmentPlaceholderInput,
105+
expected: "",
106+
}),
107+
withLabel("returns empty string when attachments are empty", {
108+
attachments: [],
109+
expected: "",
110+
}),
111+
withLabel("returns image placeholder for one image attachment", {
112+
attachments: createImageAttachments(TEST_URL_IMAGE_PNG),
113+
expected: formatImagePlaceholder(1),
114+
}),
115+
withLabel("returns image placeholder with count for many image attachments", {
116+
attachments: [
117+
...createImageAttachments(TEST_URL_IMAGE_1_PNG),
118+
{ contentType: "image/jpeg", contentUrl: TEST_URL_IMAGE_2_JPG },
119+
],
120+
expected: formatImagePlaceholder(2),
121+
}),
122+
withLabel("treats Teams file.download.info image attachments as images", {
123+
attachments: createTeamsFileDownloadInfoAttachments(),
124+
expected: formatImagePlaceholder(1),
125+
}),
126+
withLabel("returns document placeholder for non-image attachments", {
127+
attachments: createPdfAttachments(TEST_URL_PDF),
128+
expected: formatDocumentPlaceholder(1),
129+
}),
130+
withLabel("returns document placeholder with count for many non-image attachments", {
131+
attachments: createPdfAttachments(TEST_URL_PDF_1, TEST_URL_PDF_2),
132+
expected: formatDocumentPlaceholder(2),
133+
}),
134+
withLabel("counts one inline image in html attachments", {
135+
attachments: createHtmlImageAttachments([TEST_URL_HTML_A], "<p>hi</p>"),
136+
expected: formatImagePlaceholder(1),
137+
}),
138+
withLabel("counts many inline images in html attachments", {
139+
attachments: createHtmlImageAttachments([TEST_URL_HTML_A, TEST_URL_HTML_B]),
140+
expected: formatImagePlaceholder(2),
141+
}),
142+
];
143+
144+
const GRAPH_URL_EXPECTATION_CASES = [
145+
withLabel("builds channel message urls", {
146+
params: createChannelGraphMessageUrlParams({
147+
conversationId: "19:thread@thread.tacv2",
148+
messageId: "123",
149+
}),
150+
expectedPath: buildExpectedChannelMessagePath({ messageId: "123" }),
151+
}),
152+
withLabel("builds channel reply urls when replyToId is present", {
153+
params: createChannelGraphMessageUrlParams({
154+
messageId: "reply-id",
155+
replyToId: "root-id",
156+
}),
157+
expectedPath: buildExpectedChannelMessagePath({
158+
messageId: "reply-id",
159+
replyToId: "root-id",
160+
}),
161+
}),
162+
withLabel("builds chat message urls", {
163+
params: {
164+
conversationType: "groupChat" as const,
165+
conversationId: "19:chat@thread.v2",
166+
messageId: "456",
167+
} satisfies GraphMessageUrlParams,
168+
expectedPath: "/chats/19%3Achat%40thread.v2/messages/456",
169+
}),
170+
];
171+
172+
describe("msteams attachment helpers", () => {
173+
beforeEach(() => {
174+
setMSTeamsRuntime(runtimeStub);
175+
});
176+
177+
describe("buildMSTeamsAttachmentPlaceholder", () => {
178+
it.each(ATTACHMENT_PLACEHOLDER_CASES)("$label", ({ attachments, expected }) => {
179+
expect(buildMSTeamsAttachmentPlaceholder(attachments)).toBe(expected);
180+
});
181+
});
182+
183+
describe("buildMSTeamsGraphMessageUrls", () => {
184+
it.each(GRAPH_URL_EXPECTATION_CASES)("$label", ({ params, expectedPath }) => {
185+
const urls = buildMSTeamsGraphMessageUrls(params);
186+
expect(urls[0]).toContain(expectedPath);
187+
});
188+
});
189+
190+
describe("buildMSTeamsMediaPayload", () => {
191+
it("returns single and multi-file fields", () => {
192+
const payload = buildMSTeamsMediaPayload(createImageMediaEntries("/tmp/a.png", "/tmp/b.png"));
193+
expectMSTeamsMediaPayload(payload, {
194+
firstPath: "/tmp/a.png",
195+
paths: ["/tmp/a.png", "/tmp/b.png"],
196+
types: [CONTENT_TYPE_IMAGE_PNG, CONTENT_TYPE_IMAGE_PNG],
197+
});
198+
});
199+
});
200+
201+
it("retains the expected sharepoint host fixture", () => {
202+
expect(SHAREPOINT_HOST).toBe("contoso.sharepoint.com");
203+
expect(TEST_URL_IMAGE).toContain(TEST_HOST);
204+
});
205+
});

0 commit comments

Comments
 (0)