diff --git a/.env.development b/.env.development index fb1d7e8..b35f4f6 100644 --- a/.env.development +++ b/.env.development @@ -3,4 +3,11 @@ POSTGRES_PORT=5432 POSTGRES_DB=local_db POSTGRES_USER=local_user POSTGRES_PASSWORD=local_password -DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB \ No newline at end of file +DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB + +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/compose.yaml b/infra/compose.yaml index 59397f7..85ebcb3 100644 --- a/infra/compose.yaml +++ b/infra/compose.yaml @@ -6,3 +6,10 @@ services: - "../.env.development" ports: - "5432:5432" + + mailcatcher: + container_name: "mailcatcher-dev" + image: "sj26/mailcatcher" + ports: + - "1025:1025" + - "1080:1080" 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 2daaa29..ac318c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,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", @@ -8632,6 +8633,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-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 21e5337..dc2eb11 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,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/tests/integration/infra/email.test.js b/tests/integration/infra/email.test.js new file mode 100644 index 0000000..d94df5a --- /dev/null +++ b/tests/integration/infra/email.test.js @@ -0,0 +1,32 @@ +import orchestrator from "tests/orchestrator"; +import email from "infra/email"; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); +}); + +describe("infra/email.js", () => { + test("send()", async () => { + await orchestrator.deleteAllEmails(); + + await email.send({ + from: "Isaac ", + to: "muniz@isaacmuniz.pro", + subject: "Test subject", + text: "Body test.", + }); + + await email.send({ + from: "Isaac ", + to: "last.to@isaacmuniz.pro", + subject: "Last email sent", + text: "Last email body.", + }); + + 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("Last email body.\n"); + }); +}); diff --git a/tests/orchestrator.js b/tests/orchestrator.js index cb9eded..5268366 100644 --- a/tests/orchestrator.js +++ b/tests/orchestrator.js @@ -6,16 +6,33 @@ import migrator from "models/migrator.js"; import user from "models/user.js"; import session from "models/session"; +const emailHttpUrl = `http://${process.env.EMAIL_HTTP_HOST}:${process.env.EMAIL_HTTP_PORT}`; + async function waitForAllServices() { await waitForWebServer(); + await waitForEmailServer(); async function waitForWebServer() { - return retry(fetchStatusPage, { retries: 100 }); + return retry(fetchStatusPage, { retries: 100, maxTimeout: 1000 }); async function fetchStatusPage() { const response = await fetch("http://localhost:3000/api/v1/status"); - await response.json(); + if (response.status !== 200) { + throw Error(); + } + } + } + + async function waitForEmailServer() { + return retry(fetchEmailPage, { retries: 100, maxTimeout: 1000 }); + + async function fetchEmailPage() { + const response = await fetch(emailHttpUrl); + + if (response.status !== 200) { + throw Error(); + } } } } @@ -41,12 +58,33 @@ async function createSession(userId) { return await session.create(userId); } +async function deleteAllEmails() { + await fetch(`${emailHttpUrl}/messages`, { method: "DELETE" }); +} + +async function getLastEmail() { + const emailListResponse = await fetch(`${emailHttpUrl}/messages`); + const emailListBody = await emailListResponse.json(); + + const lastEmailItem = emailListBody.pop(); + const emailTextResponse = await fetch( + `${emailHttpUrl}/messages/${lastEmailItem.id}.plain`, + ); + const emailTextBody = await emailTextResponse.text(); + + lastEmailItem.text = emailTextBody; + + return lastEmailItem; +} + const orchestrator = { waitForAllServices, clearDatabase, runPendingMigrations, createUser, createSession, + deleteAllEmails, + getLastEmail, }; export default orchestrator;