Skip to content

Commit 7600be8

Browse files
committed
Add Jest setup files and tests
1 parent f30efe2 commit 7600be8

File tree

9 files changed

+2811
-25
lines changed

9 files changed

+2811
-25
lines changed

mailserver/jest-config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module.exports = {
2+
testEnvironment: 'node',
3+
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js'],
4+
collectCoverage: true,
5+
coverageDirectory: 'coverage',
6+
coverageReporters: ['text', 'lcov'],
7+
setupFilesAfterEnv: ['./jest.setup.js'],
8+
};

mailserver/jest-setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
jest.setTimeout(30000);

mailserver/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"start": "nodemon server.js",
88
"start:docker": "node server.js",
9-
"test": "echo \"Error: no test specified\" && exit 1",
9+
"test": "jest",
1010
"format": "prettier --write .",
1111
"format:check": "prettier --check ."
1212
},
@@ -30,6 +30,10 @@
3030
"validator": "^13.12.0"
3131
},
3232
"devDependencies": {
33-
"prettier": "^3.3.2"
33+
"jest": "^29.7.0",
34+
"mongodb-memory-server": "^9.4.0",
35+
"node-mocks-http": "^1.15.0",
36+
"prettier": "^3.3.2",
37+
"supertest": "^7.0.0"
3438
}
3539
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
const request = require("supertest");
2+
const { MongoMemoryServer } = require("mongodb-memory-server");
3+
const { connectMongoDB, closeMongoDB, getDB } = require("../../db");
4+
const createApp = require("../../app");
5+
const config = require("../../config");
6+
const { ObjectId } = require("mongodb");
7+
const path = require("path");
8+
const fs = require("fs");
9+
const { Readable } = require("stream");
10+
const { handleIncomingEmail } = require("../../services/emailService");
11+
12+
let mongoServer;
13+
let app;
14+
15+
beforeAll(async () => {
16+
mongoServer = await MongoMemoryServer.create();
17+
config.mongoURL = mongoServer.getUri();
18+
await connectMongoDB();
19+
app = createApp();
20+
});
21+
22+
afterAll(async () => {
23+
await closeMongoDB();
24+
await mongoServer.stop();
25+
});
26+
27+
describe("API Routes", () => {
28+
beforeEach(async () => {
29+
const db = getDB();
30+
await db.collection("test@example.com").deleteMany({});
31+
await db.collection("other@example.com").deleteMany({});
32+
});
33+
34+
const insertEmails = async (db, emails, collection) => {
35+
await db.collection(collection).insertMany(emails);
36+
};
37+
38+
const sendTestEmail = async (testEmail) => {
39+
const emailStream = new Readable();
40+
emailStream.push(
41+
Buffer.from(
42+
Object.entries(testEmail)
43+
.map(([k, v]) => `${k}: ${v}`)
44+
.join("\r\n")
45+
)
46+
);
47+
emailStream.push(null);
48+
49+
const session = {
50+
envelope: {
51+
rcptTo: [{ address: testEmail.to }],
52+
},
53+
};
54+
55+
await handleIncomingEmail(emailStream, session);
56+
const db = getDB();
57+
return db.collection(testEmail.to).findOne({ subject: testEmail.subject });
58+
};
59+
60+
test("GET /api/all-emails should return emails for all users", async () => {
61+
const db = getDB();
62+
await insertEmails(db, [{ from: { text: "sender@example.com" }, subject: "Test Email 1", date: new Date() }], "test@example.com");
63+
await insertEmails(db, [{ from: { text: "sender@example.com" }, subject: "Test Email 2", date: new Date() }], "other@example.com");
64+
65+
const response = await request(app).get("/api/all-emails");
66+
67+
expect(response.status).toBe(200);
68+
expect(response.body.length).toBe(2);
69+
});
70+
71+
test("GET /api/emails-list/:emailId should return emails for a specific user", async () => {
72+
const db = getDB();
73+
const emails = [
74+
{ subject: "Test Email 1", from: { text: "SendTestEmail <noreply@sendtestemail.com>" }, date: new Date(), readStatus: false },
75+
{ subject: "Test Email 2", from: { text: "SendTestEmail <noreply@sendtestemail.com>" }, date: new Date(), readStatus: true },
76+
];
77+
await insertEmails(db, emails, "harshal@myserver.pw");
78+
79+
const response = await request(app).get("/api/emails-list/harshal@myserver.pw");
80+
81+
expect(response.status).toBe(200);
82+
expect(response.body.length).toBe(2);
83+
expect(response.body[0].subject).toBe("Test Email 1");
84+
expect(response.body[1].subject).toBe("Test Email 2");
85+
});
86+
87+
test("GET /api/email/:emailID/:email_id should return a specific email and update readStatus", async () => {
88+
const testEmail = {
89+
from: "SendTestEmail <noreply@sendtestemail.com>",
90+
to: "harshal@myserver.pw",
91+
subject: "Test Email",
92+
text: "This is a test email.",
93+
};
94+
95+
const savedEmail = await sendTestEmail(testEmail);
96+
97+
let response = await request(app).get(`/api/email/${testEmail.to}/${savedEmail._id.toString()}`);
98+
response = await request(app).get(`/api/email/${testEmail.to}/${savedEmail._id.toString()}`);
99+
100+
expect(response.status).toBe(200);
101+
expect(response.body[0].subject).toBe("Test Email");
102+
expect(response.body[0].readStatus).toBe(true);
103+
104+
const db = getDB();
105+
const updatedEmail = await db.collection(testEmail.to).findOne({ _id: savedEmail._id });
106+
expect(updatedEmail.readStatus).toBe(true);
107+
108+
await db.collection(testEmail.to).deleteOne({ _id: savedEmail._id });
109+
});
110+
111+
test("DELETE /api/email/:emailID/:email_id should delete an email", async () => {
112+
const db = getDB();
113+
const insertResult = await db.collection("harshal@myserver.pw").insertOne({
114+
subject: "Test Email",
115+
from: { text: "SendTestEmail <noreply@sendtestemail.com>" },
116+
date: new Date(),
117+
});
118+
119+
const response = await request(app).delete(`/api/email/harshal@myserver.pw/${insertResult.insertedId.toString()}`);
120+
121+
expect(response.status).toBe(200);
122+
expect(response.body.message).toBe("Email deleted successfully");
123+
124+
const deletedEmail = await db.collection("harshal@myserver.pw").findOne({ _id: insertResult.insertedId });
125+
expect(deletedEmail).toBeNull();
126+
});
127+
128+
test("GET /api/attachment/:directory/:filename should serve attachment files", async () => {
129+
const testEmail = {
130+
from: "sender@example.com",
131+
to: "recipient@example.com",
132+
subject: "Test Email with Attachment",
133+
text: "This is a test email with an attachment.",
134+
attachments: [
135+
{
136+
filename: "test.txt",
137+
content: "This is a test attachment content.",
138+
},
139+
],
140+
};
141+
142+
const emailStream = new Readable();
143+
emailStream.push(
144+
`From: ${testEmail.from}\r\n` +
145+
`To: ${testEmail.to}\r\n` +
146+
`Subject: ${testEmail.subject}\r\n` +
147+
`Content-Type: multipart/mixed; boundary="boundary"\r\n\r\n` +
148+
`--boundary\r\n` +
149+
`Content-Type: text/plain\r\n\r\n` +
150+
`${testEmail.text}\r\n\r\n` +
151+
`--boundary\r\n` +
152+
`Content-Type: text/plain; name=${testEmail.attachments[0].filename}\r\n` +
153+
`Content-Disposition: attachment; filename=${testEmail.attachments[0].filename}\r\n` +
154+
`Content-Transfer-Encoding: base64\r\n\r\n` +
155+
`${Buffer.from(testEmail.attachments[0].content).toString("base64")}\r\n\r\n` +
156+
`--boundary--\r\n`
157+
);
158+
emailStream.push(null);
159+
160+
const session = {
161+
envelope: {
162+
rcptTo: [{ address: testEmail.to }],
163+
},
164+
};
165+
166+
await handleIncomingEmail(emailStream, session);
167+
168+
const db = getDB();
169+
const savedEmail = await db.collection(testEmail.to).findOne({ subject: testEmail.subject });
170+
171+
expect(savedEmail).toBeTruthy();
172+
expect(savedEmail.attachments.length).toBe(1);
173+
const [attachment] = savedEmail.attachments;
174+
175+
const response = await request(app).get(`/api/attachment/${attachment.directory}/${attachment.filename}`);
176+
177+
expect(response.status).toBe(200);
178+
expect(response.text).toBe("This is a test attachment content.");
179+
180+
await db.collection(testEmail.to).deleteOne({ _id: savedEmail._id });
181+
const attachmentPath = path.join(__dirname, "../../attachments", attachment.directory, attachment.filename);
182+
if (fs.existsSync(attachmentPath)) {
183+
fs.unlinkSync(attachmentPath);
184+
fs.rmdirSync(path.dirname(attachmentPath));
185+
}
186+
});
187+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const { MongoMemoryServer } = require("mongodb-memory-server");
2+
const { connectMongoDB, closeMongoDB, getDB } = require("../../db");
3+
const { startSMTPServer } = require("../../services/smtpService");
4+
const nodemailer = require("nodemailer");
5+
const config = require("../../config");
6+
7+
let mongoServer;
8+
let smtpServer;
9+
10+
beforeAll(async () => {
11+
mongoServer = await MongoMemoryServer.create();
12+
config.mongoURL = mongoServer.getUri();
13+
await connectMongoDB();
14+
smtpServer = await startSMTPServer();
15+
});
16+
17+
afterAll(async () => {
18+
await closeMongoDB();
19+
await mongoServer.stop();
20+
await new Promise((resolve) => smtpServer.close(resolve));
21+
});
22+
23+
describe("Email Receiving and Storage", () => {
24+
test("should receive email and store in DB", async () => {
25+
const transporter = nodemailer.createTransport({
26+
host: "localhost",
27+
port: config.smtpPort,
28+
secure: false,
29+
tls: { rejectUnauthorized: false },
30+
});
31+
32+
const info = await transporter.sendMail({
33+
from: "SendTestEmail <noreply@sendtestemail.com>",
34+
to: "harshal@myserver.pw",
35+
subject: "SendTestEmail.com - Testing Email ID: test123",
36+
text: "If you are reading this your email address is working.",
37+
html: "<html>Congratulations!<br><br>If you are reading this your email address is working.</html>",
38+
});
39+
40+
// Wait for email processing
41+
await new Promise((resolve) => setTimeout(resolve, 2000));
42+
43+
const db = getDB();
44+
const emails = await db.collection("harshal@myserver.pw").find({}).toArray();
45+
46+
expect(emails.length).toBe(1);
47+
expect(emails[0].subject).toBe("SendTestEmail.com - Testing Email ID: test123");
48+
expect(emails[0].text).toContain("If you are reading this your email address is working.");
49+
expect(emails[0].html).toContain("Congratulations!");
50+
expect(emails[0].from.text).toBe('"SendTestEmail" <noreply@sendtestemail.com>');
51+
expect(emails[0].to.text).toBe("harshal@myserver.pw");
52+
expect(emails[0].readStatus).toBe(false);
53+
expect(emails[0].createdAt).toBeDefined();
54+
});
55+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { MongoMemoryServer } = require("mongodb-memory-server");
2+
const { connectMongoDB, closeMongoDB, getDB } = require("../../db");
3+
const config = require("../../config");
4+
const { setupCronJobs } = require("../../services/cronService");
5+
const { getOldEmails, deleteEmailAndAttachments } = require("../../controllers/emailController");
6+
7+
let mongoServer;
8+
9+
beforeAll(async () => {
10+
mongoServer = await MongoMemoryServer.create();
11+
config.mongoURL = mongoServer.getUri();
12+
await connectMongoDB();
13+
});
14+
15+
afterAll(async () => {
16+
await closeMongoDB();
17+
await mongoServer.stop();
18+
});
19+
20+
describe("Cron Job", () => {
21+
test("should delete old emails", async () => {
22+
const db = getDB();
23+
const oldDate = new Date();
24+
oldDate.setDate(oldDate.getDate() - (config.emailRetentionDays + 1));
25+
26+
await db.collection("test@example.com").insertMany([
27+
{ from: { text: "sender@example.com" }, subject: "Old Email", date: oldDate },
28+
{ from: { text: "sender@example.com" }, subject: "New Email", date: new Date() },
29+
]);
30+
31+
const oldEmails = await getOldEmails(config.emailRetentionDays);
32+
expect(oldEmails.length).toBe(1);
33+
34+
for (const email of oldEmails) {
35+
await deleteEmailAndAttachments(email.emailID, email.emailId.toHexString());
36+
}
37+
38+
const remainingEmails = await db.collection("test@example.com").find({}).toArray();
39+
expect(remainingEmails.length).toBe(1);
40+
expect(remainingEmails[0].subject).toBe("New Email");
41+
});
42+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { startSMTPServer } = require("../../services/smtpService");
2+
3+
jest.mock("smtp-server", () => ({
4+
SMTPServer: jest.fn().mockImplementation(() => ({
5+
listen: jest.fn().mockResolvedValue(),
6+
on: jest.fn(),
7+
onData: jest.fn(),
8+
})),
9+
}));
10+
11+
describe("SMTP Service", () => {
12+
test("should create SMTP server with correct configuration", async () => {
13+
const server = await startSMTPServer();
14+
15+
expect(server.onData).toBeDefined();
16+
expect(typeof server.onData).toBe("function");
17+
});
18+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const sseService = require("../../services/sseService");
2+
const EventEmitter = require("events");
3+
4+
describe("SSE Service", () => {
5+
let mockResponse;
6+
7+
beforeEach(() => {
8+
mockResponse = new EventEmitter();
9+
mockResponse.writeHead = jest.fn();
10+
mockResponse.write = jest.fn();
11+
sseService.clients = new Map();
12+
});
13+
14+
test("should add client and send updates", () => {
15+
sseService.addClient("test@example.com", mockResponse);
16+
17+
expect(sseService.clients.has("test@example.com")).toBe(true);
18+
19+
const testData = { subject: "New Email" };
20+
sseService.sendUpdate("test@example.com", testData);
21+
22+
expect(mockResponse.write).toHaveBeenCalledWith(`data: ${JSON.stringify(testData)}\n\n`);
23+
});
24+
25+
test("should remove client on connection close", () => {
26+
sseService.addClient("test@example.com", mockResponse);
27+
mockResponse.emit("close");
28+
29+
expect(sseService.clients.has("test@example.com")).toBe(false);
30+
});
31+
});

0 commit comments

Comments
 (0)