@budibase/worker
Version:
Budibase background service
385 lines (338 loc) • 11.7 kB
text/typescript
import { EmailTemplatePurpose, SendEmailRequest } from "@budibase/types"
import { TestConfiguration } from "../../../../tests"
import {
captureEmail,
deleteAllEmail,
getAttachments,
Mailserver,
startMailserver,
stopMailserver,
} from "../../../../tests/mocks/email"
import { objectStore } from "@budibase/backend-core"
import * as cheerio from "cheerio"
describe("/api/global/email", () => {
const config = new TestConfiguration()
let mailserver: Mailserver
beforeAll(async () => {
await config.beforeAll()
mailserver = await startMailserver(config)
})
afterAll(async () => {
await stopMailserver(mailserver)
await config.afterAll()
})
beforeEach(async () => {
await deleteAllEmail(mailserver)
})
interface TestCase {
req: Partial<SendEmailRequest>
expectedStatus?: number
expectedContents?: string
}
const testCases: TestCase[] = [
{
req: {
purpose: EmailTemplatePurpose.WELCOME,
},
expectedContents: `Thanks for getting started with Budibase's Budibase platform.`,
},
{
req: {
purpose: EmailTemplatePurpose.INVITATION,
},
expectedContents: `Use the button below to set up your account and get started:`,
},
{
req: {
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
},
expectedContents: `You recently requested to reset your password for your Budibase account in your Budibase platform`,
},
{
req: {
purpose: EmailTemplatePurpose.CUSTOM,
contents: "Hello, world!",
},
expectedContents: "Hello, world!",
},
]
it.each(testCases)(
"can send $req.purpose emails",
async ({ req, expectedContents, expectedStatus }) => {
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail(
{
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
purpose: EmailTemplatePurpose.WELCOME,
...req,
},
{
status: expectedStatus || 200,
}
)
expect(res.message).toBeDefined()
})
expect(email.html).toContain(expectedContents)
expect(email.html).not.toContain("Invalid binding")
}
)
it("should be able to send an email with an attachment", async () => {
let bucket = "testbucket"
let filename = "test.txt"
await objectStore.upload({
bucket,
filename,
body: Buffer.from("test data"),
})
let presignedUrl = await objectStore.getPresignedUrl(
bucket,
filename,
60000
)
let attachmentObject = {
url: presignedUrl,
filename,
}
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
purpose: EmailTemplatePurpose.WELCOME,
attachments: [attachmentObject],
})
expect(res.message).toBeDefined()
})
expect(email.html).toContain(
"Thanks for getting started with Budibase's Budibase platform."
)
expect(email.html).not.toContain("Invalid binding")
const attachments = await getAttachments(mailserver, email)
expect(attachments).toEqual(["test data"])
})
it("should be able to send email without a userId", async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.WELCOME,
})
expect(res.message).toBeDefined()
})
it("should fail to send a password reset email without a userId", async () => {
const res = await config.api.emails.sendEmail(
{
email: "to@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
},
{
status: 400,
}
)
expect(res.message).toBeDefined()
})
it("can cc people", async () => {
const email = await captureEmail(mailserver, async () => {
await config.api.emails.sendEmail({
email: "to@example.com",
cc: "cc@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.CUSTOM,
contents: "Hello, world!",
})
})
expect(email.cc).toEqual([{ address: "cc@example.com", name: "" }])
})
it("can bcc people", async () => {
const email = await captureEmail(mailserver, async () => {
await config.api.emails.sendEmail({
email: "to@example.com",
bcc: "bcc@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.CUSTOM,
contents: "Hello, world!",
})
})
expect(email.calculatedBcc).toEqual([
{ address: "bcc@example.com", name: "" },
])
})
it("can change the from address", async () => {
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
from: "from@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.CUSTOM,
contents: "Hello, world!",
})
expect(res.message).toBeDefined()
})
expect(email.to).toEqual([{ address: "to@example.com", name: "" }])
expect(email.from).toEqual([{ address: "from@example.com", name: "" }])
})
it("can send a calendar invite", async () => {
const startTime = new Date()
const endTime = new Date()
const email = await captureEmail(mailserver, async () => {
await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
purpose: EmailTemplatePurpose.CUSTOM,
contents: "Hello, world!",
invite: {
startTime,
endTime,
summary: "Summary",
location: "Location",
url: "http://example.com",
},
})
})
expect(email.alternatives).toEqual([
{
charset: "utf-8",
contentType: "text/calendar",
method: "REQUEST",
transferEncoding: "7bit",
content: expect.any(String),
},
])
// Reference iCal invite:
// BEGIN:VCALENDAR
// VERSION:2.0
// PRODID:-//sebbo.net//ical-generator//EN
// NAME:Invite
// X-WR-CALNAME:Invite
// BEGIN:VEVENT
// UID:2b5947b7-ec5a-4341-8d70-8d8130183f2a
// SEQUENCE:0
// DTSTAMP:20200101T000000Z
// DTSTART:20200101T000000Z
// DTEND:20200101T000000Z
// SUMMARY:Summary
// LOCATION:Location
// URL;VALUE=URI:http://example.com
// END:VEVENT
// END:VCALENDAR
expect(email.alternatives[0].content).toContain("BEGIN:VCALENDAR")
expect(email.alternatives[0].content).toContain("BEGIN:VEVENT")
expect(email.alternatives[0].content).toContain("UID:")
expect(email.alternatives[0].content).toContain("SEQUENCE:0")
expect(email.alternatives[0].content).toContain("SUMMARY:Summary")
expect(email.alternatives[0].content).toContain("LOCATION:Location")
expect(email.alternatives[0].content).toContain(
"URL;VALUE=URI:http://example.com"
)
expect(email.alternatives[0].content).toContain("END:VEVENT")
expect(email.alternatives[0].content).toContain("END:VCALENDAR")
const formatDate = (date: Date) =>
date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z"
expect(email.alternatives[0].content).toContain(
`DTSTAMP:${formatDate(startTime)}`
)
expect(email.alternatives[0].content).toContain(
`DTSTART:${formatDate(startTime)}`
)
expect(email.alternatives[0].content).toContain(
`DTEND:${formatDate(endTime)}`
)
})
it("Should parse valid markdown content from automation steps into valid HTML.", async () => {
// Basic verification that the markdown is being processed.
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
contents: `test@home.com [Call Me!](tel:1111111)`,
purpose: EmailTemplatePurpose.CUSTOM,
})
expect(res.message).toBeDefined()
})
const $ = cheerio.load(email.html)
// Verify the email body rendered
const emailBody = $("td.email-body").first()
expect(emailBody.length).toBe(1)
// Verify a valid link was generated and is queryable
const emailLink = $("a[href^='mailto:']").first()
expect(emailLink.length).toBe(1)
expect(emailLink.text()).toBe("test@home.com")
// Verify the markdown link has been built correctly
const phoneLink = $("a[href^='tel:']").first()
expect(phoneLink.length).toBe(1)
expect(phoneLink.text()).toBe("Call Me!")
expect(phoneLink.attr("href")).toBe("tel:1111111")
})
it("Should ignore invalid markdown content and return nothing", async () => {
// The only failure case for a parse with marked is 'undefined'
// It should be caught and resolve to nothing.
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
contents: undefined,
purpose: EmailTemplatePurpose.CUSTOM,
})
expect(res.message).toBeDefined()
})
const $ = cheerio.load(email.html)
const emailBody = $("td.email-body").first()
expect(emailBody.length).toBe(1)
const bodyText = emailBody.text().trim()
expect(bodyText).toBe("")
})
it("Should render a mixture of content. Plain text, markdown and HTML", async () => {
// A more involved check to ensure all content types are still respected
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
contents: `<div class="html-content"><strong>Some content</strong></div>
# A heading
- This should be list entry 1
- This should be list entry 2
Some plain text`,
purpose: EmailTemplatePurpose.CUSTOM,
})
expect(res.message).toBeDefined()
})
const $ = cheerio.load(email.html)
const emailBody = $("td.email-body").first()
expect(emailBody.length).toBe(1)
const divEle = emailBody.find("div.html-content").first()
expect(divEle.length).toBe(1)
expect(divEle.text()).toBe("Some content")
const heading = emailBody.find("h1").first()
expect(heading.length).toBe(1)
expect(heading.text()).toBe("A heading")
// Both list items rendered
const listEles = emailBody.find("ul li")
expect(listEles.length).toBe(2)
const plainText = emailBody.find("p")
expect(plainText.length).toBe(1)
expect(plainText.text()).toBe("Some plain text")
})
it("Should only parse markdown content for the CUSTOM email template used in automation steps", async () => {
const email = await captureEmail(mailserver, async () => {
const res = await config.api.emails.sendEmail({
email: "to@example.com",
subject: "Test",
userId: config.user!._id,
purpose: EmailTemplatePurpose.INVITATION,
})
expect(res.message).toBeDefined()
})
const $ = cheerio.load(email.html)
const emailBody = $("td.email-body").first()
expect(emailBody.length).toBe(1)
const heading = emailBody.find("h1").first()
expect(heading.length).toBe(1)
// The email should not be parsed as markdown.
expect(heading.text()).toBe("Hi, to@example.com!")
})
})