More email tests.
This commit is contained in:
parent
8df8494b16
commit
7443cd4598
|
@ -11,7 +11,6 @@ export enum EmailTemplatePurpose {
|
|||
}
|
||||
|
||||
export interface SendEmailRequest {
|
||||
workspaceId?: string
|
||||
email: string
|
||||
userId?: string
|
||||
purpose: EmailTemplatePurpose
|
||||
|
|
|
@ -11,7 +11,6 @@ export async function sendEmail(
|
|||
ctx: UserCtx<SendEmailRequest, SendEmailResponse>
|
||||
) {
|
||||
let {
|
||||
workspaceId,
|
||||
email,
|
||||
userId,
|
||||
purpose,
|
||||
|
@ -33,7 +32,6 @@ export async function sendEmail(
|
|||
}
|
||||
}
|
||||
const response = await sendEmailFn(email, purpose, {
|
||||
workspaceId,
|
||||
user,
|
||||
contents,
|
||||
from,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EmailTemplatePurpose } from "@budibase/types"
|
||||
import { EmailTemplatePurpose, SendEmailRequest } from "@budibase/types"
|
||||
import { TestConfiguration } from "../../../../tests"
|
||||
import {
|
||||
captureEmail,
|
||||
|
@ -28,33 +28,63 @@ describe("/api/global/email", () => {
|
|||
await deleteAllEmail(mailserver)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
purpose: EmailTemplatePurpose.WELCOME,
|
||||
expectedText: `Thanks for getting started with Budibase's Budibase platform.`,
|
||||
},
|
||||
{
|
||||
purpose: EmailTemplatePurpose.INVITATION,
|
||||
expectedText: `Use the button below to set up your account and get started:`,
|
||||
},
|
||||
{
|
||||
purpose: EmailTemplatePurpose.PASSWORD_RECOVERY,
|
||||
expectedText: `You recently requested to reset your password for your Budibase account in your Budibase platform`,
|
||||
},
|
||||
])("can send $purpose emails", async ({ purpose, expectedText }) => {
|
||||
const email = await captureEmail(mailserver, async () => {
|
||||
const res = await config.api.emails.sendEmail({
|
||||
email: "foo@example.com",
|
||||
subject: "Test",
|
||||
userId: config.user!._id,
|
||||
purpose,
|
||||
})
|
||||
expect(res.message).toBeDefined()
|
||||
})
|
||||
interface TestCase {
|
||||
req: Partial<SendEmailRequest>
|
||||
expectedStatus?: number
|
||||
expectedContents?: string
|
||||
}
|
||||
|
||||
expect(email.html).toContain(expectedText)
|
||||
expect(email.html).not.toContain("Invalid binding")
|
||||
})
|
||||
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"
|
||||
|
@ -77,7 +107,7 @@ describe("/api/global/email", () => {
|
|||
|
||||
const email = await captureEmail(mailserver, async () => {
|
||||
const res = await config.api.emails.sendEmail({
|
||||
email: "foo@example.com",
|
||||
email: "to@example.com",
|
||||
subject: "Test",
|
||||
userId: config.user!._id,
|
||||
purpose: EmailTemplatePurpose.WELCOME,
|
||||
|
@ -94,4 +124,146 @@ describe("/api/global/email", () => {
|
|||
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 () => {
|
||||
const res = 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 () => {
|
||||
const res = 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)}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,13 +6,16 @@ import {
|
|||
import { TestAPI } from "./base"
|
||||
|
||||
export class EmailAPI extends TestAPI {
|
||||
sendEmail = async (req: SendEmailRequest): Promise<SendEmailResponse> => {
|
||||
sendEmail = async (
|
||||
req: SendEmailRequest,
|
||||
expectations?: { status?: number }
|
||||
): Promise<SendEmailResponse> => {
|
||||
const res = await this.request
|
||||
.post(`/api/global/email/send`)
|
||||
.send(req)
|
||||
.set(this.config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
.expect(expectations?.status || 200)
|
||||
|
||||
return res.body as SendEmailResponse
|
||||
}
|
||||
|
|
|
@ -22,6 +22,14 @@ export interface Address {
|
|||
name?: string
|
||||
}
|
||||
|
||||
export interface Alternative {
|
||||
contentType: string
|
||||
content: string
|
||||
charset: string
|
||||
method: string
|
||||
transferEncoding: string
|
||||
}
|
||||
|
||||
export interface Envelope {
|
||||
from: Address
|
||||
to: Address[]
|
||||
|
@ -31,7 +39,9 @@ export interface Envelope {
|
|||
|
||||
export interface Email {
|
||||
attachments: Attachment[]
|
||||
calculatedBcc: string[]
|
||||
alternatives: Alternative[]
|
||||
calculatedBcc: Address[]
|
||||
cc: Address[]
|
||||
date: string
|
||||
envelope: Envelope
|
||||
from: Address[]
|
||||
|
@ -62,11 +72,18 @@ export function getUnusedPort(): Promise<number> {
|
|||
})
|
||||
}
|
||||
|
||||
export function waitForEmail(
|
||||
export async function captureEmail(
|
||||
mailserver: Mailserver,
|
||||
timeoutMs = 5000
|
||||
f: () => Promise<void>
|
||||
): Promise<Email> {
|
||||
let timeout: ReturnType<typeof setTimeout>
|
||||
const timeoutMs = 5000
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
const cancel = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = undefined
|
||||
}
|
||||
}
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
reject(new Error("Timed out waiting for email"))
|
||||
|
@ -76,18 +93,15 @@ export function waitForEmail(
|
|||
// @ts-expect-error - types are wrong
|
||||
mailserver.once("new", email => {
|
||||
resolve(email as Email)
|
||||
clearTimeout(timeout)
|
||||
cancel()
|
||||
})
|
||||
})
|
||||
return Promise.race([mailPromise, timeoutPromise])
|
||||
}
|
||||
|
||||
export async function captureEmail(
|
||||
mailserver: Mailserver,
|
||||
f: () => Promise<void>
|
||||
): Promise<Email> {
|
||||
const emailPromise = waitForEmail(mailserver)
|
||||
await f()
|
||||
const emailPromise = Promise.race([mailPromise, timeoutPromise])
|
||||
try {
|
||||
await f()
|
||||
} finally {
|
||||
cancel()
|
||||
}
|
||||
return await emailPromise
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
EmailTemplatePurpose,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
import { configs, cache, objectStore } from "@budibase/backend-core"
|
||||
import { configs, cache, objectStore, HTTPError } from "@budibase/backend-core"
|
||||
import ical from "ical-generator"
|
||||
import _ from "lodash"
|
||||
|
||||
|
@ -147,7 +147,7 @@ export async function sendEmail(
|
|||
switch (purpose) {
|
||||
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
||||
if (!opts.user || !opts.user._id) {
|
||||
throw "User must be provided for password recovery."
|
||||
throw new HTTPError("User must be provided for password recovery.", 400)
|
||||
}
|
||||
code = await cache.passwordReset.createCode(opts.user._id, opts.info)
|
||||
break
|
||||
|
|
Loading…
Reference in New Issue