Skip to content

Commit 08ba312

Browse files
authored
Merge pull request #386 from recoupable/test
Merge test → main
2 parents 4afc4cf + 54667b3 commit 08ba312

File tree

10 files changed

+625
-2
lines changed

10 files changed

+625
-2
lines changed
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
import { extractMessageAttachments } from "../extractMessageAttachments";
3+
4+
vi.mock("@vercel/blob", () => ({
5+
put: vi.fn(),
6+
}));
7+
8+
const { put } = await import("@vercel/blob");
9+
10+
describe("extractMessageAttachments", () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks();
13+
});
14+
15+
it("returns null values when message has no attachments", async () => {
16+
const message = { text: "hello", attachments: [] };
17+
const result = await extractMessageAttachments(message as never);
18+
expect(result).toEqual({ songUrl: null, imageUrl: null });
19+
});
20+
21+
it("returns null values when attachments is undefined", async () => {
22+
const message = { text: "hello" };
23+
const result = await extractMessageAttachments(message as never);
24+
expect(result).toEqual({ songUrl: null, imageUrl: null });
25+
});
26+
27+
it("uploads audio with correct contentType", async () => {
28+
const audioBuffer = Buffer.from("fake-audio-data");
29+
const message = {
30+
text: "hello",
31+
attachments: [
32+
{
33+
type: "audio",
34+
name: "my-song.mp3",
35+
fetchData: vi.fn().mockResolvedValue(audioBuffer),
36+
},
37+
],
38+
};
39+
vi.mocked(put).mockResolvedValue({
40+
url: "https://blob.vercel-storage.com/content-attachments/my-song.mp3",
41+
} as never);
42+
43+
const result = await extractMessageAttachments(message as never);
44+
45+
expect(message.attachments[0].fetchData).toHaveBeenCalled();
46+
expect(put).toHaveBeenCalledWith(expect.stringContaining("my-song.mp3"), audioBuffer, {
47+
access: "public",
48+
contentType: "audio/mpeg",
49+
});
50+
expect(result.songUrl).toBe("https://blob.vercel-storage.com/content-attachments/my-song.mp3");
51+
expect(result.imageUrl).toBeNull();
52+
});
53+
54+
it("extracts and uploads an image attachment", async () => {
55+
const imageBuffer = Buffer.from("fake-image-data");
56+
const message = {
57+
text: "hello",
58+
attachments: [
59+
{
60+
type: "image",
61+
name: "face.png",
62+
fetchData: vi.fn().mockResolvedValue(imageBuffer),
63+
},
64+
],
65+
};
66+
vi.mocked(put).mockResolvedValue({
67+
url: "https://blob.vercel-storage.com/content-attachments/face.png",
68+
} as never);
69+
70+
const result = await extractMessageAttachments(message as never);
71+
72+
expect(result.imageUrl).toBe("https://blob.vercel-storage.com/content-attachments/face.png");
73+
expect(result.songUrl).toBeNull();
74+
});
75+
76+
it("extracts both audio and image when both are attached", async () => {
77+
const audioBuffer = Buffer.from("fake-audio");
78+
const imageBuffer = Buffer.from("fake-image");
79+
const message = {
80+
text: "hello",
81+
attachments: [
82+
{
83+
type: "audio",
84+
name: "song.mp3",
85+
fetchData: vi.fn().mockResolvedValue(audioBuffer),
86+
},
87+
{
88+
type: "image",
89+
name: "photo.jpg",
90+
fetchData: vi.fn().mockResolvedValue(imageBuffer),
91+
},
92+
],
93+
};
94+
vi.mocked(put).mockResolvedValueOnce({
95+
url: "https://blob.vercel-storage.com/song.mp3",
96+
} as never);
97+
vi.mocked(put).mockResolvedValueOnce({
98+
url: "https://blob.vercel-storage.com/photo.jpg",
99+
} as never);
100+
101+
const result = await extractMessageAttachments(message as never);
102+
103+
expect(result.songUrl).toBe("https://blob.vercel-storage.com/song.mp3");
104+
expect(result.imageUrl).toBe("https://blob.vercel-storage.com/photo.jpg");
105+
});
106+
107+
it("uses attachment data buffer if fetchData is not available", async () => {
108+
const audioBuffer = Buffer.from("inline-audio");
109+
const message = {
110+
text: "hello",
111+
attachments: [
112+
{
113+
type: "audio",
114+
name: "inline.mp3",
115+
data: audioBuffer,
116+
},
117+
],
118+
};
119+
vi.mocked(put).mockResolvedValue({
120+
url: "https://blob.vercel-storage.com/inline.mp3",
121+
} as never);
122+
123+
const result = await extractMessageAttachments(message as never);
124+
125+
expect(put).toHaveBeenCalledWith(expect.stringContaining("inline.mp3"), audioBuffer, {
126+
access: "public",
127+
contentType: "audio/mpeg",
128+
});
129+
expect(result.songUrl).toBe("https://blob.vercel-storage.com/inline.mp3");
130+
});
131+
132+
it("uses first audio and first image when multiple of same type exist", async () => {
133+
const audio1 = Buffer.from("audio1");
134+
const audio2 = Buffer.from("audio2");
135+
const message = {
136+
text: "hello",
137+
attachments: [
138+
{ type: "audio", name: "first.mp3", fetchData: vi.fn().mockResolvedValue(audio1) },
139+
{ type: "audio", name: "second.mp3", fetchData: vi.fn().mockResolvedValue(audio2) },
140+
],
141+
};
142+
vi.mocked(put).mockResolvedValue({
143+
url: "https://blob.vercel-storage.com/first.mp3",
144+
} as never);
145+
146+
const result = await extractMessageAttachments(message as never);
147+
148+
expect(put).toHaveBeenCalledTimes(1);
149+
expect(result.songUrl).toBe("https://blob.vercel-storage.com/first.mp3");
150+
});
151+
152+
it("detects image from file type with image mimeType (Slack uploads)", async () => {
153+
const imageBuffer = Buffer.from("fake-image");
154+
const message = {
155+
text: "hello",
156+
attachments: [
157+
{
158+
type: "file",
159+
name: "photo.jpg",
160+
mimeType: "image/jpeg",
161+
fetchData: vi.fn().mockResolvedValue(imageBuffer),
162+
},
163+
],
164+
};
165+
vi.mocked(put).mockResolvedValue({
166+
url: "https://blob.vercel-storage.com/photo.jpg",
167+
} as never);
168+
169+
const result = await extractMessageAttachments(message as never);
170+
171+
expect(result.imageUrl).toBe("https://blob.vercel-storage.com/photo.jpg");
172+
});
173+
174+
it("detects audio from file type with audio mimeType (Slack uploads)", async () => {
175+
const audioBuffer = Buffer.from("fake-audio");
176+
const message = {
177+
text: "hello",
178+
attachments: [
179+
{
180+
type: "file",
181+
name: "song.mp3",
182+
mimeType: "audio/mpeg",
183+
fetchData: vi.fn().mockResolvedValue(audioBuffer),
184+
},
185+
],
186+
};
187+
vi.mocked(put).mockResolvedValue({
188+
url: "https://blob.vercel-storage.com/song.mp3",
189+
} as never);
190+
191+
const result = await extractMessageAttachments(message as never);
192+
193+
expect(result.songUrl).toBe("https://blob.vercel-storage.com/song.mp3");
194+
});
195+
196+
it("ignores file attachments that are not audio or image", async () => {
197+
const message = {
198+
text: "hello",
199+
attachments: [
200+
{ type: "file", name: "document.pdf", fetchData: vi.fn() },
201+
{ type: "video", name: "clip.mp4", fetchData: vi.fn() },
202+
],
203+
};
204+
205+
const result = await extractMessageAttachments(message as never);
206+
207+
expect(put).not.toHaveBeenCalled();
208+
expect(result).toEqual({ songUrl: null, imageUrl: null });
209+
});
210+
211+
it("returns null when attachment has no data and no fetchData", async () => {
212+
const message = {
213+
text: "hello",
214+
attachments: [
215+
{
216+
type: "audio",
217+
name: "empty.mp3",
218+
},
219+
],
220+
};
221+
222+
const result = await extractMessageAttachments(message as never);
223+
224+
expect(put).not.toHaveBeenCalled();
225+
expect(result.songUrl).toBeNull();
226+
});
227+
228+
it("gracefully handles upload failure without crashing", async () => {
229+
const audioBuffer = Buffer.from("fake-audio");
230+
const imageBuffer = Buffer.from("fake-image");
231+
const message = {
232+
text: "hello",
233+
attachments: [
234+
{
235+
type: "audio",
236+
name: "song.mp3",
237+
fetchData: vi.fn().mockResolvedValue(audioBuffer),
238+
},
239+
{
240+
type: "image",
241+
name: "photo.jpg",
242+
fetchData: vi.fn().mockResolvedValue(imageBuffer),
243+
},
244+
],
245+
};
246+
// First call (audio) throws, second call (image) succeeds
247+
vi.mocked(put).mockRejectedValueOnce(new Error("upload failed"));
248+
vi.mocked(put).mockResolvedValueOnce({
249+
url: "https://blob.vercel-storage.com/photo.jpg",
250+
} as never);
251+
252+
const result = await extractMessageAttachments(message as never);
253+
254+
expect(result.songUrl).toBeNull();
255+
expect(result.imageUrl).toBe("https://blob.vercel-storage.com/photo.jpg");
256+
});
257+
258+
it("falls back to generic name when attachment name is missing", async () => {
259+
const audioBuffer = Buffer.from("audio");
260+
const message = {
261+
text: "hello",
262+
attachments: [{ type: "audio", fetchData: vi.fn().mockResolvedValue(audioBuffer) }],
263+
};
264+
vi.mocked(put).mockResolvedValue({
265+
url: "https://blob.vercel-storage.com/attachment",
266+
} as never);
267+
268+
await extractMessageAttachments(message as never);
269+
270+
expect(put).toHaveBeenCalledWith(expect.stringContaining("attachment"), audioBuffer, {
271+
access: "public",
272+
contentType: "audio/mpeg",
273+
});
274+
});
275+
});

0 commit comments

Comments
 (0)