import handler, { IMailRequest, IRenderRequest } from "."; import { server } from "../mocks/server"; import { rest } from "msw"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { IEmailIntegrationConfig, invalidConfigs } from "./testHelpers"; const validEmailRequest = { to: "someone@test.com", from: "somebody@test.com", subject: "Test Subject", cc: "cc@test.com", bcc: "bcc@test.com", attachments: [ { filename: "test.pdf", content: "base64-encoded-string", type: "pdf" }, ], }; const validEmailRequestBody = { ...validEmailRequest, parameters: { name: "Alexander Hamilton", }, }; let renderRequest: IRenderRequest | undefined; let sendRequest: IMailRequest | undefined; describe("Send email handler", () => { const OLD_ENV = process.env; beforeEach(() => { renderRequest = undefined; sendRequest = undefined; vi.resetModules(); process.env = { ...OLD_ENV, SITE_ID: "testSiteId", }; server.use( rest.post( "https://netlify-integration-emails.netlify.app/.netlify/functions/send", async (req, res, ctx) => { sendRequest = await req.json(); if (req.headers.get("content-type") !== "application/json") { return await res( ctx.status( 400, "Bad Request - Content-Type must be application/json" ) ); } return await res( ctx.json({ message: "Email sent successfully" }), ctx.status(200) ); } ), rest.post( "https://netlify-integration-emails.netlify.app/.netlify/functions/render", async (req, res, ctx) => { if (req.headers.get("content-type") !== "application/json") { return await res( ctx.status( 400, "Bad Request - Content-Type must be application/json" ) ); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion renderRequest = (await req.json()) as IRenderRequest; if ( (renderRequest.template, (renderRequest.type === "html" || renderRequest.type === "mjml") && !renderRequest.showParameterDictionary) ) { // If request.parameters has no keys if (Object.keys(renderRequest.parameters).length === 0) { return await res( ctx.json({ html: "

Hello:

", parameterDictionary: { name: "", people: [{ this: "" }] }, }) ); } // Replace {{name}} with Alex as the render endpoint would const html = renderRequest.template.replace( "{{name}}", renderRequest.parameters.name as string ); return await res( ctx.json({ html, }) ); } return await res( ctx.status( 500, "Internal Server Error - Check all parameters were sent" ) ); } ) ); }); afterEach(() => { process.env = OLD_ENV; }); it("should return 200 when valid email request sent", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(200); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.text()).toContain("Email sent successfully"); expect(sendRequest).toEqual({ request: { to: validEmailRequestBody.to, from: validEmailRequestBody.from, subject: validEmailRequestBody.subject, cc: validEmailRequestBody.cc, bcc: validEmailRequestBody.bcc, attachments: validEmailRequestBody.attachments, html: expect.any(String), }, configuration: { apiKey: "some-key", providerName: "mailgun", mailgunDomain: "test.com", mailgunHostRegion: "us", }, }); expect(sendRequest?.request.html.replace(/\s+/g, "").trim()).toEqual( "

AlexanderHamilton

Thisistheconfirmemail

" ); expect(renderRequest).toEqual({ siteId: "testSiteId", template: expect.any(String), parameters: validEmailRequestBody.parameters, type: "html", showParameterDictionary: false, }); expect(renderRequest?.template.replace(/\s+/g, "").trim()).toEqual( "

{{name}}

Thisistheconfirmemail

" ); }); it.each(invalidConfigs)( "should return 400 when invalid config sent with: %s", async (_, config, missingProperty) => { const secret = "super-secret"; const invalidConfig = config as IEmailIntegrationConfig; process.env = { ...process.env, ...invalidConfig }; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); const responseBody = (await response.json()) as { message: string; }; expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain( "application/json" ); expect(responseBody.message).toEqual( `The emails integration is not configured correctly. We have detected the following configuration is missing: ${ missingProperty as string }` ); } ); it("should return 400 with list of missing configs", async () => { const secret = "super-secret"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); const responseBody = (await response.json()) as { message: string; }; expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(responseBody.message).toEqual( "The emails integration is not configured correctly. We have detected the following configuration is missing: NETLIFY_EMAILS_PROVIDER, NETLIFY_EMAILS_PROVIDER_API_KEY, NETLIFY_EMAILS_SECRET" ); }); it("should reject request when secret not set", async () => { process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": "super-secret" }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "The emails integration is not configured correctly. We have detected the following configuration is missing: NETLIFY_EMAILS_SECRET", }); }); it("should reject request when secret does not match", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": "wrong-secret" }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(403); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Secret does not match", }); }); it("should reject request when secret set is empty", async () => { const secret = ""; process.env.NETLIFY_EMAILS_SECRET = ""; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "The emails integration is not configured correctly. We have detected the following configuration is missing: NETLIFY_EMAILS_SECRET", }); }); it("should reject request when from address is not set", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const emailRequestBody = { ...validEmailRequestBody, from: "", }; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(emailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "From address is required", }); }); it("should reject request when to address is not set", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const emailRequestBody = { ...validEmailRequestBody, to: "", }; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(emailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "To address is required", }); }); it("should reject request when email path is not specificed", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "You have not specified the email you wish to send in the URL", }); }); it("should reject request when no request body provided", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "sendgrid"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Request body required", }); }); it("should reject request when directory does not exist", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails/does-not-exist"; process.env.NETLIFY_EMAILS_PROVIDER = "sendgrid"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(404); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Email templates directory ./fixtures/emails/does-not-exist does not exist", }); }); it("should reject request when email path does not exist", async () => { const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER = "sendgrid"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; const response = await handler( new Request( "http://localhost:8888/.netlify/functions/emails/dibble-dobble", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), } ) ); expect(response.status).toEqual(404); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Email path ./fixtures/emails/dibble-dobble does not exist", }); }); it("should handle error response from render endpoint", async () => { server.use( rest.post( "https://netlify-integration-emails.netlify.app/.netlify/functions/render", (req, res, ctx) => { return res(ctx.status(418), ctx.json({ error: "I'm a teapot" })); } ) ); const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(418); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Error rendering email template: I'm a teapot", }); }); it("should handle error response from send endpoint", async () => { server.use( rest.post( "https://netlify-integration-emails.netlify.app/.netlify/functions/send", (req, res, ctx) => { return res(ctx.status(400), ctx.json({ message: "Mailgun says no" })); } ) ); const secret = "super-secret"; process.env.NETLIFY_EMAILS_SECRET = secret; process.env.NETLIFY_EMAILS_DIRECTORY = "./fixtures/emails"; process.env.NETLIFY_EMAILS_PROVIDER_API_KEY = "some-key"; process.env.NETLIFY_EMAILS_PROVIDER = "mailgun"; process.env.NETLIFY_EMAILS_MAILGUN_DOMAIN = "test.com"; process.env.NETLIFY_EMAILS_MAILGUN_HOST_REGION = "us"; const response = await handler( new Request("http://localhost:8888/.netlify/functions/emails/confirm", { method: "POST", headers: { "netlify-emails-secret": secret }, body: JSON.stringify(validEmailRequestBody), }) ); expect(response.status).toEqual(400); expect(response.headers.get("content-type")).toContain("application/json"); expect(await response.json()).toEqual({ message: "Mailgun says no", }); }); });