From 9cfe8c0664e93ff986c5dcc3038559508d3cb3aa Mon Sep 17 00:00:00 2001 From: Jesse Shawl Date: Wed, 20 Mar 2024 18:20:34 -0500 Subject: [PATCH] add attachments (#11) --- .eslintrc.js | 5 ++++- README.md | 2 +- src/handlers.js | 1 + src/handlers.spec.js | 3 ++- src/mail.js | 5 +++-- src/mail.test.js | 37 +++++++++++++++++++++++++++---------- src/params.js | 21 +++++++++++++++++---- src/params.test.js | 15 ++++++++++++--- src/test.js | 14 ++++++++++++++ 9 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/test.js diff --git a/.eslintrc.js b/.eslintrc.js index 99ac063..fef7fc3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,5 +22,8 @@ module.exports = { ecmaVersion: 'latest', sourceType: 'module', }, - rules: {}, + rules: { + 'no-console': ['error'], + 'no-unused-vars': ['error', { destructuredArrayIgnorePattern: '^_' }], + }, }; diff --git a/README.md b/README.md index b2fbdfb..f876204 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ``` curl --request POST 'http://localhost:8787/api/notify' \ - -d "to=api-demo@jesse.sh&subject=🎉" + -d "to=api-readme@jesse.sh&subject=hello&body=see attached&attachments[][type]=text/plain&attachments[][content]=aGVsbG8sIHdvcmxkIQ==&attachments[][filename]=hello.txt" ``` ## Local Development diff --git a/src/handlers.js b/src/handlers.js index 321fec6..9b3484c 100644 --- a/src/handlers.js +++ b/src/handlers.js @@ -45,6 +45,7 @@ export const handler = async ({ env, request }) => { response = await postApiNotify({ env, request }); } response = response ?? notFound(); + // eslint-disable-next-line no-console console.log(`${request.method} ${pathname} ${response.status} ${userAgent}`); return response; }; diff --git a/src/handlers.spec.js b/src/handlers.spec.js index a138e84..826ab52 100644 --- a/src/handlers.spec.js +++ b/src/handlers.spec.js @@ -1,5 +1,6 @@ import { describe, it, expect, beforeAll, vi } from 'vitest'; import { handler } from './handlers'; +import { input } from './test'; describe('handlers', () => { beforeAll(() => { @@ -33,7 +34,7 @@ describe('handlers', () => { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ to: 'example@example.com', subject: 'ok' }), + body: JSON.stringify(input), }); await handler({ request }); expect(global.fetch).toHaveBeenCalledWith( diff --git a/src/mail.js b/src/mail.js index decbd54..88fd491 100644 --- a/src/mail.js +++ b/src/mail.js @@ -1,9 +1,10 @@ -const payload = ({ subject, to }) => ({ +const payload = ({ subject, to, body, attachments }) => ({ // https://docs.sendgrid.com/api-reference/mail-send/mail-send#body + attachments, content: [ { type: 'text/plain', - value: ' ', + value: body ?? ' ', }, ], from: { diff --git a/src/mail.test.js b/src/mail.test.js index b39620f..d04af0e 100644 --- a/src/mail.test.js +++ b/src/mail.test.js @@ -1,21 +1,38 @@ -import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { input } from './test'; import { mail } from './mail'; -const originalFetch = global.fetch; +// https://docs.sendgrid.com/api-reference/mail-send/mail-send#body +const fixture = { + attachments: input.attachments, + content: [ + { + type: 'text/plain', + value: input.body, + }, + ], + from: { + email: 'app@cinotify.cc', + }, + personalizations: [ + { + subject: input.subject, + to: [ + { + email: input.to, + }, + ], + }, + ], +}; describe('mail', () => { beforeAll(() => { global.fetch = vi.fn(); }); - afterAll(() => { - global.fetch = originalFetch; - vi.clearAllMocks(); - }); it('makes a request to the sendgrid api', async () => { global.fetch.mockResolvedValueOnce({ json: () => '{}' }); - const to = 'ex@mple.com'; - const subject = 'hello'; - await mail({ to, subject }); + await mail(input); expect(global.fetch).toHaveBeenCalledWith( 'https://api.sendgrid.com/v3/mail/send', { @@ -24,7 +41,7 @@ describe('mail', () => { 'Content-Type': 'application/json', }, method: 'POST', - body: expect.stringMatching(/hello(.*)ex@mple.com/), + body: JSON.stringify(fixture), }, ); }); diff --git a/src/params.js b/src/params.js index a18c51b..759cb11 100644 --- a/src/params.js +++ b/src/params.js @@ -1,3 +1,18 @@ +export const parseUrlEncoded = (string) => { + const data = Object.fromEntries(new URLSearchParams(string)); + + // with array[][key]=value query params + for (let key in data) { + if (key.match(/\[/)) { + const [_, object, property] = key.match(/([\w]+)\[\]\[([\w]+)/); + data[object] = data[object] || [{}]; + data[object][0] = { ...data[object][0], [property]: data[key] }; + delete data[key]; + } + } + return data; +}; + /** * Parse a payload from a json string or a urlencoded string * @param {Request} request @@ -13,12 +28,10 @@ export const params = async (request) => { } if (contentType === 'application/x-www-form-urlencoded') { - data = Object.fromEntries( - new URLSearchParams((await request.text()) ?? ''), - ); + data = parseUrlEncoded(await request.text()); } } catch (e) { - data = {}; + // pass } data.errors = required.reduce( diff --git a/src/params.test.js b/src/params.test.js index e633785..de2eef1 100644 --- a/src/params.test.js +++ b/src/params.test.js @@ -1,12 +1,13 @@ import { describe, it, expect } from 'vitest'; -import { params } from './params'; +import { params, parseUrlEncoded } from './params'; +import { input, inputUrlEncoded } from './test'; describe.each(['application/json', 'application/x-www-form-urlencoded'])( 'params', (contentType) => { const body = { - to: 'example@example.com', - subject: 'hello world', + to: input.to, + subject: input.subject, }; const format = { @@ -41,3 +42,11 @@ describe.each(['application/json', 'application/x-www-form-urlencoded'])( }); }, ); + +describe('parseUrlEncoded()', () => { + it('parses a query string into an object', async () => { + expect(parseUrlEncoded(inputUrlEncoded)).toStrictEqual({ + ...input, + }); + }); +}); diff --git a/src/test.js b/src/test.js new file mode 100644 index 0000000..d1f2b02 --- /dev/null +++ b/src/test.js @@ -0,0 +1,14 @@ +export const input = { + attachments: [ + { + content: 'aGVsbG8sIHdvcmxkIQ==', + type: 'text/plain', + filename: 'hello.txt', + }, + ], + body: 'this is the body', + subject: 'hello', + to: 'ex@mple.com', +}; + +export const inputUrlEncoded = `to=${input.to}&subject=${input.subject}&body=${input.body}&attachments[][type]=text/plain&attachments[][content]=aGVsbG8sIHdvcmxkIQ==&attachments[][filename]=hello.txt`;