Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
7 changes: 7 additions & 0 deletions infra/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ services:
- "../.env.development"
ports:
- "5432:5432"

mailcatcher:
container_name: "mailcatcher-dev"
image: "sj26/mailcatcher"
ports:
- "1025:1025"
- "1080:1080"
21 changes: 21 additions & 0 deletions infra/email.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/infra/email.test.js
Original file line number Diff line number Diff line change
@@ -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 <isaac@isaacmuniz.pro>",
to: "muniz@isaacmuniz.pro",
subject: "Test subject",
text: "Body test.",
});

await email.send({
from: "Isaac <last.from@isaacmuniz.pro>",
to: "last.to@isaacmuniz.pro",
subject: "Last email sent",
text: "Last email body.",
});

const lastEmail = await orchestrator.getLastEmail();
expect(lastEmail.sender).toBe("<last.from@isaacmuniz.pro>");
expect(lastEmail.recipients[0]).toBe("<last.to@isaacmuniz.pro>");
expect(lastEmail.subject).toBe("Last email sent");
expect(lastEmail.text).toBe("Last email body.\n");
});
});
42 changes: 40 additions & 2 deletions tests/orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
}
Expand All @@ -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;