From 76a9fac50311a95ae0ef8e1c86aa55020b83aec6 Mon Sep 17 00:00:00 2001 From: isaacisrael Date: Sun, 24 Aug 2025 15:59:01 +0000 Subject: [PATCH 1/2] feat: add `mailcather` to local services --- infra/compose.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/infra/compose.yaml b/infra/compose.yaml index aecd776..95955ba 100644 --- a/infra/compose.yaml +++ b/infra/compose.yaml @@ -6,3 +6,9 @@ services: - ../.env.development ports: - "5432:5432" + mailcatcher: + container_name: "mailcatcher-dev" + image: "sj26/mailcatcher" + ports: + - "1025:1025" + - "1080:1080" From 186e5ce359c9b262f1c717f503ae7fb13502a92e Mon Sep 17 00:00:00 2001 From: isaacisrael Date: Sun, 7 Sep 2025 06:34:32 +0000 Subject: [PATCH 2/2] feat: add `email.js` infra module --- .env.development | 8 +++++- infra/email.js | 21 ++++++++++++++ package-lock.json | 10 +++++++ package.json | 1 + test/integration/infra/email.test.js | 32 ++++++++++++++++++++++ test/orchestrator.js | 41 ++++++++++++++++++++++++++-- 6 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 infra/email.js create mode 100644 test/integration/infra/email.test.js diff --git a/.env.development b/.env.development index f03dd8e..455f306 100644 --- a/.env.development +++ b/.env.development @@ -4,4 +4,10 @@ POSTGRES_USER=local_postgres POSTGRES_DB=local_db POSTGRES_PASSWORD=local_password DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB -PEPPER=a!GB7 \ No newline at end of file +PEPPER=a!GB7 +EMAIL_SMTP_HOST=localhost +EMAIL_SMTP_PORT=1025 +EMAIL_SMTP_USER= +EMAIL_SMTP_PASSWORD= +EMAIL_HTTP_HOST=localhost +EMAIL_HTTP_PORT=1080 \ No newline at end of file diff --git a/infra/email.js b/infra/email.js new file mode 100644 index 0000000..e977efe --- /dev/null +++ b/infra/email.js @@ -0,0 +1,21 @@ +import nodemailer from "nodemailer"; + +const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_SMTP_HOST, + port: process.env.EMAIL_SMTP_PORT, + auth: { + user: process.env.EMAIL_SMTP_USER, + pass: process.env.EMAIL_SMTP_PASSWORD, + }, + secure: process.env.NODE_ENV === "production", +}); + +async function send(mailOptions) { + await transporter.sendMail(mailOptions); +} + +const email = { + send, +}; + +export default email; diff --git a/package-lock.json b/package-lock.json index bfe8377..8f4adcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "next": "14.2.5", "next-connect": "1.0.0", "node-pg-migrate": "7.6.1", + "nodemailer": "7.0.5", "pg": "8.12.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -9614,6 +9615,15 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemailer": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.5.tgz", + "integrity": "sha512-nsrh2lO3j4GkLLXoeEksAMgAOqxOv6QumNRVQTJwKH4nuiww6iC2y7GyANs9kRAxCexg3+lTWM3PZ91iLlVjfg==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-package-data": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", diff --git a/package.json b/package.json index d1dba9d..b2d9c1d 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "next": "14.2.5", "next-connect": "1.0.0", "node-pg-migrate": "7.6.1", + "nodemailer": "7.0.5", "pg": "8.12.0", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/test/integration/infra/email.test.js b/test/integration/infra/email.test.js new file mode 100644 index 0000000..ac72d65 --- /dev/null +++ b/test/integration/infra/email.test.js @@ -0,0 +1,32 @@ +import email from "infra/email.js"; +import orchestrator from "test/orchestrator.js"; + +beforeAll(async () => { + await orchestrator.waitAllServices(); +}); + +describe("infra/email.js", () => { + test("send()", async () => { + await orchestrator.deleteAllEmails(); + await email.send({ + from: "INSystem ", + to: "contato@curso.dev", + subject: "Subject test", + text: "Body test", + }); + + await email.send({ + from: "INSystem ", + to: "contato@curso.dev", + subject: "Last email sent", + text: "Body of the last email", + }); + + const lastEmail = await orchestrator.getLastEmail(); + + expect(lastEmail.sender).toBe(""); + expect(lastEmail.recipients[0]).toBe(""); + expect(lastEmail.subject).toBe("Last email sent"); + expect(lastEmail.text).toBe("Body of the last email\n"); + }); +}); diff --git a/test/orchestrator.js b/test/orchestrator.js index 69d0e05..0c8ced7 100644 --- a/test/orchestrator.js +++ b/test/orchestrator.js @@ -5,10 +5,13 @@ import migrator from "models/migrator"; import session from "models/session"; import user from "models/user"; +const emailHttpUrl = `http://${process.env.EMAIL_HTTP_HOST}:${process.env.EMAIL_HTTP_PORT}`; + async function waitAllServices() { - await forWebServices(); + await waitForWebServices(); + await waitForEmailServices(); - async function forWebServices() { + async function waitForWebServices() { return retry(fetchStatusPage, { retries: 100, maxTimeout: 1000, @@ -21,6 +24,20 @@ async function waitAllServices() { } } } + + async function waitForEmailServices() { + return retry(fetchEmailPage, { + retries: 100, + maxTimeout: 1000, + }); + + async function fetchEmailPage() { + const response = await fetch(emailHttpUrl); + if (response.status !== 200) { + throw Error(); + } + } + } } async function clearDatabase() { @@ -44,12 +61,32 @@ async function createSession(userId) { return session.create(userId); } +async function deleteAllEmails() { + await fetch(`${emailHttpUrl}/messages`, { method: "DELETE" }); +} + +async function getLastEmail() { + const emailListResponse = await fetch(`${emailHttpUrl}/messages`); + const emailListResponseBody = await emailListResponse.json(); + const lastEmailItem = emailListResponseBody.pop(); + + const emailTextResponse = await fetch( + `${emailHttpUrl}/messages/${lastEmailItem.id}.plain`, + ); + const emailTextBody = await emailTextResponse.text(); + + lastEmailItem.text = emailTextBody; + return lastEmailItem; +} + const orchestrator = { waitAllServices, clearDatabase, runPendingMigrations, createUser, createSession, + deleteAllEmails, + getLastEmail, }; export default orchestrator;