diff --git a/src/utils/logging/config.js b/src/utils/logging/config.js index 9ccbee2..01d067e 100644 --- a/src/utils/logging/config.js +++ b/src/utils/logging/config.js @@ -22,8 +22,8 @@ const LOG_LEVEL = process.env.LOG_LEVEL?.toLowerCase() || "info"; const LOG_LEVELS = customLevels.levels; -const logDir = path.join(__dirname, "..", "..", "logs"); -const projectRoot = path.join(__dirname, "..", ".."); +const projectRoot = path.join(__dirname, "..", "..", ".."); +const logDir = path.join(projectRoot, "logs"); const sessionTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); const sessionDir = path.join(logDir, "sessions", sessionTimestamp); diff --git a/src/utils/sendNewsletterSubscriptionMail.js b/src/utils/sendNewsletterSubscriptionMail.js index db48659..85de480 100644 --- a/src/utils/sendNewsletterSubscriptionMail.js +++ b/src/utils/sendNewsletterSubscriptionMail.js @@ -1,20 +1,23 @@ +// src/utils/sendNewsletterSubscriptionMail.js const transporter = require("./transporter"); const { winstonLogger } = require("./logging"); -const MAIL_DOMAIN = process.env.MAIL_DOMAIN; -const MAIL_NEWSLETTER = process.env.MAIL_NEWSLETTER; - const MAIL_SUBJECT = "New Newsletter Subscription"; -const MAIL_FROM = `Newsletter `; -const MAIL_TEXT_TEMPLATE = (email) => - `Please add this email to the newsletter list: ${MAIL_NEWSLETTER}`; // fixme + +function getMailFrom() { + return `Newsletter `; +} + +function getMailText() { + return `Please add this email to the newsletter list: ${process.env.MAIL_NEWSLETTER}`; +} async function sendNewsletterSubscriptionMail({ email }) { const mailData = { - from: MAIL_FROM, + from: getMailFrom(), to: email, subject: MAIL_SUBJECT, - text: MAIL_TEXT_TEMPLATE(email), + text: getMailText(email), }; try { diff --git a/src/utils/transporter.js b/src/utils/transporter.js index 089cfcc..f2537bd 100644 --- a/src/utils/transporter.js +++ b/src/utils/transporter.js @@ -1,4 +1,4 @@ -// src/utils/mailer.js +// src/utils/transporter.js const nodemailer = require("nodemailer"); require("dotenv").config(); diff --git a/test/units/middleware/logging/createLogStreams.test.js b/test/units/middleware/logging/createLogStreams.test.js deleted file mode 100644 index ff2647f..0000000 --- a/test/units/middleware/logging/createLogStreams.test.js +++ /dev/null @@ -1,31 +0,0 @@ -// test/createLogStreams.test.js -const fs = require("fs"); -const path = require("path"); -const { expect } = require("chai"); -const { createLogStreams } = require("../../../../src/utils/logging"); - -describe("createLogStreams", () => { - const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs"); - const files = { - info: path.join(testDir, "info.log"), - error: path.join(testDir, "error.log"), - warn: path.join(testDir, "warn.log"), - notice: path.join(testDir, "notice.log"), - debug: path.join(testDir, "debug.log"), - }; - - afterEach(() => { - Object.values(files).forEach((file) => { - try { - fs.unlinkSync(file); - } catch (_) {} - }); - }); - - it("should create write streams for all log files", () => { - const streams = createLogStreams(files); - for (const key of Object.keys(files)) { - expect(streams[key]).to.be.an.instanceof(fs.WriteStream); - } - }); -}); diff --git a/test/units/middleware/logging/formatFunctionName.test.js b/test/units/middleware/logging/formatFunctionName.test.js deleted file mode 100644 index 9c847ad..0000000 --- a/test/units/middleware/logging/formatFunctionName.test.js +++ /dev/null @@ -1,14 +0,0 @@ -// test/formatFunctionName.test.js -const { expect } = require("chai"); -const path = require("path"); -const { formatFunctionName } = require("../../../../src/utils/logging"); - -describe("formatFunctionName", () => { - it("returns relative path with forward slashes", () => { - const base = path.join(__dirname, "..", "..", "..", ".."); - const testPath = path.join(base, "src", "somefile.js"); - const result = formatFunctionName(testPath, base); - - expect(result).to.equal("src/somefile.js"); - }); -}); diff --git a/test/units/middleware/logging/formatLogMessage.test.js b/test/units/middleware/logging/formatLogMessage.test.js deleted file mode 100644 index 4b30976..0000000 --- a/test/units/middleware/logging/formatLogMessage.test.js +++ /dev/null @@ -1,15 +0,0 @@ -// test/formatLogMessage.test.js -const { expect } = require("chai"); -const { formatLogMessage } = require("../../../../src/utils/logging"); - -describe("formatLogMessage", () => { - it("formats message with timestamp and args", () => { - const fn = "testFunc.js"; - const args = ["arg1", "arg2"]; - const result = formatLogMessage(fn, args); - - expect(result).to.match(/\[\d{4}-\d{2}-\d{2}T/); // ISO date start - expect(result).to.include("arg1 arg2"); - expect(result).to.match(/\n$/); - }); -}); diff --git a/test/units/middleware/logging/handleUncaughtException.test.js b/test/units/middleware/logging/handleUncaughtException.test.js deleted file mode 100644 index d650708..0000000 --- a/test/units/middleware/logging/handleUncaughtException.test.js +++ /dev/null @@ -1,28 +0,0 @@ -// test/handleUncaughtException.test.js -const { expect } = require("chai"); -const sinon = require("sinon"); -const proxyquire = require("proxyquire").noCallThru(); // prevents loading actual file if stubbed - -describe("handleUncaughtException", () => { - it("logs error using winstonLogger", () => { - const errorStub = sinon.stub(); - - const fakeLogger = { - winstonLogger: { - error: errorStub, - }, - }; - - const { handleUncaughtException } = proxyquire( - "../../../../src/utils/logging/handlers", - { - "./index": fakeLogger, - } - ); - - const err = new Error("fail"); - handleUncaughtException(err); - - expect(errorStub.calledWith("Uncaught Exception:", err.stack)).to.be.true; - }); -}); diff --git a/test/units/middleware/logging/handleUnhandledRejection.test.js b/test/units/middleware/logging/handleUnhandledRejection.test.js deleted file mode 100644 index 74e720b..0000000 --- a/test/units/middleware/logging/handleUnhandledRejection.test.js +++ /dev/null @@ -1,22 +0,0 @@ -const { expect } = require("chai"); -const sinon = require("sinon"); -const path = require("path"); -const proxyquire = require("proxyquire"); - -describe("handleUnhandledRejection", () => { - it("logs rejection using winstonLogger", () => { - const errorStub = sinon.stub(); - const reason = new Error("rejection"); - - const handlers = proxyquire( - path.resolve(__dirname, "../../../../src/utils/logging/handlers"), - { - "../logging": { winstonLogger: { error: errorStub } }, - } - ); - - handlers.handleUnhandledRejection(reason); - expect(errorStub.calledWith("Unhandled Rejection:", reason.stack)).to.be - .true; - }); -}); diff --git a/test/units/middleware/logging/initializeLogDirectories.test.js b/test/units/middleware/logging/initializeLogDirectories.test.js deleted file mode 100644 index aa52d26..0000000 --- a/test/units/middleware/logging/initializeLogDirectories.test.js +++ /dev/null @@ -1,44 +0,0 @@ -// test/initializeLogDirectories.test.js -const { expect } = require("chai"); -const fs = require("fs"); -const path = require("path"); -const mockFs = require("mock-fs"); -const { initializeLogDirectories } = require("../../../../src/utils/logging"); - -describe("initializeLogDirectories", () => { - const customLogFiles = { - info: "logs/info/info.log", - error: "logs/error/error.log", - warn: "logs/warn/warn.log", - notice: "logs/notice/notice.log", - debug: "logs/debug/debug.log", - }; - - afterEach(() => mockFs.restore()); - - it("should create all required directories for given log files", () => { - mockFs({}); - const result = initializeLogDirectories(customLogFiles); - - for (const file of Object.values(customLogFiles)) { - const dir = path.dirname(file); - expect(fs.existsSync(dir)).to.be.true; - } - - expect(fs.existsSync(result)).to.be.true; - }); - - it("should not fail if directories already exist", () => { - const dirs = Object.values(customLogFiles).reduce( - (acc, file) => { - acc[path.dirname(file)] = {}; - return acc; - }, - { "logs/functions": {} } - ); - - mockFs(dirs); - - expect(() => initializeLogDirectories(customLogFiles)).to.not.throw(); - }); -}); diff --git a/test/units/middleware/logging/shouldLog.test.js b/test/units/middleware/logging/shouldLog.test.js deleted file mode 100644 index d9b9ed7..0000000 --- a/test/units/middleware/logging/shouldLog.test.js +++ /dev/null @@ -1,26 +0,0 @@ -// test/shouldLog.test.js -const { expect } = require("chai"); -const path = require("path"); - -describe("shouldLog", () => { - const originalLogLevel = process.env.LOG_LEVEL; - - beforeEach(() => { - process.env.LOG_LEVEL = "warn"; - }); - - afterEach(() => { - process.env.LOG_LEVEL = originalLogLevel; - delete require.cache[ - require.resolve("../../../../src/utils/logging/consolePatch") - ]; - }); - - it("returns true if level is higher or equal to current log level", () => { - const { shouldLog } = require("../../../../src/utils/logging/consolePatch"); - - expect(shouldLog("error")).to.be.true; - expect(shouldLog("warn")).to.be.true; - expect(shouldLog("debug")).to.be.false; - }); -}); diff --git a/test/units/middleware/logging/writeLog.test.js b/test/units/middleware/logging/writeLog.test.js deleted file mode 100644 index 6ba1a61..0000000 --- a/test/units/middleware/logging/writeLog.test.js +++ /dev/null @@ -1,83 +0,0 @@ -// test/writeLog.test.js -const { expect } = require("chai"); -const sinon = require("sinon"); -const { writeLog } = require("../../../../src/utils/logging/consolePatch"); - -describe("writeLog", () => { - let stream; - let consoleFn; - let sessionTransport; - let clock; - const fixedDate = new Date("2025-07-25T12:00:00.000Z"); - - beforeEach(() => { - stream = { write: sinon.spy() }; - consoleFn = sinon.spy(); - - global.sessionTransport = { write: sinon.spy() }; - sessionTransport = global.sessionTransport; - - clock = sinon.useFakeTimers(fixedDate.getTime()); - }); - - afterEach(() => { - clock.restore(); - delete global.sessionTransport; - }); - - it("does not write when shouldLog returns false", () => { - const originalLogLevel = process.env.LOG_LEVEL; - process.env.LOG_LEVEL = "error"; - - writeLog("DEBUG", stream, consoleFn, "test message"); - - expect(stream.write.called).to.be.false; - expect(consoleFn.called).to.be.false; - expect(sessionTransport.write.called).to.be.false; - - process.env.LOG_LEVEL = originalLogLevel; - }); - - it("writes log line to stream and calls consoleFn and sessionTransport.write", () => { - writeLog("INFO", stream, consoleFn, "test", "message"); - - const expectedTimestamp = fixedDate.toISOString(); - const expectedLogLine = `[${expectedTimestamp}] [INFO] test message\n`; - - expect(stream.write.calledWith(expectedLogLine)).to.be.true; - expect( - sessionTransport.write.calledWith({ - level: "info", - message: "test message", - timestamp: expectedTimestamp, - }) - ).to.be.true; - expect( - consoleFn.calledWith(`[${expectedTimestamp}] [INFO]`, "test", "message") - ).to.be.true; - }); - - it("joins multiple args correctly in message", () => { - writeLog("WARN", stream, consoleFn, "part1", "part2", "part3"); - - const expectedTimestamp = fixedDate.toISOString(); - const expectedLogLine = `[${expectedTimestamp}] [WARN] part1 part2 part3\n`; - - expect(stream.write.calledWith(expectedLogLine)).to.be.true; - expect( - sessionTransport.write.calledWith({ - level: "warn", - message: "part1 part2 part3", - timestamp: expectedTimestamp, - }) - ).to.be.true; - expect( - consoleFn.calledWith( - `[${expectedTimestamp}] [WARN]`, - "part1", - "part2", - "part3" - ) - ).to.be.true; - }); -}); diff --git a/test/units/utils/logging/config.test.js b/test/units/utils/logging/config.test.js new file mode 100644 index 0000000..c8b2b0c --- /dev/null +++ b/test/units/utils/logging/config.test.js @@ -0,0 +1,79 @@ +// test/units/utils/logging/config.test.js +const { expect } = require("chai"); +const fs = require("fs"); +const path = require("path"); +const proxyquire = require("proxyquire").noPreserveCache(); + +const { + projectRoot, + logDir, + sessionTimestamp, + sessionDir, + logFiles, + LOG_LEVELS, +} = require("../../../../src/utils/logging/config"); + +describe("config.js", () => { + it("projectRoot contains package.json", () => { + const pkgJsonPath = path.join(projectRoot, "package.json"); + const exists = fs.existsSync(pkgJsonPath); + expect(exists).to.equal(true, `package.json not found in ${projectRoot}`); + }); + + it("projectRoot matches resolved 3-levels-up path", () => { + const expected = path.resolve(__dirname, "../../../../"); + expect(projectRoot).to.equal(expected); + }); + + it("logDir is within projectRoot and ends with 'logs'", () => { + expect(logDir.startsWith(projectRoot)).to.be.true; + expect(path.basename(logDir)).to.equal("logs"); + }); + + it("sessionTimestamp matches expected ISO pattern with no colons or dots", () => { + expect(sessionTimestamp).to.match( + /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/ + ); + }); + + it("sessionDir is built from logDir and sessionTimestamp", () => { + const expected = path.join(logDir, "sessions", sessionTimestamp); + expect(sessionDir).to.equal(expected); + }); + + it("logFiles.session points to session.log in sessionDir", () => { + expect(logFiles.session).to.equal(path.join(sessionDir, "session.log")); + }); + + ["info", "notice", "error", "warn", "debug"].forEach((level) => { + it(`logFiles.${level} points to ${level}.log in correct subdir`, () => { + expect(logFiles[level]).to.equal( + path.join(logDir, level, `${level}.log`) + ); + }); + }); + + it("LOG_LEVELS defines correct level-to-priority mapping", () => { + expect(LOG_LEVELS).to.deep.equal({ + error: 0, + warn: 1, + security: 2, + notice: 3, + info: 4, + debug: 5, + }); + }); + + it("LOG_LEVEL defaults to 'info' when process.env.LOG_LEVEL is unset", () => { + const original = process.env.LOG_LEVEL; + delete process.env.LOG_LEVEL; + + const { LOG_LEVEL } = proxyquire( + "../../../../src/utils/logging/config", + {} + ); + expect(LOG_LEVEL).to.equal("info"); + + if (original !== undefined) process.env.LOG_LEVEL = original; + }); +}); diff --git a/test/units/utils/logging/createLogStreams.test.js b/test/units/utils/logging/createLogStreams.test.js new file mode 100644 index 0000000..ff2647f --- /dev/null +++ b/test/units/utils/logging/createLogStreams.test.js @@ -0,0 +1,31 @@ +// test/createLogStreams.test.js +const fs = require("fs"); +const path = require("path"); +const { expect } = require("chai"); +const { createLogStreams } = require("../../../../src/utils/logging"); + +describe("createLogStreams", () => { + const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs"); + const files = { + info: path.join(testDir, "info.log"), + error: path.join(testDir, "error.log"), + warn: path.join(testDir, "warn.log"), + notice: path.join(testDir, "notice.log"), + debug: path.join(testDir, "debug.log"), + }; + + afterEach(() => { + Object.values(files).forEach((file) => { + try { + fs.unlinkSync(file); + } catch (_) {} + }); + }); + + it("should create write streams for all log files", () => { + const streams = createLogStreams(files); + for (const key of Object.keys(files)) { + expect(streams[key]).to.be.an.instanceof(fs.WriteStream); + } + }); +}); diff --git a/test/units/utils/logging/formatFunctionName.test.js b/test/units/utils/logging/formatFunctionName.test.js new file mode 100644 index 0000000..9c847ad --- /dev/null +++ b/test/units/utils/logging/formatFunctionName.test.js @@ -0,0 +1,14 @@ +// test/formatFunctionName.test.js +const { expect } = require("chai"); +const path = require("path"); +const { formatFunctionName } = require("../../../../src/utils/logging"); + +describe("formatFunctionName", () => { + it("returns relative path with forward slashes", () => { + const base = path.join(__dirname, "..", "..", "..", ".."); + const testPath = path.join(base, "src", "somefile.js"); + const result = formatFunctionName(testPath, base); + + expect(result).to.equal("src/somefile.js"); + }); +}); diff --git a/test/units/utils/logging/formatLogMessage.test.js b/test/units/utils/logging/formatLogMessage.test.js new file mode 100644 index 0000000..4b30976 --- /dev/null +++ b/test/units/utils/logging/formatLogMessage.test.js @@ -0,0 +1,15 @@ +// test/formatLogMessage.test.js +const { expect } = require("chai"); +const { formatLogMessage } = require("../../../../src/utils/logging"); + +describe("formatLogMessage", () => { + it("formats message with timestamp and args", () => { + const fn = "testFunc.js"; + const args = ["arg1", "arg2"]; + const result = formatLogMessage(fn, args); + + expect(result).to.match(/\[\d{4}-\d{2}-\d{2}T/); // ISO date start + expect(result).to.include("arg1 arg2"); + expect(result).to.match(/\n$/); + }); +}); diff --git a/test/units/utils/logging/handleUncaughtException.test.js b/test/units/utils/logging/handleUncaughtException.test.js new file mode 100644 index 0000000..458f05b --- /dev/null +++ b/test/units/utils/logging/handleUncaughtException.test.js @@ -0,0 +1,28 @@ +// test/handleUncaughtException.test.js +const { expect } = require("chai"); +const sinon = require("sinon"); +const proxyquire = require("proxyquire").noCallThru(); + +describe("handleUncaughtException", () => { + it("logs error using winstonLogger", () => { + const errorStub = sinon.stub(); + + const fakeLogger = { + winstonLogger: { + error: errorStub, + }, + }; + + const { handleUncaughtException } = proxyquire( + "../../../../src/utils/logging/handlers", + { + "./index": fakeLogger, + } + ); + + const err = new Error("fail"); + handleUncaughtException(err); + + expect(errorStub.calledWith("Uncaught Exception:", err.stack)).to.be.true; + }); +}); diff --git a/test/units/utils/logging/handleUnhandledRejection.test.js b/test/units/utils/logging/handleUnhandledRejection.test.js new file mode 100644 index 0000000..74e720b --- /dev/null +++ b/test/units/utils/logging/handleUnhandledRejection.test.js @@ -0,0 +1,22 @@ +const { expect } = require("chai"); +const sinon = require("sinon"); +const path = require("path"); +const proxyquire = require("proxyquire"); + +describe("handleUnhandledRejection", () => { + it("logs rejection using winstonLogger", () => { + const errorStub = sinon.stub(); + const reason = new Error("rejection"); + + const handlers = proxyquire( + path.resolve(__dirname, "../../../../src/utils/logging/handlers"), + { + "../logging": { winstonLogger: { error: errorStub } }, + } + ); + + handlers.handleUnhandledRejection(reason); + expect(errorStub.calledWith("Unhandled Rejection:", reason.stack)).to.be + .true; + }); +}); diff --git a/test/units/utils/logging/initializeLogDirectories.test.js b/test/units/utils/logging/initializeLogDirectories.test.js new file mode 100644 index 0000000..89cd398 --- /dev/null +++ b/test/units/utils/logging/initializeLogDirectories.test.js @@ -0,0 +1,44 @@ +// test/initializeLogDirectories.test.js +const { expect } = require("chai"); +const fs = require("fs"); +const path = require("path"); +const mockFs = require("mock-fs"); +const { initializeLogDirectories } = require("../../../../src/utils/logging"); + +describe("initializeLogDirectories", () => { + const customLogFiles = { + info: "../test/logs/info/info.log", + error: "../test/logs/error/error.log", + warn: "../test/logs/warn/warn.log", + notice: "../test/logs/notice/notice.log", + debug: "../test/logs/debug/debug.log", + }; + + afterEach(() => mockFs.restore()); + + it("should create all required directories for given log files", () => { + mockFs({}); + const result = initializeLogDirectories(customLogFiles); + + for (const file of Object.values(customLogFiles)) { + const dir = path.dirname(file); + expect(fs.existsSync(dir)).to.be.true; + } + + expect(fs.existsSync(result)).to.be.true; + }); + + it("should not fail if directories already exist", () => { + const dirs = Object.values(customLogFiles).reduce( + (acc, file) => { + acc[path.dirname(file)] = {}; + return acc; + }, + { "../test/logs/functions": {} } + ); + + mockFs(dirs); + + expect(() => initializeLogDirectories(customLogFiles)).to.not.throw(); + }); +}); diff --git a/test/units/utils/logging/shouldLog.test.js b/test/units/utils/logging/shouldLog.test.js new file mode 100644 index 0000000..2449594 --- /dev/null +++ b/test/units/utils/logging/shouldLog.test.js @@ -0,0 +1,25 @@ +// test/shouldLog.test.js +const { expect } = require("chai"); + +describe("shouldLog", () => { + const originalLogLevel = process.env.LOG_LEVEL; + + beforeEach(() => { + process.env.LOG_LEVEL = "warn"; + }); + + afterEach(() => { + process.env.LOG_LEVEL = originalLogLevel; + delete require.cache[ + require.resolve("../../../../src/utils/logging/consolePatch") + ]; + }); + + it("returns true if level is higher or equal to current log level", () => { + const { shouldLog } = require("../../../../src/utils/logging/consolePatch"); + + expect(shouldLog("error")).to.be.true; + expect(shouldLog("warn")).to.be.true; + expect(shouldLog("debug")).to.be.false; + }); +}); diff --git a/test/units/utils/logging/writeLog.test.js b/test/units/utils/logging/writeLog.test.js new file mode 100644 index 0000000..6ba1a61 --- /dev/null +++ b/test/units/utils/logging/writeLog.test.js @@ -0,0 +1,83 @@ +// test/writeLog.test.js +const { expect } = require("chai"); +const sinon = require("sinon"); +const { writeLog } = require("../../../../src/utils/logging/consolePatch"); + +describe("writeLog", () => { + let stream; + let consoleFn; + let sessionTransport; + let clock; + const fixedDate = new Date("2025-07-25T12:00:00.000Z"); + + beforeEach(() => { + stream = { write: sinon.spy() }; + consoleFn = sinon.spy(); + + global.sessionTransport = { write: sinon.spy() }; + sessionTransport = global.sessionTransport; + + clock = sinon.useFakeTimers(fixedDate.getTime()); + }); + + afterEach(() => { + clock.restore(); + delete global.sessionTransport; + }); + + it("does not write when shouldLog returns false", () => { + const originalLogLevel = process.env.LOG_LEVEL; + process.env.LOG_LEVEL = "error"; + + writeLog("DEBUG", stream, consoleFn, "test message"); + + expect(stream.write.called).to.be.false; + expect(consoleFn.called).to.be.false; + expect(sessionTransport.write.called).to.be.false; + + process.env.LOG_LEVEL = originalLogLevel; + }); + + it("writes log line to stream and calls consoleFn and sessionTransport.write", () => { + writeLog("INFO", stream, consoleFn, "test", "message"); + + const expectedTimestamp = fixedDate.toISOString(); + const expectedLogLine = `[${expectedTimestamp}] [INFO] test message\n`; + + expect(stream.write.calledWith(expectedLogLine)).to.be.true; + expect( + sessionTransport.write.calledWith({ + level: "info", + message: "test message", + timestamp: expectedTimestamp, + }) + ).to.be.true; + expect( + consoleFn.calledWith(`[${expectedTimestamp}] [INFO]`, "test", "message") + ).to.be.true; + }); + + it("joins multiple args correctly in message", () => { + writeLog("WARN", stream, consoleFn, "part1", "part2", "part3"); + + const expectedTimestamp = fixedDate.toISOString(); + const expectedLogLine = `[${expectedTimestamp}] [WARN] part1 part2 part3\n`; + + expect(stream.write.calledWith(expectedLogLine)).to.be.true; + expect( + sessionTransport.write.calledWith({ + level: "warn", + message: "part1 part2 part3", + timestamp: expectedTimestamp, + }) + ).to.be.true; + expect( + consoleFn.calledWith( + `[${expectedTimestamp}] [WARN]`, + "part1", + "part2", + "part3" + ) + ).to.be.true; + }); +}); diff --git a/test/units/utils/sendNewsletterSubscriptionMail.test.js b/test/units/utils/sendNewsletterSubscriptionMail.test.js new file mode 100644 index 0000000..b35252b --- /dev/null +++ b/test/units/utils/sendNewsletterSubscriptionMail.test.js @@ -0,0 +1,51 @@ +// test/sendNewsletterSubscriptionMail.test.js +const sinon = require("sinon"); +const transporter = require("../../../src/utils/transporter"); +const { winstonLogger } = require("../../../src/utils/logging"); +const sendNewsletterSubscriptionMail = require("../../../src/utils/sendNewsletterSubscriptionMail"); + +describe("sendNewsletterSubscriptionMail", () => { + let sendMailStub; + let errorStub; + + beforeEach(() => { + process.env.MAIL_DOMAIN = "example.com"; + process.env.MAIL_NEWSLETTER = "newsletter@example.com"; + + sendMailStub = sinon.stub(transporter, "sendMail"); + errorStub = sinon.stub(winstonLogger, "error"); + }); + + afterEach(() => { + sendMailStub.restore(); + errorStub.restore(); + delete process.env.MAIL_DOMAIN; + delete process.env.MAIL_NEWSLETTER; + }); + + it("calls transporter.sendMail with correct mail data", async () => { + sendMailStub.resolves("sent"); + + const email = "user@example.com"; + const result = await sendNewsletterSubscriptionMail({ email }); + + sinon.assert.calledOnce(sendMailStub); + sinon.assert.calledWith(sendMailStub, { + from: "Newsletter ", + to: email, + subject: "New Newsletter Subscription", + text: "Please add this email to the newsletter list: newsletter@example.com", + }); + sinon.assert.notCalled(errorStub); + }); + + it("logs error when transporter.sendMail rejects", async () => { + const error = new Error("send failed"); + sendMailStub.rejects(error); + + await sendNewsletterSubscriptionMail({ email: "fail@example.com" }); + + sinon.assert.calledOnce(errorStub); + sinon.assert.calledWith(errorStub, error); + }); +});