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