Skip to content

Commit 963cb7b

Browse files
committed
test(firestore-send-email): add edge case tests
1 parent 14390f9 commit 963cb7b

File tree

7 files changed

+374
-17
lines changed

7 files changed

+374
-17
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as admin from "firebase-admin";
2+
3+
export const TEST_COLLECTIONS = ["mail", "templates"] as const;
4+
5+
// Initialize Firebase Admin once for all e2e tests
6+
beforeAll(() => {
7+
if (!admin.apps.length) {
8+
admin.initializeApp({ projectId: "demo-test" });
9+
}
10+
process.env.FIRESTORE_EMULATOR_HOST = "localhost:8080";
11+
});
12+
13+
/**
14+
* Clears all documents from test collections.
15+
* Call this in beforeEach to ensure clean state between tests.
16+
*/
17+
export async function clearCollections() {
18+
const db = admin.firestore();
19+
for (const collection of TEST_COLLECTIONS) {
20+
const snapshot = await db.collection(collection).get();
21+
const batch = db.batch();
22+
snapshot.docs.forEach((doc) => batch.delete(doc.ref));
23+
await batch.commit();
24+
}
25+
}
26+
27+
/**
28+
* Gets the test email address from environment or returns default.
29+
*/
30+
export function getTestEmail() {
31+
return process.env.TEST_EMAIL || "test@example.com";
32+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* E2E tests for attachment validation edge cases.
3+
*
4+
* Tests that the extension handles various attachment formats gracefully:
5+
* - Missing attachments field
6+
* - Null attachments
7+
* - Single attachment object (should normalize to array)
8+
* - Empty objects in attachments array (should be filtered out)
9+
*
10+
* Run with: npm run test:e2e
11+
*/
12+
13+
import * as admin from "firebase-admin";
14+
import { clearCollections, getTestEmail } from "./setup";
15+
16+
const TEST_TEMPLATE = {
17+
name: "validation_test_template",
18+
subject: "Test Subject {{id}}",
19+
text: "Test content for {{id}}",
20+
html: "<p>Test content for {{id}}</p>",
21+
};
22+
23+
describe.skip("Attachment validation edge cases", () => {
24+
beforeEach(async () => {
25+
await clearCollections();
26+
27+
const db = admin.firestore();
28+
await db.collection("templates").doc(TEST_TEMPLATE.name).set({
29+
subject: TEST_TEMPLATE.subject,
30+
text: TEST_TEMPLATE.text,
31+
html: TEST_TEMPLATE.html,
32+
});
33+
});
34+
35+
test("should process template email without attachments field", async () => {
36+
const db = admin.firestore();
37+
38+
const testData = {
39+
template: {
40+
name: TEST_TEMPLATE.name,
41+
data: { id: "test-1" },
42+
},
43+
to: getTestEmail(),
44+
};
45+
46+
const docRef = db.collection("mail").doc("test-no-attachments");
47+
await docRef.set(testData);
48+
49+
await new Promise((resolve) => setTimeout(resolve, 2000));
50+
51+
const doc = await docRef.get();
52+
const updatedData = doc.data();
53+
54+
expect(updatedData?.delivery.state).toBe("SUCCESS");
55+
expect(updatedData?.delivery.error).toBeNull();
56+
});
57+
58+
test("should process template email with null message attachments", async () => {
59+
const db = admin.firestore();
60+
61+
const testData = {
62+
template: {
63+
name: TEST_TEMPLATE.name,
64+
data: { id: "test-2" },
65+
},
66+
message: {
67+
attachments: null,
68+
},
69+
to: getTestEmail(),
70+
};
71+
72+
const docRef = db
73+
.collection("emailCollection")
74+
.doc("test-null-attachments");
75+
await docRef.set(testData);
76+
77+
await new Promise((resolve) => setTimeout(resolve, 2000));
78+
79+
const doc = await docRef.get();
80+
const updatedData = doc.data();
81+
82+
expect(updatedData?.delivery.state).toBe("SUCCESS");
83+
expect(updatedData?.delivery.error).toBeNull();
84+
});
85+
86+
test("should normalize single attachment object to array", async () => {
87+
const db = admin.firestore();
88+
89+
const testData = {
90+
template: {
91+
name: TEST_TEMPLATE.name,
92+
data: { id: "test-3" },
93+
},
94+
message: {
95+
attachments: {
96+
filename: "test.txt",
97+
content: "test content",
98+
},
99+
},
100+
to: getTestEmail(),
101+
};
102+
103+
const docRef = db
104+
.collection("emailCollection")
105+
.doc("test-object-attachment");
106+
await docRef.set(testData);
107+
108+
await new Promise((resolve) => setTimeout(resolve, 2000));
109+
110+
const doc = await docRef.get();
111+
const updatedData = doc.data();
112+
113+
expect(updatedData?.delivery.state).toBe("SUCCESS");
114+
expect(updatedData?.delivery.error).toBeNull();
115+
});
116+
117+
test("should filter out empty objects in attachments array", async () => {
118+
const db = admin.firestore();
119+
120+
const testData = {
121+
template: {
122+
name: TEST_TEMPLATE.name,
123+
data: { id: "test-4" },
124+
},
125+
message: {
126+
attachments: [{}],
127+
},
128+
to: getTestEmail(),
129+
};
130+
131+
const docRef = db
132+
.collection("emailCollection")
133+
.doc("test-empty-attachment");
134+
await docRef.set(testData);
135+
136+
await new Promise((resolve) => setTimeout(resolve, 2000));
137+
138+
const doc = await docRef.get();
139+
const updatedData = doc.data();
140+
141+
expect(updatedData?.delivery.state).toBe("SUCCESS");
142+
expect(updatedData?.delivery.error).toBeNull();
143+
});
144+
});

firestore-send-email/functions/__tests__/prepare-payload.test.ts

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ class MockTemplates {
4848
text: undefined,
4949
subject: "Template Subject",
5050
};
51+
case "template-with-object-attachment":
52+
// Simulates a template that returns attachments as an object instead of array
53+
return {
54+
html: "<h1>Template HTML</h1>",
55+
subject: "Template Subject",
56+
attachments: { filename: "report.pdf" },
57+
};
58+
case "template-with-null-attachments":
59+
return {
60+
html: "<h1>Template HTML</h1>",
61+
subject: "Template Subject",
62+
attachments: null,
63+
};
5164
default:
5265
return {};
5366
}
@@ -351,18 +364,13 @@ describe("preparePayload Template Merging", () => {
351364
expect(result.message.subject).toBe("Template Subject");
352365
});
353366

354-
it("should handle incorrectly formatted attachments object", async () => {
367+
it("should filter out empty attachment objects with only null values", async () => {
355368
const payload = {
356-
to: "tester@gmx.at",
369+
to: "test@example.com",
357370
template: {
358-
name: "med_order_reply_greimel",
371+
name: "html-only-template",
359372
data: {
360-
address: "Halbenrain 140 Graz",
361-
doctorName: "Dr. Andreas",
362-
openingHours: "Mo., Mi., Fr. 8:00-12:00Di., Do. 10:30-15:30",
363-
orderText: "Some stuff i need",
364-
userName: "Pfeiler ",
365-
name: "med_order_reply_greimel",
373+
name: "Test User",
366374
},
367375
},
368376
message: {
@@ -372,20 +380,20 @@ describe("preparePayload Template Merging", () => {
372380
text: null,
373381
},
374382
],
375-
subject: "Bestellbestätigung",
383+
subject: "Test Subject",
376384
},
377385
};
378386

379387
const result = await preparePayload(payload);
380388

381-
// Should convert attachments to an empty array since the format is incorrect
389+
// Empty attachment objects should be filtered out
382390
expect(result.message.attachments).toEqual([]);
383-
expect(result.message.subject).toBe("Bestellbestätigung");
384-
expect(result.to).toEqual(["tester@gmx.at"]);
391+
expect(result.message.subject).toBe("Template Subject");
392+
expect(result.to).toEqual(["test@example.com"]);
385393
});
386394

387395
describe("attachment validation", () => {
388-
it("should handle non-array attachments", async () => {
396+
it("should throw clear error for string attachments", async () => {
389397
const payload = {
390398
to: "test@example.com",
391399
message: {
@@ -395,7 +403,30 @@ describe("preparePayload Template Merging", () => {
395403
},
396404
};
397405

398-
await expect(preparePayload(payload)).rejects.toThrow();
406+
await expect(preparePayload(payload)).rejects.toThrow(
407+
"Invalid message configuration: Field 'message.attachments' must be an array"
408+
);
409+
});
410+
411+
it("should throw clear error for invalid attachment httpHeaders", async () => {
412+
const payload = {
413+
to: "test@example.com",
414+
message: {
415+
subject: "Test Subject",
416+
text: "Test text",
417+
attachments: [
418+
{
419+
filename: "test.txt",
420+
href: "https://example.com",
421+
httpHeaders: "invalid",
422+
},
423+
],
424+
},
425+
};
426+
427+
await expect(preparePayload(payload)).rejects.toThrow(
428+
"Invalid message configuration: Field 'message.attachments.0.httpHeaders' must be a map"
429+
);
399430
});
400431

401432
it("should handle null attachments as no attachments", async () => {
@@ -456,4 +487,51 @@ describe("preparePayload Template Merging", () => {
456487
expect(result.message.attachments).toEqual([]);
457488
});
458489
});
490+
491+
describe("template-rendered attachments", () => {
492+
it("should normalize template-returned attachment object to array", async () => {
493+
// This tests the exact scenario from issue #2550 where a template
494+
// returns attachments as an object instead of an array
495+
const payload = {
496+
to: "test@example.com",
497+
template: {
498+
name: "template-with-object-attachment",
499+
data: {},
500+
},
501+
};
502+
503+
const result = await preparePayload(payload);
504+
expect(result.message.attachments).toEqual([{ filename: "report.pdf" }]);
505+
});
506+
507+
it("should handle template-returned null attachments", async () => {
508+
const payload = {
509+
to: "test@example.com",
510+
template: {
511+
name: "template-with-null-attachments",
512+
data: {},
513+
},
514+
};
515+
516+
const result = await preparePayload(payload);
517+
expect(result.message.attachments).toEqual([]);
518+
});
519+
520+
it("should process template-only payload without message field", async () => {
521+
// Matches the user's payload structure - template only, no message field
522+
const payload = {
523+
to: "test@example.com",
524+
template: {
525+
name: "html-only-template",
526+
data: {
527+
someField: "value",
528+
},
529+
},
530+
};
531+
532+
const result = await preparePayload(payload);
533+
expect(result.message.html).toBe("<h1>Template HTML</h1>");
534+
expect(result.message.subject).toBe("Template Subject");
535+
});
536+
});
459537
});

0 commit comments

Comments
 (0)