diff --git a/package.json b/package.json
index c4a318c..b0e1b2f 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"type": "commonjs",
"scripts": {
"combine:css": "node scripts/combine-css.js",
- "test": "mocha test/**/*.unit.test.js test/**/*.property.test.js",
+ "test": "mocha \"test/**/*.unit.test.js\" \"test/**/*.property.test.js\"",
"start": "nodemon ./src/app.js --trace-exit",
"debug": "nodemon --inspect-brk ./src/app.js --trace-exit",
"maildev": "maildev",
diff --git a/src/utils/logging/config.js b/src/utils/logging/config.js
index 4ed1a36..69bbf85 100644
--- a/src/utils/logging/config.js
+++ b/src/utils/logging/config.js
@@ -5,12 +5,12 @@
levels: {
error: 0,
warn: 1,
- security: 2,
+ security: 3,
event: 2,
- notice: 3,
- info: 4,
- debug: 5,
- analytics: 6, // use a unique value
+ notice: 4,
+ info: 5,
+ debug: 6,
+ analytics: 7, // use a unique value
},
colors: {
error: "red",
diff --git a/src/utils/logging/index.js b/src/utils/logging/index.js
index 2704b3e..cb23ed1 100644
--- a/src/utils/logging/index.js
+++ b/src/utils/logging/index.js
@@ -144,7 +144,6 @@
manualLogger,
winstonLogger,
initializeLogDirectories,
- createLogStreams,
sessionTransport,
patchConsole,
shouldLog,
diff --git a/src/utils/logging/streams.js b/src/utils/logging/streams.js
index 09003c6..70975f5 100644
--- a/src/utils/logging/streams.js
+++ b/src/utils/logging/streams.js
@@ -15,6 +15,7 @@
debug: fs.createWriteStream(files.debug, { flags: "a" }),
security: fs.createWriteStream(files.security, { flags: "a" }),
event: fs.createWriteStream(files.event, { flags: "a" }),
+ analytics: fs.createWriteStream(files.analytics, { flags: "a" }),
};
}
diff --git a/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
deleted file mode 100644
index e07bac2..0000000
--- a/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// test/validateAndSanitizeEmail.fastcheck.test.js
-const { expect } = require("chai");
-const fc = require("fast-check");
-const validator = require("validator");
-
-const {
- validateAndSanitizeEmail,
- MESSAGES,
- MAX_EMAIL_LENGTH,
-} = require("../../../../src/utils/emailValidator");
-
-describe("validateAndSanitizeEmail - fast-check property-based tests", () => {
- it("should not throw for arbitrary strings", () => {
- fc.assert(
- fc.property(fc.string(), (str) => {
- const result = validateAndSanitizeEmail(str);
- expect(result).to.have.property("valid").that.is.a("boolean");
- if (result.valid) {
- expect(result).to.have.property("email").that.is.a("string");
- expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
- expect(validator.isEmail(result.email)).to.equal(true);
- } else {
- expect(result).to.have.property("message").that.is.a("string");
- expect(Object.values(MESSAGES)).to.include(result.message);
- }
- })
- );
- });
-
- it("should always return valid=true for valid, normalized, RFC-compliant email addresses", () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const result = validateAndSanitizeEmail(email);
- expect(result.valid).to.equal(true);
- expect(result.email).to.be.a("string");
- expect(validator.isEmail(result.email)).to.equal(true);
- expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
- expect(result.email.includes("..")).to.equal(false);
- expect(result.email.startsWith(".")).to.equal(false);
- expect(result.email.endsWith(".")).to.equal(false);
- })
- );
- });
-
- it("should reject emails longer than MAX_EMAIL_LENGTH", () => {
- const longLocal = "a".repeat(64);
- const longDomain = "b".repeat(MAX_EMAIL_LENGTH);
- const longEmail = `${longLocal}@${longDomain}.com`; // definitely > 320
-
- const result = validateAndSanitizeEmail(longEmail);
- expect(result.valid).to.equal(false);
- expect(result.message).to.equal(MESSAGES.TOO_LONG);
- });
-
- it("should reject strings that normalize to null", () => {
- const nonEmailInput = "invalid input string";
-
- const result = validateAndSanitizeEmail(nonEmailInput);
- if (result.valid) {
- expect(result.email).to.be.a("string");
- } else {
- expect([MESSAGES.INVALID, MESSAGES.REQUIRED]).to.include(result.message);
- }
- });
-
- it('should reject emails with ".." in them', () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const mutated = email.replace("@", "..@");
- const result = validateAndSanitizeEmail(mutated);
- expect(result.valid).to.equal(false);
- expect(result.message).to.equal(MESSAGES.INVALID);
- })
- );
- });
-
- it('should reject emails starting or ending with "."', () => {
- fc.assert(
- fc.property(fc.emailAddress(), (email) => {
- const startDot = `.${email}`;
- const endDot = `${email}.`;
-
- const result1 = validateAndSanitizeEmail(startDot);
- const result2 = validateAndSanitizeEmail(endDot);
-
- expect(result1.valid).to.equal(false);
- expect(result2.valid).to.equal(false);
- expect(result1.message).to.equal(MESSAGES.INVALID);
- expect(result2.message).to.equal(MESSAGES.INVALID);
- })
- );
- });
-});
diff --git a/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
deleted file mode 100644
index 5c052a3..0000000
--- a/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
+++ /dev/null
@@ -1,148 +0,0 @@
-// test/utils/emailValidator/validateAndSanitizeEmail.test.js
-const { expect } = require("chai");
-const sinon = require("sinon");
-const validator = require("validator");
-
-const {
- validateAndSanitizeEmail,
- MESSAGES,
- MAX_EMAIL_LENGTH,
-} = require("../../../../src/utils/emailValidator");
-
-describe("validateAndSanitizeEmail", () => {
- afterEach(() => {
- sinon.restore();
- });
-
- it("should return REQUIRED if input is undefined", () => {
- const result = validateAndSanitizeEmail(undefined);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is null", () => {
- const result = validateAndSanitizeEmail(null);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is a non-string type (number)", () => {
- const result = validateAndSanitizeEmail(123);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return REQUIRED if input is an empty string", () => {
- const result = validateAndSanitizeEmail("");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
- });
-
- it("should return INVALID if normalized email is null (validator.normalizeEmail returns null)", () => {
- sinon.stub(validator, "normalizeEmail").returns(null);
- const result = validateAndSanitizeEmail("notanemail");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return INVALID if normalized email is not valid (validator.isEmail returns false)", () => {
- sinon.stub(validator, "normalizeEmail").returns("invalid@domain");
- sinon.stub(validator, "isEmail").returns(false);
- const result = validateAndSanitizeEmail("invalid@domain");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return TOO_LONG if email exceeds MAX_EMAIL_LENGTH", () => {
- const localPart = "a".repeat(64);
- const domain = "b".repeat(255 - localPart.length - 1); // keep it under 320 when combined
- const email = `${localPart}@${domain}.com`;
-
- const tooLongEmail = `${email}${"x".repeat(MAX_EMAIL_LENGTH - email.length + 1)}`; // force >320
-
- sinon.stub(validator, "normalizeEmail").returns(tooLongEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(tooLongEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.TOO_LONG });
- });
-
- it('should return INVALID if email contains ".."', () => {
- const badEmail = "test..dot@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID if email starts with "."', () => {
- const badEmail = ".start@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID if email ends with "."', () => {
- const badEmail = "end.@example.com";
- sinon.stub(validator, "normalizeEmail").returns(badEmail);
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(badEmail);
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return valid email if all conditions are satisfied", () => {
- const rawEmail = " John.Doe@Example.com ";
- const normalized = "john.doe@example.com";
-
- sinon.stub(validator, "normalizeEmail").callsFake((email) => {
- // simulate trimming + lowercasing + normalization
- return email === "john.doe@example.com" ? normalized : null;
- });
-
- sinon.stub(validator, "isEmail").returns(true);
-
- const result = validateAndSanitizeEmail(rawEmail);
- expect(result).to.deep.equal({ valid: true, email: normalized });
- });
-
- it('should return INVALID for email with multiple "@" characters', () => {
- const result = validateAndSanitizeEmail("john@doe@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID for email with no "@" character', () => {
- const result = validateAndSanitizeEmail("johndoe.example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return VALID for a minimally valid email address", () => {
- const result = validateAndSanitizeEmail("a@b.co");
- expect(result.valid).to.equal(true);
- expect(result.email).to.be.a("string");
- });
-
- it("should return INVALID for email with spaces in local part", () => {
- const result = validateAndSanitizeEmail("john doe@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it('should return INVALID for email with space after "@"', () => {
- const result = validateAndSanitizeEmail("john@ example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return INVALID for email with quoted local part (validator accepts, you might not)", () => {
- const result = validateAndSanitizeEmail('"john.doe"@example.com');
- // Accept if validator does; reject if you disallow quotes
- // Adjust depending on your business rules
- expect(result.valid).to.equal(true);
- });
-
- it("should return INVALID for email with emoji in local part", () => {
- const result = validateAndSanitizeEmail("🧟@example.com");
- expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
- });
-
- it("should return VALID for email with subdomain", () => {
- const result = validateAndSanitizeEmail("user@sub.example.com");
- expect(result.valid).to.equal(true);
- });
-});
diff --git a/test/units/utils/logging/config.unit.test.js b/test/units/utils/logging/config.unit.test.js
deleted file mode 100644
index c8b2b0c..0000000
--- a/test/units/utils/logging/config.unit.test.js
+++ /dev/null
@@ -1,79 +0,0 @@
-// 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.unit.test.js b/test/units/utils/logging/createLogStreams.unit.test.js
deleted file mode 100644
index bb5482c..0000000
--- a/test/units/utils/logging/createLogStreams.unit.test.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// test/units/utils/logging/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.unit.test.js b/test/units/utils/logging/formatFunctionName.unit.test.js
deleted file mode 100644
index def137f..0000000
--- a/test/units/utils/logging/formatFunctionName.unit.test.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// test/units/utils/logging/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/formatLog.unit.test.js b/test/units/utils/logging/formatLog.unit.test.js
deleted file mode 100644
index c073a81..0000000
--- a/test/units/utils/logging/formatLog.unit.test.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const { formatLog } = require("../../../../src/utils/logging/consolePatch");
-const { expect } = require("chai");
-
-describe("Logger Format Function Tests", () => {
- it("should format circular objects without throwing and stringify correctly", () => {
- const circular = { name: "test" };
- circular.ref = circular;
-
- const methods = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"];
-
- methods.forEach((level) => {
- const { timestamp, safeArgs, message, logLine } = formatLog(
- level,
- circular
- );
-
- expect(timestamp).to.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/);
- expect(safeArgs).to.be.an("array");
- expect(message).to.include("name");
- expect(message).to.include("test");
- expect(message).to.not.include("[object Object]");
- expect(logLine).to.include(`[${timestamp}] [${level}]`);
- expect(logLine).to.include(message);
- });
- });
-});
diff --git a/test/units/utils/logging/formatLogMessage.unit.test.js b/test/units/utils/logging/formatLogMessage.unit.test.js
deleted file mode 100644
index 0dc0aeb..0000000
--- a/test/units/utils/logging/formatLogMessage.unit.test.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// test/units/utils/logging/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.unit.test.js b/test/units/utils/logging/handleUncaughtException.unit.test.js
deleted file mode 100644
index 6e4e3c5..0000000
--- a/test/units/utils/logging/handleUncaughtException.unit.test.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// test/units/utils/logging/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.unit.test.js b/test/units/utils/logging/handleUnhandledRejection.unit.test.js
deleted file mode 100644
index 74e720b..0000000
--- a/test/units/utils/logging/handleUnhandledRejection.unit.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/utils/logging/initializeLogDirectories.unit.test.js b/test/units/utils/logging/initializeLogDirectories.unit.test.js
deleted file mode 100644
index 15acc5a..0000000
--- a/test/units/utils/logging/initializeLogDirectories.unit.test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// test/units/utils/logging/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/object-formatting.unit.test.js b/test/units/utils/logging/object-formatting.unit.test.js
deleted file mode 100644
index 0649a60..0000000
--- a/test/units/utils/logging/object-formatting.unit.test.js
+++ /dev/null
@@ -1,521 +0,0 @@
-const { expect } = require("chai");
-const sinon = require("sinon");
-const fs = require("fs");
-const path = require("path");
-const { Writable } = require("stream");
-
-// Mock dependencies
-const mockLogStreams = {
- info: new Writable({ write() {} }),
- error: new Writable({ write() {} }),
- warn: new Writable({ write() {} }),
- debug: new Writable({ write() {} }),
- notice: new Writable({ write() {} }),
-};
-
-const mockSessionTransport = {
- write: sinon.stub(),
-};
-
-// Import the modules under test
-const { writeLog } = require("../../../../src/utils/logging/consolePatch");
-const {
- manualLogger,
- winstonLogger,
-} = require("../../../../src/utils/logging/index");
-
-describe("Logger Object Expansion Tests", () => {
- let streamWriteStubs;
-
- beforeEach(() => {
- // Create fresh stream write stubs for each test
- streamWriteStubs = {
- info: sinon.stub(mockLogStreams.info, "write"),
- error: sinon.stub(mockLogStreams.error, "write"),
- warn: sinon.stub(mockLogStreams.warn, "write"),
- debug: sinon.stub(mockLogStreams.debug, "write"),
- notice: sinon.stub(mockLogStreams.notice, "write"),
- };
-
- // Reset session transport
- mockSessionTransport.write.reset();
- });
-
- afterEach(() => {
- // Restore all stubs
- sinon.restore(); // This restores all stubs created by sinon.stub()
- });
-
- describe("writeLog function", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should never log [object Object] for simple objects", () => {
- const testObject = { name: "test", value: 42 };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- testObject
- );
-
- // Check console output
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("name");
- expect(outputString).to.include("test");
- expect(outputString).to.include("value");
- expect(outputString).to.include("42");
-
- // Check stream output
- expect(streamWriteStubs.info.called).to.be.true;
- const streamOutput = streamWriteStubs.info.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("name");
- expect(streamOutput).to.include("test");
- });
-
- it("should properly expand nested objects", () => {
- const nestedObject = {
- user: {
- id: 123,
- profile: {
- name: "John Doe",
- settings: { theme: "dark", notifications: true },
- },
- },
- };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- nestedObject
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("John Doe");
- expect(outputString).to.include("theme");
- expect(outputString).to.include("dark");
- expect(outputString).to.include("notifications");
- });
-
- it("should handle circular references without [object Object]", () => {
- const circularObj = { name: "circular" };
- circularObj.self = circularObj;
- circularObj.nested = { parent: circularObj };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- circularObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("name");
- expect(outputString).to.include("circular");
- // Should handle circular reference gracefully
- expect(outputString).to.include("self");
- });
-
- it("should expand arrays containing objects", () => {
- const arrayWithObjects = [
- { id: 1, name: "first" },
- { id: 2, name: "second", nested: { value: "test" } },
- ];
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- arrayWithObjects
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("first");
- expect(outputString).to.include("second");
- expect(outputString).to.include("nested");
- expect(outputString).to.include("test");
- });
-
- it("should handle mixed argument types without [object Object]", () => {
- const mixedArgs = [
- "String message",
- { obj: "value" },
- 42,
- ["array", "items"],
- { deeply: { nested: { object: "here" } } },
- ];
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- ...mixedArgs
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("String message");
- expect(outputString).to.include("obj");
- expect(outputString).to.include("value");
- expect(outputString).to.include("deeply");
- expect(outputString).to.include("here");
- });
-
- it("should handle Error objects without [object Object]", () => {
- const error = new Error("Test error");
- error.customProperty = { details: "additional info" };
-
- // Stub console.error for this test (local stub, not interfering with console.log stub)
- const consoleErrorStub = sinon.stub(console, "error");
-
- writeLog(
- "ERROR",
- mockLogStreams.error,
- console.error,
- mockSessionTransport,
- error
- );
-
- expect(consoleErrorStub.called).to.be.true;
- const consoleArgs = consoleErrorStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("Test error");
-
- consoleErrorStub.restore();
- });
-
- it("should handle objects with special properties", () => {
- const specialObj = {
- toString: () => "custom toString",
- valueOf: () => 99,
- [Symbol.toStringTag]: "CustomObject",
- normalProp: "normal value",
- };
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- specialObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("normalProp");
- expect(outputString).to.include("normal value");
- });
- });
-
- describe("Manual Logger Methods", () => {
- let manualLoggerStubs;
- // Removed writeLogStub as manualLogger directly interacts with console
-
- beforeEach(() => {
- // Create fresh stubs for manual logger streams if they exist
- manualLoggerStubs = {};
- if (manualLogger.streams) {
- Object.keys(manualLogger.streams).forEach((level) => {
- if (
- manualLogger.streams[level] &&
- typeof manualLogger.streams[level].write === "function"
- ) {
- if (!manualLogger.streams[level].write.isSinonProxy) {
- manualLoggerStubs[level] = sinon.stub(
- manualLogger.streams[level],
- "write"
- );
- }
- }
- });
- }
- });
-
- afterEach(() => {
- // Restore all stubs created within this describe block or its tests
- sinon.restore();
- });
-
- it("should not produce [object Object] in manualLogger.info", () => {
- const testObj = { key: "value", nested: { deep: "property" } };
-
- // Stub console.log locally for this specific test
- const consoleLogStub = sinon.stub(console, "log");
- manualLogger.info(testObj);
-
- expect(consoleLogStub.called).to.be.true; // Check if console.log was called
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" "); // Join them to check content
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("key");
- expect(outputString).to.include("nested");
- expect(outputString).to.include("deep");
- consoleLogStub.restore(); // Restore after test
- });
-
- it("should not produce [object Object] in manualLogger.error", () => {
- const errorObj = {
- error: "Something went wrong",
- context: { userId: 123, action: "login" },
- };
-
- // Stub console.error for this test
- const consoleErrorStub = sinon.stub(console, "error");
-
- manualLogger.error(errorObj);
-
- expect(consoleErrorStub.called).to.be.true;
- const consoleArgs = consoleErrorStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("Something went wrong");
- expect(outputString).to.include("userId");
- expect(outputString).to.include("login");
-
- consoleErrorStub.restore();
- });
- });
-
- describe("Winston Logger", () => {
- let winstonInfoStub;
-
- beforeEach(() => {
- winstonInfoStub = sinon.stub(winstonLogger, "info");
- });
-
- afterEach(() => {
- winstonInfoStub.restore(); // Ensure winston stub is restored
- });
-
- it("should not produce [object Object] in winston logs", () => {
- const logData = {
- user: { id: 456, name: "Jane" },
- action: "update",
- metadata: { timestamp: Date.now() },
- };
-
- winstonLogger.info("User action", logData);
-
- // Check that winston was called with properly formatted data
- expect(winstonInfoStub.called).to.be.true;
- const logCall = winstonInfoStub.getCall(0).args;
- // Winston typically stringifies objects, so we check the stringified output
- const logString = JSON.stringify(logCall);
- expect(logString).to.not.include("[object Object]");
- expect(logString).to.include("Jane");
- expect(logString).to.include("update");
- });
- });
-
- describe("Edge Cases", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should handle null and undefined without [object Object]", () => {
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- null,
- undefined
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("null");
- expect(outputString).to.include("undefined");
- });
-
- it("should handle objects with null prototype", () => {
- const nullProtoObj = Object.create(null);
- nullProtoObj.key = "value";
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- nullProtoObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("key");
- expect(outputString).to.include("value");
- });
-
- it("should handle Date objects", () => {
- const dateObj = new Date("2023-01-01T12:00:00.000Z"); // Use ISO string for consistent output
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- dateObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- // Check for parts of the date string that are likely to be present in ISO format
- // Console.log's output for Date objects can vary, but the ISO string is often included or derived.
- expect(outputString).to.include("2023");
- // Check for the time part of the ISO string for more robustness
- expect(outputString).to.include("T12:00:00.000Z");
- });
-
- it("should handle RegExp objects", () => {
- const regexObj = /test.*pattern/gi;
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- regexObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("test");
- expect(outputString).to.include("pattern");
- expect(outputString).to.include("gi"); // Check for flags
- });
-
- it("should handle very deeply nested objects", () => {
- let deepObj = { level: 0 };
- let current = deepObj;
-
- // Create 10 levels deep
- for (let i = 1; i <= 10; i++) {
- current.next = { level: i };
- current = current.next;
- }
-
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- deepObj
- );
-
- expect(consoleLogStub.called).to.be.true;
- const consoleArgs = consoleLogStub.getCall(0).args;
- expect(consoleArgs).to.exist;
- const outputString = consoleArgs.join(" ");
- expect(outputString).to.not.include("[object Object]");
- expect(outputString).to.include("level");
- // Check for presence of multiple levels
- expect(outputString.match(/level/g).length).to.be.at.least(10);
- });
- });
-
- describe("Stream Output Validation", () => {
- let consoleLogStub; // Declare stub for this describe block
-
- beforeEach(() => {
- // Stub console.log specifically for this describe block
- consoleLogStub = sinon.stub(console, "log");
- });
-
- afterEach(() => {
- // Restore console.log stub after each test in this block
- consoleLogStub.restore();
- });
-
- it("should ensure stream writes never contain [object Object]", () => {
- const testObjects = [
- { simple: "object" },
- { nested: { deep: { value: "test" } } },
- [{ array: "item" }],
- { mixed: ["array", { in: "object" }] },
- ];
-
- testObjects.forEach((obj, index) => {
- streamWriteStubs.info.resetHistory(); // Reset history for each iteration
- writeLog(
- "INFO",
- mockLogStreams.info,
- console.log,
- mockSessionTransport,
- obj
- );
-
- expect(streamWriteStubs.info.called).to.be.true;
- const streamWrites = streamWriteStubs.info.getCalls();
- streamWrites.forEach((call) => {
- const writeData = call.args[0];
- // Ensure the written data is a string and does not contain "[object Object]"
- expect(typeof writeData).to.equal("string");
- expect(writeData).to.not.include("[object Object]");
- });
- });
- });
- });
-});
diff --git a/test/units/utils/logging/writeLog.unit.test.js b/test/units/utils/logging/writeLog.unit.test.js
deleted file mode 100644
index 85070bb..0000000
--- a/test/units/utils/logging/writeLog.unit.test.js
+++ /dev/null
@@ -1,341 +0,0 @@
-// test/units/utils/logging/writeLog.test.js
-const { expect } = require("chai");
-const sinon = require("sinon");
-const { writeLog } = require("../../../../src/utils/logging/consolePatch");
-
-describe("writeLog - Object Expansion Tests", () => {
- 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();
- sessionTransport = { write: sinon.spy() };
- clock = sinon.useFakeTimers(fixedDate.getTime());
- });
-
- afterEach(() => {
- clock.restore();
- sinon.restore();
- });
-
- describe("prevents [object Object] output", () => {
- it("expands simple objects instead of showing [object Object]", () => {
- const testObject = { name: "test", value: 42 };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, testObject);
-
- const expectedTimestamp = fixedDate.toISOString();
-
- // Check stream output doesn't contain [object Object]
- expect(stream.write.called).to.be.true;
- const streamCall = stream.write.getCall(0);
- const streamOutput = streamCall.args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("name");
- expect(streamOutput).to.include("test");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("42");
-
- // Check console output doesn't contain [object Object]
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleArgs = consoleCall.args;
- expect(consoleArgs).to.exist;
- expect(Array.isArray(consoleArgs)).to.be.true;
- const consoleOutput = consoleArgs.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleArgs).to.include.members([`[${expectedTimestamp}] [INFO]`]);
-
- // Check sessionTransport message doesn't contain [object Object]
- expect(sessionTransport.write.called).to.be.true;
- const sessionCall = sessionTransport.write.getCall(0);
- const sessionData = sessionCall.args[0];
- expect(sessionData.message).to.not.include("[object Object]");
- expect(sessionData.message).to.include("name");
- expect(sessionData.message).to.include("test");
- });
-
- it("expands nested objects completely", () => {
- const nestedObject = {
- user: {
- id: 123,
- profile: {
- name: "John Doe",
- settings: { theme: "dark", notifications: true },
- },
- },
- };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, nestedObject);
-
- // Check all outputs expand the nested structure
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("John Doe");
- expect(streamOutput).to.include("theme");
- expect(streamOutput).to.include("dark");
- expect(streamOutput).to.include("notifications");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("John Doe");
- expect(consoleOutput).to.include("theme");
-
- expect(sessionTransport.write.called).to.be.true;
- const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("John Doe");
- });
-
- it("expands arrays containing objects", () => {
- const arrayWithObjects = [
- { id: 1, name: "first" },
- { id: 2, name: "second", nested: { value: "test" } },
- ];
-
- writeLog("INFO", stream, consoleFn, sessionTransport, arrayWithObjects);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("first");
- expect(streamOutput).to.include("second");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("test");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("first");
- expect(consoleOutput).to.include("second");
- });
-
- it("handles mixed argument types without [object Object]", () => {
- const mixedArgs = [
- "String message",
- { obj: "value" },
- 42,
- ["array", "items"],
- { deeply: { nested: { object: "here" } } },
- ];
-
- writeLog("INFO", stream, consoleFn, sessionTransport, ...mixedArgs);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("String message");
- expect(streamOutput).to.include("obj");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("deeply");
- expect(streamOutput).to.include("here");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("String message");
- expect(consoleOutput).to.include("obj");
- });
-
- it("expands Error objects properly", () => {
- const error = new Error("Test error");
- error.customProperty = { details: "additional info" };
-
- writeLog("ERROR", stream, consoleFn, sessionTransport, error);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("Test error");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("Test error");
- });
-
- it("handles objects with special properties", () => {
- const specialObj = {
- toString: () => "custom toString",
- valueOf: () => 99,
- normalProp: "normal value",
- anotherProp: { nested: "data" },
- };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, specialObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("normalProp");
- expect(streamOutput).to.include("normal value");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("data");
-
- expect(sessionTransport.write.called).to.be.true;
- const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("normalProp");
- });
- });
-
- describe("edge cases", () => {
- it("handles null and undefined without [object Object]", () => {
- writeLog("INFO", stream, consoleFn, sessionTransport, null, undefined);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("null");
- expect(streamOutput).to.include("undefined");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- });
-
- it("handles Date objects", () => {
- const dateObj = new Date("2023-01-01");
-
- writeLog("INFO", stream, consoleFn, sessionTransport, dateObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("2023");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- });
-
- it("handles RegExp objects", () => {
- const regexObj = /test.*pattern/gi;
-
- writeLog("INFO", stream, consoleFn, sessionTransport, regexObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("test");
- expect(streamOutput).to.include("pattern");
- });
-
- it("handles objects with null prototype", () => {
- const nullProtoObj = Object.create(null);
- nullProtoObj.key = "value";
- nullProtoObj.nested = { prop: "data" };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, nullProtoObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("key");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("prop");
- expect(streamOutput).to.include("data");
- });
-
- it("handles very deeply nested objects", () => {
- let deepObj = { level: 0 };
- let current = deepObj;
-
- // Create 5 levels deep (reasonable for testing)
- for (let i = 1; i <= 5; i++) {
- current.next = { level: i, data: `level${i}data` };
- current = current.next;
- }
-
- writeLog("INFO", stream, consoleFn, sessionTransport, deepObj);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("level");
- expect(streamOutput).to.include("level5data");
- });
- });
-
- describe("different log levels", () => {
- const levels = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"];
-
- levels.forEach((level) => {
- it(`expands objects properly for ${level} level`, () => {
- const testObj = {
- level: level.toLowerCase(),
- data: { nested: "value" },
- array: [{ item: "test" }],
- };
-
- writeLog(level, stream, consoleFn, sessionTransport, testObj);
-
- // Only check if the function was called for levels that should log
- if (stream.write.called) {
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("value");
- expect(streamOutput).to.include("item");
- expect(streamOutput).to.include("test");
- }
-
- if (sessionTransport.write.called) {
- const sessionMessage =
- sessionTransport.write.getCall(0).args[0].message;
- expect(sessionMessage).to.not.include("[object Object]");
- expect(sessionMessage).to.include("nested");
- }
- });
- });
- });
-
- describe("multiple objects in single call", () => {
- it("expands all objects in arguments", () => {
- const obj1 = { first: "object", nested: { value: 1 } };
- const obj2 = { second: "object", array: [{ item: "test" }] };
- const obj3 = { third: { deeply: { nested: "value" } } };
-
- writeLog("INFO", stream, consoleFn, sessionTransport, obj1, obj2, obj3);
-
- expect(stream.write.called).to.be.true;
- const streamOutput = stream.write.getCall(0).args[0];
- expect(streamOutput).to.not.include("[object Object]");
- expect(streamOutput).to.include("first");
- expect(streamOutput).to.include("second");
- expect(streamOutput).to.include("third");
- expect(streamOutput).to.include("deeply");
- expect(streamOutput).to.include("nested");
- expect(streamOutput).to.include("item");
- expect(streamOutput).to.include("test");
-
- expect(consoleFn.called).to.be.true;
- const consoleCall = consoleFn.getCall(0);
- expect(consoleCall).to.exist;
- const consoleOutput = consoleCall.args.join(" ");
- expect(consoleOutput).to.not.include("[object Object]");
- expect(consoleOutput).to.include("first");
- expect(consoleOutput).to.include("second");
- expect(consoleOutput).to.include("third");
- });
- });
-});
diff --git a/test/units/utils/sendContactMail/sanitizeInput.property.test.js b/test/units/utils/sendContactMail/sanitizeInput.property.test.js
deleted file mode 100644
index 3620788..0000000
--- a/test/units/utils/sendContactMail/sanitizeInput.property.test.js
+++ /dev/null
@@ -1,175 +0,0 @@
-const { expect } = require("chai");
-const fc = require("fast-check");
-const { sanitizeInput } = require("../../../../src/utils/sendContactMail");
-
-describe("sanitizeInput", () => {
- it("should remove all newline, carriage return, and angle brackets", () => {
- fc.assert(
- fc.property(fc.string(), (str) => {
- const result = sanitizeInput(str);
- expect(result).to.not.include("\r");
- expect(result).to.not.include("\n");
- expect(result).to.not.include("<");
- expect(result).to.not.include(">");
- })
- );
- });
-
- it("should return a string for any input", () => {
- fc.assert(
- fc.property(
- fc.anything(), // This can generate any value, including complex objects
- (input) => {
- const result = sanitizeInput(input);
- expect(typeof result).to.equal("string");
- }
- )
- );
- });
-
- it("should preserve safe characters when only safe characters are present", () => {
- fc.assert(
- fc.property(
- fc.string({
- // ONLY include characters that won't be filtered out
- // Explicitly exclude: \r, \n, <, >
- minLength: 0, // Allow empty string
- maxLength: 100, // Or some reasonable max length for your test
- // Generate strings of characters that are NOT \r, \n, <, or >
- // We'll filter the characters after generation
- // For individual characters, you'd typically generate a string of length 1
- // and then map/filter the characters from that string.
- // A common pattern is to use fc.array(fc.char()), but fc.char() doesn't exist.
- // So, we'll generate strings with specific character sets, or filter a broader set.
- }),
- (input) => {
- // Filter the input string to ensure it only contains allowed characters
- // This is a common pattern when specific character sets are needed
- const filteredInput = [...input]
- .filter((c) => !["\r", "\n", "<", ">"].includes(c))
- .join("");
-
- const result = sanitizeInput(filteredInput); // Pass the filtered input to your sanitize function
- expect(result).to.equal(filteredInput.trim());
- }
- )
- );
- });
-
- // You might still want a separate test for only "safe" characters
- // where it confirms that characters *not* in the removal list are preserved.
- it("should preserve characters NOT in the removal list when only such characters are present", () => {
- fc.assert(
- fc.property(
- fc.string({
- // ONLY include characters that *should not* be filtered out
- characters:
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;'\":,./?`~ \t", // Excludes \r, \n, <, >
- }),
- (input) => {
- if (/[<>\r\n]/.test(input)) fc.pre(false); // Reject inputs with excluded chars
- const result = sanitizeInput(input);
- // For inputs that *only* contain allowed characters,
- // the result should simply be the trimmed version of the input.
- expect(result).to.equal(input.trim());
- }
- )
- );
- });
-
- it("should remove carriage returns, newlines, angle brackets, and trim whitespace", () => {
- fc.assert(
- fc.property(
- fc.string({
- // Include characters that are *expected* to be removed by sanitizeInput
- // along with regular safe characters to ensure comprehensive testing.
- characters:
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$%^&*()_+-=[]{}|;':\",./?`~<>\\r\\n \t", // Added spaces and tabs for trim testing
- }),
- (input) => {
- const result = sanitizeInput(input);
-
- // Manually apply the *expected* sanitization logic to create the expected output.
- // This should match precisely what sanitizeInput is designed to do.
- const expectedOutput = String(input)
- .replace(/[\r\n<>]/g, "") // Remove \r, \n, <, >
- .trim(); // Trim whitespace
-
- expect(result).to.equal(expectedOutput);
- }
- )
- );
- });
-
- it("should remove dangerous characters from any string", () => {
- fc.assert(
- fc.property(
- fc.string(), // Any string
- (input) => {
- const result = sanitizeInput(input);
-
- // Result should be a string
- expect(typeof result).to.equal("string");
-
- // Result should not contain dangerous characters
- expect(result).to.not.include("\r");
- expect(result).to.not.include("\n");
- expect(result).to.not.include("<");
- expect(result).to.not.include(">");
-
- // Result should be trimmed
- expect(result).to.equal(result.trim());
- }
- )
- );
- });
-
- it("should handle edge cases correctly", () => {
- const testCases = [
- { input: "", expected: "" },
- { input: null, expected: "null" },
- { input: undefined, expected: "undefined" },
- { input: " ", expected: "" },
- { input: "hello");
- expect(result).to.equal("scriptalert(1)/script");
- });
-
- it("trims leading and trailing spaces", () => {
- const result = sanitizeInput(" test input ");
- expect(result).to.equal("test input");
- });
-
- it("coerces non-strings to strings", () => {
- const result = sanitizeInput(12345);
- expect(result).to.equal("12345");
- });
-
- it("returns empty string for null", () => {
- const result = sanitizeInput(null);
- expect(result).to.equal("null");
- });
-
- it("returns empty string for undefined", () => {
- const result = sanitizeInput(undefined);
- expect(result).to.equal("undefined");
- });
-});
diff --git a/test/units/utils/sendContactMail/sendContactMail.property.test.js b/test/units/utils/sendContactMail/sendContactMail.property.test.js
deleted file mode 100644
index e164235..0000000
--- a/test/units/utils/sendContactMail/sendContactMail.property.test.js
+++ /dev/null
@@ -1,115 +0,0 @@
-const { expect } = require("chai");
-const sinon = require("sinon");
-const fc = require("fast-check");
-const proxyquire = require("proxyquire");
-
-describe("sendContactMail", () => {
- let sendContactMail;
- let transporterStub;
- let fsStub;
- let validateStub;
- let loggerStub;
- let HttpError;
-
- const MAIL_DOMAIN = "example.com";
- const MAIL_USER = "admin@example.com";
- const DEFAULT_SUBJECT = "Default Subject";
- const EMAIL_LOG_PATH = "/tmp/test-mail-log.json";
-
- beforeEach(() => {
- transporterStub = {
- sendMail: sinon.stub().resolves("OK"),
- };
-
- fsStub = {
- readFile: sinon.stub().resolves("[]"),
- writeFile: sinon.stub().resolves(),
- };
-
- validateStub = sinon.stub().callsFake((email) => ({
- valid: /^[^@]+@[^@]+\.[^@]+$/.test(email),
- email,
- message: "Invalid email",
- }));
-
- loggerStub = { error: sinon.stub() };
-
- HttpError = class extends Error {
- constructor(message, code) {
- super(message);
- this.code = code;
- }
- };
-
- const mod = proxyquire("../../../../src/utils/sendContactMail", {
- "./transporter": transporterStub,
- fs: { promises: fsStub },
- path: require("path"),
- "../utils/emailValidator": { validateAndSanitizeEmail: validateStub },
- "../utils/logging": { winstonLogger: loggerStub },
- "../config/emailConfig": {
- MAIL_DOMAIN,
- MAIL_USER,
- DEFAULT_SUBJECT,
- EMAIL_LOG_PATH,
- },
- "./HttpError": HttpError,
- });
-
- sendContactMail = mod.sendContactMail;
- });
-
- it("should send mail and write log for any valid email", async () => {
- await fc.assert(
- fc.asyncProperty(
- fc.record({
- name: fc.string(),
- email: fc.emailAddress(),
- subject: fc.option(fc.string(), { nil: undefined }),
- message: fc.string(),
- }),
- async (input) => {
- await sendContactMail(input);
-
- expect(transporterStub.sendMail.calledOnce).to.be.true;
- expect(fsStub.writeFile.calledOnce).to.be.true;
-
- const args = transporterStub.sendMail.firstCall.args[0];
- expect(args).to.include.keys(
- "from",
- "to",
- "replyTo",
- "subject",
- "text"
- );
-
- transporterStub.sendMail.resetHistory();
- fsStub.writeFile.resetHistory();
- }
- )
- );
- });
-
- it("should throw HttpError on invalid email", async () => {
- await fc.assert(
- fc.asyncProperty(
- fc.record({
- name: fc.string(),
- email: fc.string().filter((s) => !/^[^@]+@[^@]+\.[^@]+$/.test(s)), // force invalid
- subject: fc.string(),
- message: fc.string(),
- }),
- async (input) => {
- try {
- await sendContactMail(input);
- expect.fail("Expected HttpError");
- } catch (err) {
- expect(err).to.be.instanceOf(HttpError);
- expect(err.message).to.equal("Invalid email");
- expect(err.code).to.equal(400);
- }
- }
- )
- );
- });
-});
diff --git a/test/units/utils/sendContactMail/sendContactMail.unit.test.js b/test/units/utils/sendContactMail/sendContactMail.unit.test.js
deleted file mode 100644
index f8e3a43..0000000
--- a/test/units/utils/sendContactMail/sendContactMail.unit.test.js
+++ /dev/null
@@ -1,207 +0,0 @@
-const chai = require("chai");
-const sinon = require("sinon");
-const fs = require("fs").promises;
-const path = require("path");
-
-const chaiAsPromised =
- require("chai-as-promised").default || require("chai-as-promised");
-
-const HttpError = require("../../../../src/utils/HttpError");
-
-chai.use(chaiAsPromised);
-const { expect } = chai;
-
-describe("sendContactMail", () => {
- let validateAndSanitizeEmailStub;
- let transporterStub;
- let loggerStub;
- let sendContactMail;
- let fsReadStub;
- let fsWriteStub;
-
- const validInput = {
- name: "Jane Doe",
- email: "jane@example.com",
- subject: "Hello",
- message: "This is a test.",
- };
-
- const mockEmailResponse = {
- accepted: ["admin@example.com"],
- rejected: [],
- response: "250 Message accepted",
- envelope: {
- from: "no-reply@example.com",
- to: ["admin@example.com"],
- },
- messageId: "",
- };
-
- const mockEmailConfig = {
- MAIL_DOMAIN: "example.com",
- MAIL_USER: "admin@example.com",
- DEFAULT_SUBJECT: "New Contact Form Submission",
- EMAIL_LOG_PATH: path.join(__dirname, "../../../data/emails.json"),
- };
-
- beforeEach(() => {
- // Clear module cache
- delete require.cache[
- require.resolve("../../../../src/utils/sendContactMail")
- ];
- delete require.cache[
- require.resolve("../../../../src/utils/emailValidator")
- ];
- delete require.cache[require.resolve("../../../../src/utils/transporter")];
- delete require.cache[require.resolve("../../../../src/utils/logging")];
- delete require.cache[require.resolve("../../../../src/config/emailConfig")];
-
- // Create stubs
- validateAndSanitizeEmailStub = sinon.stub().returns({
- valid: true,
- email: "jane@example.com",
- });
-
- transporterStub = {
- sendMail: sinon.stub().resolves(mockEmailResponse),
- };
-
- loggerStub = {
- error: sinon.stub(),
- };
-
- require.cache[require.resolve("../../../../src/config/emailConfig")] = {
- exports: mockEmailConfig,
- };
-
- // Mock modules in require cache
- require.cache[require.resolve("../../../../src/utils/emailValidator")] = {
- exports: { validateAndSanitizeEmail: validateAndSanitizeEmailStub },
- };
-
- require.cache[require.resolve("../../../../src/utils/transporter")] = {
- exports: transporterStub,
- };
-
- require.cache[require.resolve("../../../../src/utils/logging")] = {
- exports: { winstonLogger: loggerStub },
- };
-
- // Set environment variables
- process.env.MAIL_DOMAIN = "example.com";
- process.env.MAIL_USER = "admin@example.com";
-
- // Create fs stubs
- fsReadStub = sinon.stub(fs, "readFile");
- fsWriteStub = sinon.stub(fs, "writeFile");
-
- // Require the module after mocking
- const module = require("../../../../src/utils/sendContactMail");
- sendContactMail = module.sendContactMail;
- });
-
- afterEach(() => {
- sinon.restore();
- });
-
- it("sends an email with valid input", async () => {
- fsReadStub.resolves("[]");
- fsWriteStub.resolves();
-
- const result = await sendContactMail(validInput);
-
- // Check the result matches what transporter.sendMail returns
- expect(result).to.deep.equal(mockEmailResponse);
-
- // Verify transporter.sendMail was called once
- expect(transporterStub.sendMail.calledOnce).to.be.true;
-
- // Check the email parameters
- const sendArgs = transporterStub.sendMail.getCall(0).args[0];
- expect(sendArgs.from).to.equal('"Contact Form" ');
- expect(sendArgs.to).to.equal("admin@example.com");
- expect(sendArgs.replyTo).to.equal('"Jane Doe" ');
- expect(sendArgs.subject).to.equal("Hello");
- expect(sendArgs.text).to.equal("This is a test.");
-
- // Verify file operations
- expect(fsReadStub.calledOnce).to.be.true;
- expect(fsWriteStub.calledOnce).to.be.true;
- });
-
- it("uses default subject if none provided", async () => {
- const input = { ...validInput, subject: undefined };
- fsReadStub.resolves("[]");
- fsWriteStub.resolves();
-
- const result = await sendContactMail(input);
-
- // Check that we get the mock response, not an empty object
- expect(result).to.deep.equal(mockEmailResponse);
-
- const args = transporterStub.sendMail.firstCall.args[0];
- expect(args.subject).to.equal("New Contact Form Submission");
- });
-
- it("throws HttpError on invalid email", async () => {
- validateAndSanitizeEmailStub.returns({
- valid: false,
- message: "Invalid email format",
- });
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
- });
-
- it("logs and rethrows on readFile error", async () => {
- const error = new Error("Disk failure");
- fsReadStub.rejects(error);
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(
- "Disk failure"
- );
-
- // Verify logger was called
- expect(loggerStub.error.calledOnce).to.be.true;
- expect(loggerStub.error.calledWith("Failed to log email to file:", error))
- .to.be.true;
- });
-
- it("throws on invalid email without message", async () => {
- validateAndSanitizeEmailStub.returns({
- valid: false,
- message: undefined,
- });
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
- });
-
- it("handles non-empty existing log file", async () => {
- const existingLogs = JSON.stringify([{ test: true }]);
- fsReadStub.resolves(existingLogs);
- fsWriteStub.resolves();
-
- const result = await sendContactMail(validInput);
- expect(result).to.deep.equal(mockEmailResponse);
-
- // Verify the log was written with existing data plus new entry
- expect(fsWriteStub.calledOnce).to.be.true;
- const writtenData = JSON.parse(fsWriteStub.firstCall.args[1]);
- expect(writtenData).to.be.an("array").with.lengthOf(2);
- expect(writtenData[0]).to.deep.equal({ test: true });
- });
-
- it("throws if writing log file fails", async () => {
- fsReadStub.resolves("[]");
- const error = new Error("Write failed");
- fsWriteStub.rejects(error);
-
- await expect(sendContactMail(validInput)).to.be.rejectedWith(
- "Write failed"
- );
-
- // Verify logger was called
- expect(loggerStub.error.calledOnce).to.be.true;
- expect(loggerStub.error.calledWith("Failed to log email to file:", error))
- .to.be.true;
- });
-});
diff --git a/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js b/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js
deleted file mode 100644
index 9a30e5d..0000000
--- a/test/units/utils/sendNewsletterSubscriptionMail.unit.test.js
+++ /dev/null
@@ -1,51 +0,0 @@
-// test/units/utils/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);
- });
-});
diff --git a/test/utils/emailValidator/validaterAndSanitizeEmail.property.test.js b/test/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
new file mode 100644
index 0000000..678583f
--- /dev/null
+++ b/test/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
@@ -0,0 +1,93 @@
+// test/validateAndSanitizeEmail.fastcheck.test.js
+const { expect } = require("chai");
+const fc = require("fast-check");
+const validator = require("validator");
+
+const {
+ validateAndSanitizeEmail,
+ MESSAGES,
+ MAX_EMAIL_LENGTH,
+} = require("../../../src/utils/emailValidator");
+
+describe("validateAndSanitizeEmail - fast-check property-based tests", () => {
+ it("should not throw for arbitrary strings", () => {
+ fc.assert(
+ fc.property(fc.string(), (str) => {
+ const result = validateAndSanitizeEmail(str);
+ expect(result).to.have.property("valid").that.is.a("boolean");
+ if (result.valid) {
+ expect(result).to.have.property("email").that.is.a("string");
+ expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
+ expect(validator.isEmail(result.email)).to.equal(true);
+ } else {
+ expect(result).to.have.property("message").that.is.a("string");
+ expect(Object.values(MESSAGES)).to.include(result.message);
+ }
+ })
+ );
+ });
+
+ it("should always return valid=true for valid, normalized, RFC-compliant email addresses", () => {
+ fc.assert(
+ fc.property(fc.emailAddress(), (email) => {
+ const result = validateAndSanitizeEmail(email);
+ expect(result.valid).to.equal(true);
+ expect(result.email).to.be.a("string");
+ expect(validator.isEmail(result.email)).to.equal(true);
+ expect(result.email.length).to.be.at.most(MAX_EMAIL_LENGTH);
+ expect(result.email.includes("..")).to.equal(false);
+ expect(result.email.startsWith(".")).to.equal(false);
+ expect(result.email.endsWith(".")).to.equal(false);
+ })
+ );
+ });
+
+ it("should reject emails longer than MAX_EMAIL_LENGTH", () => {
+ const longLocal = "a".repeat(64);
+ const longDomain = "b".repeat(MAX_EMAIL_LENGTH);
+ const longEmail = `${longLocal}@${longDomain}.com`; // definitely > 320
+
+ const result = validateAndSanitizeEmail(longEmail);
+ expect(result.valid).to.equal(false);
+ expect(result.message).to.equal(MESSAGES.TOO_LONG);
+ });
+
+ it("should reject strings that normalize to null", () => {
+ const nonEmailInput = "invalid input string";
+
+ const result = validateAndSanitizeEmail(nonEmailInput);
+ if (result.valid) {
+ expect(result.email).to.be.a("string");
+ } else {
+ expect([MESSAGES.INVALID, MESSAGES.REQUIRED]).to.include(result.message);
+ }
+ });
+
+ it('should reject emails with ".." in them', () => {
+ fc.assert(
+ fc.property(fc.emailAddress(), (email) => {
+ const mutated = email.replace("@", "..@");
+ const result = validateAndSanitizeEmail(mutated);
+ expect(result.valid).to.equal(false);
+ expect(result.message).to.equal(MESSAGES.INVALID);
+ })
+ );
+ });
+
+ it('should reject emails starting or ending with "."', () => {
+ fc.assert(
+ fc.property(fc.emailAddress(), (email) => {
+ const startDot = `.${email}`;
+ const endDot = `${email}.`;
+
+ const result1 = validateAndSanitizeEmail(startDot);
+ const result2 = validateAndSanitizeEmail(endDot);
+
+ expect(result1.valid).to.equal(false);
+ expect(result2.valid).to.equal(false);
+ expect(result1.message).to.equal(MESSAGES.INVALID);
+ expect(result2.message).to.equal(MESSAGES.INVALID);
+ })
+ );
+ });
+});
diff --git a/test/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js b/test/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
new file mode 100644
index 0000000..927eef1
--- /dev/null
+++ b/test/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
@@ -0,0 +1,148 @@
+// test/utils/emailValidator/validateAndSanitizeEmail.test.js
+const { expect } = require("chai");
+const sinon = require("sinon");
+const validator = require("validator");
+
+const {
+ validateAndSanitizeEmail,
+ MESSAGES,
+ MAX_EMAIL_LENGTH,
+} = require("../../../src/utils/emailValidator");
+
+describe("validateAndSanitizeEmail", () => {
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it("should return REQUIRED if input is undefined", () => {
+ const result = validateAndSanitizeEmail(undefined);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
+ });
+
+ it("should return REQUIRED if input is null", () => {
+ const result = validateAndSanitizeEmail(null);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
+ });
+
+ it("should return REQUIRED if input is a non-string type (number)", () => {
+ const result = validateAndSanitizeEmail(123);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
+ });
+
+ it("should return REQUIRED if input is an empty string", () => {
+ const result = validateAndSanitizeEmail("");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.REQUIRED });
+ });
+
+ it("should return INVALID if normalized email is null (validator.normalizeEmail returns null)", () => {
+ sinon.stub(validator, "normalizeEmail").returns(null);
+ const result = validateAndSanitizeEmail("notanemail");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return INVALID if normalized email is not valid (validator.isEmail returns false)", () => {
+ sinon.stub(validator, "normalizeEmail").returns("invalid@domain");
+ sinon.stub(validator, "isEmail").returns(false);
+ const result = validateAndSanitizeEmail("invalid@domain");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return TOO_LONG if email exceeds MAX_EMAIL_LENGTH", () => {
+ const localPart = "a".repeat(64);
+ const domain = "b".repeat(255 - localPart.length - 1); // keep it under 320 when combined
+ const email = `${localPart}@${domain}.com`;
+
+ const tooLongEmail = `${email}${"x".repeat(MAX_EMAIL_LENGTH - email.length + 1)}`; // force >320
+
+ sinon.stub(validator, "normalizeEmail").returns(tooLongEmail);
+ sinon.stub(validator, "isEmail").returns(true);
+
+ const result = validateAndSanitizeEmail(tooLongEmail);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.TOO_LONG });
+ });
+
+ it('should return INVALID if email contains ".."', () => {
+ const badEmail = "test..dot@example.com";
+ sinon.stub(validator, "normalizeEmail").returns(badEmail);
+ sinon.stub(validator, "isEmail").returns(true);
+
+ const result = validateAndSanitizeEmail(badEmail);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it('should return INVALID if email starts with "."', () => {
+ const badEmail = ".start@example.com";
+ sinon.stub(validator, "normalizeEmail").returns(badEmail);
+ sinon.stub(validator, "isEmail").returns(true);
+
+ const result = validateAndSanitizeEmail(badEmail);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it('should return INVALID if email ends with "."', () => {
+ const badEmail = "end.@example.com";
+ sinon.stub(validator, "normalizeEmail").returns(badEmail);
+ sinon.stub(validator, "isEmail").returns(true);
+
+ const result = validateAndSanitizeEmail(badEmail);
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return valid email if all conditions are satisfied", () => {
+ const rawEmail = " John.Doe@Example.com ";
+ const normalized = "john.doe@example.com";
+
+ sinon.stub(validator, "normalizeEmail").callsFake((email) => {
+ // simulate trimming + lowercasing + normalization
+ return email === "john.doe@example.com" ? normalized : null;
+ });
+
+ sinon.stub(validator, "isEmail").returns(true);
+
+ const result = validateAndSanitizeEmail(rawEmail);
+ expect(result).to.deep.equal({ valid: true, email: normalized });
+ });
+
+ it('should return INVALID for email with multiple "@" characters', () => {
+ const result = validateAndSanitizeEmail("john@doe@example.com");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it('should return INVALID for email with no "@" character', () => {
+ const result = validateAndSanitizeEmail("johndoe.example.com");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return VALID for a minimally valid email address", () => {
+ const result = validateAndSanitizeEmail("a@b.co");
+ expect(result.valid).to.equal(true);
+ expect(result.email).to.be.a("string");
+ });
+
+ it("should return INVALID for email with spaces in local part", () => {
+ const result = validateAndSanitizeEmail("john doe@example.com");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it('should return INVALID for email with space after "@"', () => {
+ const result = validateAndSanitizeEmail("john@ example.com");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return INVALID for email with quoted local part (validator accepts, you might not)", () => {
+ const result = validateAndSanitizeEmail('"john.doe"@example.com');
+ // Accept if validator does; reject if you disallow quotes
+ // Adjust depending on your business rules
+ expect(result.valid).to.equal(true);
+ });
+
+ it("should return INVALID for email with emoji in local part", () => {
+ const result = validateAndSanitizeEmail("🧟@example.com");
+ expect(result).to.deep.equal({ valid: false, message: MESSAGES.INVALID });
+ });
+
+ it("should return VALID for email with subdomain", () => {
+ const result = validateAndSanitizeEmail("user@sub.example.com");
+ expect(result.valid).to.equal(true);
+ });
+});
diff --git a/test/utils/logging/config.test.js b/test/utils/logging/config.test.js
index 7b6b82a..33dd3ab 100644
--- a/test/utils/logging/config.test.js
+++ b/test/utils/logging/config.test.js
@@ -21,7 +21,7 @@
});
it("projectRoot matches resolved 3-levels-up path", () => {
- const expected = path.resolve(__dirname, "../../../../");
+ const expected = path.resolve(__dirname, "../../../");
expect(projectRoot).to.equal(expected);
});
@@ -68,10 +68,7 @@
const original = process.env.LOG_LEVEL;
delete process.env.LOG_LEVEL;
- const { LOG_LEVEL } = proxyquire(
- "../../../../src/utils/logging/config",
- {}
- );
+ 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/utils/logging/config.unit.test.js b/test/utils/logging/config.unit.test.js
new file mode 100644
index 0000000..f3be6ef
--- /dev/null
+++ b/test/utils/logging/config.unit.test.js
@@ -0,0 +1,78 @@
+// 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,
+ event: 2,
+ security: 3,
+ notice: 4,
+ info: 5,
+ debug: 6,
+ analytics: 7,
+ });
+ });
+
+ 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/utils/logging/createLogStreams.test.js b/test/utils/logging/createLogStreams.test.js
index 253578c..784902f 100644
--- a/test/utils/logging/createLogStreams.test.js
+++ b/test/utils/logging/createLogStreams.test.js
@@ -2,16 +2,19 @@
const fs = require("fs");
const path = require("path");
const { expect } = require("chai");
-const { createLogStreams } = require("../../../src/utils/logging");
+const { createLogStreams } = require("../../../src/utils/logging/streams");
describe("createLogStreams", () => {
- const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs");
+ const testDir = path.join(__dirname, "..", "..", "logs");
const files = {
info: path.join(testDir, "info.log"),
error: path.join(testDir, "error.log"),
warn: path.join(testDir, "warn.log"),
+ event: path.join(testDir, "event.log"),
+ security: path.join(testDir, "security.log"),
notice: path.join(testDir, "notice.log"),
debug: path.join(testDir, "debug.log"),
+ analytics: path.join(testDir, "analytics.log"),
};
afterEach(() => {
diff --git a/test/utils/logging/createLogStreams.unit.test.js b/test/utils/logging/createLogStreams.unit.test.js
new file mode 100644
index 0000000..7dd9ce2
--- /dev/null
+++ b/test/utils/logging/createLogStreams.unit.test.js
@@ -0,0 +1,34 @@
+// test/units/utils/logging/createLogStreams.test.js
+const fs = require("fs");
+const path = require("path");
+const { expect } = require("chai");
+const { createLogStreams } = require("../../../src/utils/logging/streams");
+
+describe("createLogStreams", () => {
+ const testDir = path.join(__dirname, "..", "..", "logs");
+ const files = {
+ analytics: path.join(testDir, "analytics.log"),
+ error: path.join(testDir, "error.log"),
+ security: path.join(testDir, "security.log"),
+ warn: path.join(testDir, "warn.log"),
+ notice: path.join(testDir, "notice.log"),
+ event: path.join(testDir, "event.log"),
+ info: path.join(testDir, "info.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/utils/logging/formatFunctionName.unit.test.js b/test/utils/logging/formatFunctionName.unit.test.js
new file mode 100644
index 0000000..5c61d09
--- /dev/null
+++ b/test/utils/logging/formatFunctionName.unit.test.js
@@ -0,0 +1,14 @@
+// test/units/utils/logging/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/utils/logging/formatLog.unit.test.js b/test/utils/logging/formatLog.unit.test.js
new file mode 100644
index 0000000..5bf410d
--- /dev/null
+++ b/test/utils/logging/formatLog.unit.test.js
@@ -0,0 +1,35 @@
+const { formatLog } = require("../../../src/utils/logging/consolePatch");
+const { expect } = require("chai");
+
+describe("Logger Format Function Tests", () => {
+ it("should format circular objects without throwing and stringify correctly", () => {
+ const circular = { name: "test" };
+ circular.ref = circular;
+
+ const methods = [
+ "INFO",
+ "EVENT",
+ "WARN",
+ "SECURITY",
+ "ERROR",
+ "DEBUG",
+ "NOTICE",
+ "ANALYTICS",
+ ];
+
+ methods.forEach((level) => {
+ const { timestamp, safeArgs, message, logLine } = formatLog(
+ level,
+ circular
+ );
+
+ expect(timestamp).to.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/);
+ expect(safeArgs).to.be.an("array");
+ expect(message).to.include("name");
+ expect(message).to.include("test");
+ expect(message).to.not.include("[object Object]");
+ expect(logLine).to.include(`[${timestamp}] [${level}]`);
+ expect(logLine).to.include(message);
+ });
+ });
+});
diff --git a/test/utils/logging/formatLogMessage.unit.test.js b/test/utils/logging/formatLogMessage.unit.test.js
new file mode 100644
index 0000000..adb7351
--- /dev/null
+++ b/test/utils/logging/formatLogMessage.unit.test.js
@@ -0,0 +1,15 @@
+// test/units/utils/logging/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/utils/logging/handleUncaughtException.unit.test.js b/test/utils/logging/handleUncaughtException.unit.test.js
new file mode 100644
index 0000000..823ceb4
--- /dev/null
+++ b/test/utils/logging/handleUncaughtException.unit.test.js
@@ -0,0 +1,28 @@
+// test/units/utils/logging/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/utils/logging/handleUnhandledRejection.unit.test.js b/test/utils/logging/handleUnhandledRejection.unit.test.js
new file mode 100644
index 0000000..8bdd3e3
--- /dev/null
+++ b/test/utils/logging/handleUnhandledRejection.unit.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/utils/logging/initializeLogDirectories.unit.test.js b/test/utils/logging/initializeLogDirectories.unit.test.js
new file mode 100644
index 0000000..b61e23a
--- /dev/null
+++ b/test/utils/logging/initializeLogDirectories.unit.test.js
@@ -0,0 +1,47 @@
+// test/units/utils/logging/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",
+ security: "../test/logs/security/security.log",
+ warn: "../test/logs/warn/warn.log",
+ event: "../test/logs/event/event.log",
+ notice: "../test/logs/notice/notice.log",
+ debug: "../test/logs/debug/debug.log",
+ analytics: "../test/logs/analytics/analytics.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/utils/logging/object-formatting.unit.test.js b/test/utils/logging/object-formatting.unit.test.js
new file mode 100644
index 0000000..a7d6d0c
--- /dev/null
+++ b/test/utils/logging/object-formatting.unit.test.js
@@ -0,0 +1,527 @@
+const { expect } = require("chai");
+const sinon = require("sinon");
+const fs = require("fs");
+const path = require("path");
+const { Writable } = require("stream");
+
+// Mock dependencies
+const mockLogStreams = {
+ info: new Writable({ write() {} }),
+ error: new Writable({ write() {} }),
+ warn: new Writable({ write() {} }),
+ debug: new Writable({ write() {} }),
+ notice: new Writable({ write() {} }),
+ security: new Writable({ write() {} }),
+ analytics: new Writable({ write() {} }),
+ event: new Writable({ write() {} }),
+};
+
+const mockSessionTransport = {
+ write: sinon.stub(),
+};
+
+// Import the modules under test
+const { writeLog } = require("../../../src/utils/logging/consolePatch");
+const {
+ manualLogger,
+ winstonLogger,
+} = require("../../../src/utils/logging/index");
+
+describe("Logger Object Expansion Tests", () => {
+ let streamWriteStubs;
+
+ beforeEach(() => {
+ // Create fresh stream write stubs for each test
+ streamWriteStubs = {
+ info: sinon.stub(mockLogStreams.info, "write"),
+ error: sinon.stub(mockLogStreams.error, "write"),
+ warn: sinon.stub(mockLogStreams.warn, "write"),
+ security: sinon.stub(mockLogStreams.security, "write"),
+ event: sinon.stub(mockLogStreams.event, "write"),
+ analytics: sinon.stub(mockLogStreams.analytics, "write"),
+ debug: sinon.stub(mockLogStreams.debug, "write"),
+ notice: sinon.stub(mockLogStreams.notice, "write"),
+ };
+
+ // Reset session transport
+ mockSessionTransport.write.reset();
+ });
+
+ afterEach(() => {
+ // Restore all stubs
+ sinon.restore(); // This restores all stubs created by sinon.stub()
+ });
+
+ describe("writeLog function", () => {
+ let consoleLogStub; // Declare stub for this describe block
+
+ beforeEach(() => {
+ // Stub console.log specifically for this describe block
+ consoleLogStub = sinon.stub(console, "log");
+ });
+
+ afterEach(() => {
+ // Restore console.log stub after each test in this block
+ consoleLogStub.restore();
+ });
+
+ it("should never log [object Object] for simple objects", () => {
+ const testObject = { name: "test", value: 42 };
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ testObject
+ );
+
+ // Check console output
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("name");
+ expect(outputString).to.include("test");
+ expect(outputString).to.include("value");
+ expect(outputString).to.include("42");
+
+ // Check stream output
+ expect(streamWriteStubs.info.called).to.be.true;
+ const streamOutput = streamWriteStubs.info.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("name");
+ expect(streamOutput).to.include("test");
+ });
+
+ it("should properly expand nested objects", () => {
+ const nestedObject = {
+ user: {
+ id: 123,
+ profile: {
+ name: "John Doe",
+ settings: { theme: "dark", notifications: true },
+ },
+ },
+ };
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ nestedObject
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("John Doe");
+ expect(outputString).to.include("theme");
+ expect(outputString).to.include("dark");
+ expect(outputString).to.include("notifications");
+ });
+
+ it("should handle circular references without [object Object]", () => {
+ const circularObj = { name: "circular" };
+ circularObj.self = circularObj;
+ circularObj.nested = { parent: circularObj };
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ circularObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("name");
+ expect(outputString).to.include("circular");
+ // Should handle circular reference gracefully
+ expect(outputString).to.include("self");
+ });
+
+ it("should expand arrays containing objects", () => {
+ const arrayWithObjects = [
+ { id: 1, name: "first" },
+ { id: 2, name: "second", nested: { value: "test" } },
+ ];
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ arrayWithObjects
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("first");
+ expect(outputString).to.include("second");
+ expect(outputString).to.include("nested");
+ expect(outputString).to.include("test");
+ });
+
+ it("should handle mixed argument types without [object Object]", () => {
+ const mixedArgs = [
+ "String message",
+ { obj: "value" },
+ 42,
+ ["array", "items"],
+ { deeply: { nested: { object: "here" } } },
+ ];
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ ...mixedArgs
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("String message");
+ expect(outputString).to.include("obj");
+ expect(outputString).to.include("value");
+ expect(outputString).to.include("deeply");
+ expect(outputString).to.include("here");
+ });
+
+ it("should handle Error objects without [object Object]", () => {
+ const error = new Error("Test error");
+ error.customProperty = { details: "additional info" };
+
+ // Stub console.error for this test (local stub, not interfering with console.log stub)
+ const consoleErrorStub = sinon.stub(console, "error");
+
+ writeLog(
+ "ERROR",
+ mockLogStreams.error,
+ mockSessionTransport,
+ console.error,
+ error
+ );
+
+ expect(consoleErrorStub.called).to.be.true;
+ const consoleArgs = consoleErrorStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("Test error");
+
+ consoleErrorStub.restore();
+ });
+
+ it("should handle objects with special properties", () => {
+ const specialObj = {
+ toString: () => "custom toString",
+ valueOf: () => 99,
+ [Symbol.toStringTag]: "CustomObject",
+ normalProp: "normal value",
+ };
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ specialObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("normalProp");
+ expect(outputString).to.include("normal value");
+ });
+ });
+
+ describe("Manual Logger Methods", () => {
+ let manualLoggerStubs;
+ // Removed writeLogStub as manualLogger directly interacts with console
+
+ beforeEach(() => {
+ // Create fresh stubs for manual logger streams if they exist
+ manualLoggerStubs = {};
+ if (manualLogger.streams) {
+ Object.keys(manualLogger.streams).forEach((level) => {
+ if (
+ manualLogger.streams[level] &&
+ typeof manualLogger.streams[level].write === "function"
+ ) {
+ if (!manualLogger.streams[level].write.isSinonProxy) {
+ manualLoggerStubs[level] = sinon.stub(
+ manualLogger.streams[level],
+ "write"
+ );
+ }
+ }
+ });
+ }
+ });
+
+ afterEach(() => {
+ // Restore all stubs created within this describe block or its tests
+ sinon.restore();
+ });
+
+ it("should not produce [object Object] in manualLogger.info", () => {
+ const testObj = { key: "value", nested: { deep: "property" } };
+
+ // Stub console.log locally for this specific test
+ const consoleLogStub = sinon.stub(console, "log");
+ manualLogger.info(testObj);
+
+ expect(consoleLogStub.called).to.be.true; // Check if console.log was called
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" "); // Join them to check content
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("key");
+ expect(outputString).to.include("nested");
+ expect(outputString).to.include("deep");
+ consoleLogStub.restore(); // Restore after test
+ });
+
+ it("should not produce [object Object] in manualLogger.error", () => {
+ const errorObj = {
+ error: "Something went wrong",
+ context: { userId: 123, action: "login" },
+ };
+
+ // Stub console.error for this test
+ const consoleErrorStub = sinon.stub(console, "error");
+
+ manualLogger.error(errorObj);
+
+ expect(consoleErrorStub.called).to.be.true;
+ const consoleArgs = consoleErrorStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("Something went wrong");
+ expect(outputString).to.include("userId");
+ expect(outputString).to.include("login");
+
+ consoleErrorStub.restore();
+ });
+ });
+
+ describe("Winston Logger", () => {
+ let winstonInfoStub;
+
+ beforeEach(() => {
+ winstonInfoStub = sinon.stub(winstonLogger, "info");
+ });
+
+ afterEach(() => {
+ winstonInfoStub.restore(); // Ensure winston stub is restored
+ });
+
+ it("should not produce [object Object] in winston logs", () => {
+ const logData = {
+ user: { id: 456, name: "Jane" },
+ action: "update",
+ metadata: { timestamp: Date.now() },
+ };
+
+ winstonLogger.info("User action", logData);
+
+ // Check that winston was called with properly formatted data
+ expect(winstonInfoStub.called).to.be.true;
+ const logCall = winstonInfoStub.getCall(0).args;
+ // Winston typically stringifies objects, so we check the stringified output
+ const logString = JSON.stringify(logCall);
+ expect(logString).to.not.include("[object Object]");
+ expect(logString).to.include("Jane");
+ expect(logString).to.include("update");
+ });
+ });
+
+ describe("Edge Cases", () => {
+ let consoleLogStub; // Declare stub for this describe block
+
+ beforeEach(() => {
+ // Stub console.log specifically for this describe block
+ consoleLogStub = sinon.stub(console, "log");
+ });
+
+ afterEach(() => {
+ // Restore console.log stub after each test in this block
+ consoleLogStub.restore();
+ });
+
+ it("should handle null and undefined without [object Object]", () => {
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ null,
+ undefined
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("null");
+ expect(outputString).to.include("undefined");
+ });
+
+ it("should handle objects with null prototype", () => {
+ const nullProtoObj = Object.create(null);
+ nullProtoObj.key = "value";
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ nullProtoObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("key");
+ expect(outputString).to.include("value");
+ });
+
+ it("should handle Date objects", () => {
+ const dateObj = new Date("2023-01-01T12:00:00.000Z"); // Use ISO string for consistent output
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ dateObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ // Check for parts of the date string that are likely to be present in ISO format
+ // Console.log's output for Date objects can vary, but the ISO string is often included or derived.
+ expect(outputString).to.include("2023");
+ // Check for the time part of the ISO string for more robustness
+ expect(outputString).to.include("T12:00:00.000Z");
+ });
+
+ it("should handle RegExp objects", () => {
+ const regexObj = /test.*pattern/gi;
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ regexObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("test");
+ expect(outputString).to.include("pattern");
+ expect(outputString).to.include("gi"); // Check for flags
+ });
+
+ it("should handle very deeply nested objects", () => {
+ let deepObj = { level: 0 };
+ let current = deepObj;
+
+ // Create 10 levels deep
+ for (let i = 1; i <= 10; i++) {
+ current.next = { level: i };
+ current = current.next;
+ }
+
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ deepObj
+ );
+
+ expect(consoleLogStub.called).to.be.true;
+ const consoleArgs = consoleLogStub.getCall(0).args;
+ expect(consoleArgs).to.exist;
+ const outputString = consoleArgs.join(" ");
+ expect(outputString).to.not.include("[object Object]");
+ expect(outputString).to.include("level");
+ // Check for presence of multiple levels
+ expect(outputString.match(/level/g).length).to.be.at.least(10);
+ });
+ });
+
+ describe("Stream Output Validation", () => {
+ let consoleLogStub; // Declare stub for this describe block
+
+ beforeEach(() => {
+ // Stub console.log specifically for this describe block
+ consoleLogStub = sinon.stub(console, "log");
+ });
+
+ afterEach(() => {
+ // Restore console.log stub after each test in this block
+ consoleLogStub.restore();
+ });
+
+ it("should ensure stream writes never contain [object Object]", () => {
+ const testObjects = [
+ { simple: "object" },
+ { nested: { deep: { value: "test" } } },
+ [{ array: "item" }],
+ { mixed: ["array", { in: "object" }] },
+ ];
+
+ testObjects.forEach((obj, index) => {
+ streamWriteStubs.info.resetHistory(); // Reset history for each iteration
+ writeLog(
+ "INFO",
+ mockLogStreams.info,
+ mockSessionTransport,
+ console.log,
+ obj
+ );
+
+ expect(streamWriteStubs.info.called).to.be.true;
+ const streamWrites = streamWriteStubs.info.getCalls();
+ streamWrites.forEach((call) => {
+ const writeData = call.args[0];
+ // Ensure the written data is a string and does not contain "[object Object]"
+ expect(typeof writeData).to.equal("string");
+ expect(writeData).to.not.include("[object Object]");
+ });
+ });
+ });
+ });
+});
diff --git a/test/utils/logging/writeLog.unit.test.js b/test/utils/logging/writeLog.unit.test.js
new file mode 100644
index 0000000..96624ff
--- /dev/null
+++ b/test/utils/logging/writeLog.unit.test.js
@@ -0,0 +1,341 @@
+// test/units/utils/logging/writeLog.test.js
+const { expect } = require("chai");
+const sinon = require("sinon");
+const { writeLog } = require("../../../src/utils/logging/consolePatch");
+
+describe("writeLog - Object Expansion Tests", () => {
+ 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();
+ sessionTransport = { write: sinon.spy() };
+ clock = sinon.useFakeTimers(fixedDate.getTime());
+ });
+
+ afterEach(() => {
+ clock.restore();
+ sinon.restore();
+ });
+
+ describe("prevents [object Object] output", () => {
+ it("expands simple objects instead of showing [object Object]", () => {
+ const testObject = { name: "test", value: 42 };
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, testObject);
+
+ const expectedTimestamp = fixedDate.toISOString();
+
+ // Check stream output doesn't contain [object Object]
+ expect(stream.write.called).to.be.true;
+ const streamCall = stream.write.getCall(0);
+ const streamOutput = streamCall.args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("name");
+ expect(streamOutput).to.include("test");
+ expect(streamOutput).to.include("value");
+ expect(streamOutput).to.include("42");
+
+ // Check console output doesn't contain [object Object]
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleArgs = consoleCall.args;
+ expect(consoleArgs).to.exist;
+ expect(Array.isArray(consoleArgs)).to.be.true;
+ const consoleOutput = consoleArgs.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleArgs).to.include.members([`[${expectedTimestamp}] [INFO]`]);
+
+ // Check sessionTransport message doesn't contain [object Object]
+ expect(sessionTransport.write.called).to.be.true;
+ const sessionCall = sessionTransport.write.getCall(0);
+ const sessionData = sessionCall.args[0];
+ expect(sessionData.message).to.not.include("[object Object]");
+ expect(sessionData.message).to.include("name");
+ expect(sessionData.message).to.include("test");
+ });
+
+ it("expands nested objects completely", () => {
+ const nestedObject = {
+ user: {
+ id: 123,
+ profile: {
+ name: "John Doe",
+ settings: { theme: "dark", notifications: true },
+ },
+ },
+ };
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, nestedObject);
+
+ // Check all outputs expand the nested structure
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("John Doe");
+ expect(streamOutput).to.include("theme");
+ expect(streamOutput).to.include("dark");
+ expect(streamOutput).to.include("notifications");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleOutput).to.include("John Doe");
+ expect(consoleOutput).to.include("theme");
+
+ expect(sessionTransport.write.called).to.be.true;
+ const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
+ expect(sessionMessage).to.not.include("[object Object]");
+ expect(sessionMessage).to.include("John Doe");
+ });
+
+ it("expands arrays containing objects", () => {
+ const arrayWithObjects = [
+ { id: 1, name: "first" },
+ { id: 2, name: "second", nested: { value: "test" } },
+ ];
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, arrayWithObjects);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("first");
+ expect(streamOutput).to.include("second");
+ expect(streamOutput).to.include("nested");
+ expect(streamOutput).to.include("test");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleOutput).to.include("first");
+ expect(consoleOutput).to.include("second");
+ });
+
+ it("handles mixed argument types without [object Object]", () => {
+ const mixedArgs = [
+ "String message",
+ { obj: "value" },
+ 42,
+ ["array", "items"],
+ { deeply: { nested: { object: "here" } } },
+ ];
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, ...mixedArgs);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("String message");
+ expect(streamOutput).to.include("obj");
+ expect(streamOutput).to.include("value");
+ expect(streamOutput).to.include("deeply");
+ expect(streamOutput).to.include("here");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleOutput).to.include("String message");
+ expect(consoleOutput).to.include("obj");
+ });
+
+ it("expands Error objects properly", () => {
+ const error = new Error("Test error");
+ error.customProperty = { details: "additional info" };
+
+ writeLog("ERROR", stream, sessionTransport, consoleFn, error);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("Test error");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleOutput).to.include("Test error");
+ });
+
+ it("handles objects with special properties", () => {
+ const specialObj = {
+ toString: () => "custom toString",
+ valueOf: () => 99,
+ normalProp: "normal value",
+ anotherProp: { nested: "data" },
+ };
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, specialObj);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("normalProp");
+ expect(streamOutput).to.include("normal value");
+ expect(streamOutput).to.include("nested");
+ expect(streamOutput).to.include("data");
+
+ expect(sessionTransport.write.called).to.be.true;
+ const sessionMessage = sessionTransport.write.getCall(0).args[0].message;
+ expect(sessionMessage).to.not.include("[object Object]");
+ expect(sessionMessage).to.include("normalProp");
+ });
+ });
+
+ describe("edge cases", () => {
+ it("handles null and undefined without [object Object]", () => {
+ writeLog("INFO", stream, sessionTransport, consoleFn, null, undefined);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("null");
+ expect(streamOutput).to.include("undefined");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ });
+
+ it("handles Date objects", () => {
+ const dateObj = new Date("2023-01-01");
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, dateObj);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("2023");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ });
+
+ it("handles RegExp objects", () => {
+ const regexObj = /test.*pattern/gi;
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, regexObj);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("test");
+ expect(streamOutput).to.include("pattern");
+ });
+
+ it("handles objects with null prototype", () => {
+ const nullProtoObj = Object.create(null);
+ nullProtoObj.key = "value";
+ nullProtoObj.nested = { prop: "data" };
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, nullProtoObj);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("key");
+ expect(streamOutput).to.include("value");
+ expect(streamOutput).to.include("prop");
+ expect(streamOutput).to.include("data");
+ });
+
+ it("handles very deeply nested objects", () => {
+ let deepObj = { level: 0 };
+ let current = deepObj;
+
+ // Create 5 levels deep (reasonable for testing)
+ for (let i = 1; i <= 5; i++) {
+ current.next = { level: i, data: `level${i}data` };
+ current = current.next;
+ }
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, deepObj);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("level");
+ expect(streamOutput).to.include("level5data");
+ });
+ });
+
+ describe("different log levels", () => {
+ const levels = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"];
+
+ levels.forEach((level) => {
+ it(`expands objects properly for ${level} level`, () => {
+ const testObj = {
+ level: level.toLowerCase(),
+ data: { nested: "value" },
+ array: [{ item: "test" }],
+ };
+
+ writeLog(level, stream, sessionTransport, consoleFn, testObj);
+
+ // Only check if the function was called for levels that should log
+ if (stream.write.called) {
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("nested");
+ expect(streamOutput).to.include("value");
+ expect(streamOutput).to.include("item");
+ expect(streamOutput).to.include("test");
+ }
+
+ if (sessionTransport.write.called) {
+ const sessionMessage =
+ sessionTransport.write.getCall(0).args[0].message;
+ expect(sessionMessage).to.not.include("[object Object]");
+ expect(sessionMessage).to.include("nested");
+ }
+ });
+ });
+ });
+
+ describe("multiple objects in single call", () => {
+ it("expands all objects in arguments", () => {
+ const obj1 = { first: "object", nested: { value: 1 } };
+ const obj2 = { second: "object", array: [{ item: "test" }] };
+ const obj3 = { third: { deeply: { nested: "value" } } };
+
+ writeLog("INFO", stream, sessionTransport, consoleFn, obj1, obj2, obj3);
+
+ expect(stream.write.called).to.be.true;
+ const streamOutput = stream.write.getCall(0).args[0];
+ expect(streamOutput).to.not.include("[object Object]");
+ expect(streamOutput).to.include("first");
+ expect(streamOutput).to.include("second");
+ expect(streamOutput).to.include("third");
+ expect(streamOutput).to.include("deeply");
+ expect(streamOutput).to.include("nested");
+ expect(streamOutput).to.include("item");
+ expect(streamOutput).to.include("test");
+
+ expect(consoleFn.called).to.be.true;
+ const consoleCall = consoleFn.getCall(0);
+ expect(consoleCall).to.exist;
+ const consoleOutput = consoleCall.args.join(" ");
+ expect(consoleOutput).to.not.include("[object Object]");
+ expect(consoleOutput).to.include("first");
+ expect(consoleOutput).to.include("second");
+ expect(consoleOutput).to.include("third");
+ });
+ });
+});
diff --git a/test/utils/sendContactMail/sanitizeInput.property.test.js b/test/utils/sendContactMail/sanitizeInput.property.test.js
new file mode 100644
index 0000000..0c03f85
--- /dev/null
+++ b/test/utils/sendContactMail/sanitizeInput.property.test.js
@@ -0,0 +1,175 @@
+const { expect } = require("chai");
+const fc = require("fast-check");
+const { sanitizeInput } = require("../../../src/utils/sendContactMail");
+
+describe("sanitizeInput", () => {
+ it("should remove all newline, carriage return, and angle brackets", () => {
+ fc.assert(
+ fc.property(fc.string(), (str) => {
+ const result = sanitizeInput(str);
+ expect(result).to.not.include("\r");
+ expect(result).to.not.include("\n");
+ expect(result).to.not.include("<");
+ expect(result).to.not.include(">");
+ })
+ );
+ });
+
+ it("should return a string for any input", () => {
+ fc.assert(
+ fc.property(
+ fc.anything(), // This can generate any value, including complex objects
+ (input) => {
+ const result = sanitizeInput(input);
+ expect(typeof result).to.equal("string");
+ }
+ )
+ );
+ });
+
+ it("should preserve safe characters when only safe characters are present", () => {
+ fc.assert(
+ fc.property(
+ fc.string({
+ // ONLY include characters that won't be filtered out
+ // Explicitly exclude: \r, \n, <, >
+ minLength: 0, // Allow empty string
+ maxLength: 100, // Or some reasonable max length for your test
+ // Generate strings of characters that are NOT \r, \n, <, or >
+ // We'll filter the characters after generation
+ // For individual characters, you'd typically generate a string of length 1
+ // and then map/filter the characters from that string.
+ // A common pattern is to use fc.array(fc.char()), but fc.char() doesn't exist.
+ // So, we'll generate strings with specific character sets, or filter a broader set.
+ }),
+ (input) => {
+ // Filter the input string to ensure it only contains allowed characters
+ // This is a common pattern when specific character sets are needed
+ const filteredInput = [...input]
+ .filter((c) => !["\r", "\n", "<", ">"].includes(c))
+ .join("");
+
+ const result = sanitizeInput(filteredInput); // Pass the filtered input to your sanitize function
+ expect(result).to.equal(filteredInput.trim());
+ }
+ )
+ );
+ });
+
+ // You might still want a separate test for only "safe" characters
+ // where it confirms that characters *not* in the removal list are preserved.
+ it("should preserve characters NOT in the removal list when only such characters are present", () => {
+ fc.assert(
+ fc.property(
+ fc.string({
+ // ONLY include characters that *should not* be filtered out
+ characters:
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;'\":,./?`~ \t", // Excludes \r, \n, <, >
+ }),
+ (input) => {
+ if (/[<>\r\n]/.test(input)) fc.pre(false); // Reject inputs with excluded chars
+ const result = sanitizeInput(input);
+ // For inputs that *only* contain allowed characters,
+ // the result should simply be the trimmed version of the input.
+ expect(result).to.equal(input.trim());
+ }
+ )
+ );
+ });
+
+ it("should remove carriage returns, newlines, angle brackets, and trim whitespace", () => {
+ fc.assert(
+ fc.property(
+ fc.string({
+ // Include characters that are *expected* to be removed by sanitizeInput
+ // along with regular safe characters to ensure comprehensive testing.
+ characters:
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 !@#$%^&*()_+-=[]{}|;':\",./?`~<>\\r\\n \t", // Added spaces and tabs for trim testing
+ }),
+ (input) => {
+ const result = sanitizeInput(input);
+
+ // Manually apply the *expected* sanitization logic to create the expected output.
+ // This should match precisely what sanitizeInput is designed to do.
+ const expectedOutput = String(input)
+ .replace(/[\r\n<>]/g, "") // Remove \r, \n, <, >
+ .trim(); // Trim whitespace
+
+ expect(result).to.equal(expectedOutput);
+ }
+ )
+ );
+ });
+
+ it("should remove dangerous characters from any string", () => {
+ fc.assert(
+ fc.property(
+ fc.string(), // Any string
+ (input) => {
+ const result = sanitizeInput(input);
+
+ // Result should be a string
+ expect(typeof result).to.equal("string");
+
+ // Result should not contain dangerous characters
+ expect(result).to.not.include("\r");
+ expect(result).to.not.include("\n");
+ expect(result).to.not.include("<");
+ expect(result).to.not.include(">");
+
+ // Result should be trimmed
+ expect(result).to.equal(result.trim());
+ }
+ )
+ );
+ });
+
+ it("should handle edge cases correctly", () => {
+ const testCases = [
+ { input: "", expected: "" },
+ { input: null, expected: "null" },
+ { input: undefined, expected: "undefined" },
+ { input: " ", expected: "" },
+ { input: "hello");
+ expect(result).to.equal("scriptalert(1)/script");
+ });
+
+ it("trims leading and trailing spaces", () => {
+ const result = sanitizeInput(" test input ");
+ expect(result).to.equal("test input");
+ });
+
+ it("coerces non-strings to strings", () => {
+ const result = sanitizeInput(12345);
+ expect(result).to.equal("12345");
+ });
+
+ it("returns empty string for null", () => {
+ const result = sanitizeInput(null);
+ expect(result).to.equal("null");
+ });
+
+ it("returns empty string for undefined", () => {
+ const result = sanitizeInput(undefined);
+ expect(result).to.equal("undefined");
+ });
+});
diff --git a/test/utils/sendContactMail/sendContactMail.property.test.js b/test/utils/sendContactMail/sendContactMail.property.test.js
new file mode 100644
index 0000000..ffef81c
--- /dev/null
+++ b/test/utils/sendContactMail/sendContactMail.property.test.js
@@ -0,0 +1,115 @@
+const { expect } = require("chai");
+const sinon = require("sinon");
+const fc = require("fast-check");
+const proxyquire = require("proxyquire");
+
+describe("sendContactMail", () => {
+ let sendContactMail;
+ let transporterStub;
+ let fsStub;
+ let validateStub;
+ let loggerStub;
+ let HttpError;
+
+ const MAIL_DOMAIN = "example.com";
+ const MAIL_USER = "admin@example.com";
+ const DEFAULT_SUBJECT = "Default Subject";
+ const EMAIL_LOG_PATH = "/tmp/test-mail-log.json";
+
+ beforeEach(() => {
+ transporterStub = {
+ sendMail: sinon.stub().resolves("OK"),
+ };
+
+ fsStub = {
+ readFile: sinon.stub().resolves("[]"),
+ writeFile: sinon.stub().resolves(),
+ };
+
+ validateStub = sinon.stub().callsFake((email) => ({
+ valid: /^[^@]+@[^@]+\.[^@]+$/.test(email),
+ email,
+ message: "Invalid email",
+ }));
+
+ loggerStub = { error: sinon.stub() };
+
+ HttpError = class extends Error {
+ constructor(message, code) {
+ super(message);
+ this.code = code;
+ }
+ };
+
+ const mod = proxyquire("../../../src/utils/sendContactMail", {
+ "./transporter": transporterStub,
+ fs: { promises: fsStub },
+ path: require("path"),
+ "../utils/emailValidator": { validateAndSanitizeEmail: validateStub },
+ "../utils/logging": { winstonLogger: loggerStub },
+ "../config/emailConfig": {
+ MAIL_DOMAIN,
+ MAIL_USER,
+ DEFAULT_SUBJECT,
+ EMAIL_LOG_PATH,
+ },
+ "./HttpError": HttpError,
+ });
+
+ sendContactMail = mod.sendContactMail;
+ });
+
+ it("should send mail and write log for any valid email", async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ fc.record({
+ name: fc.string(),
+ email: fc.emailAddress(),
+ subject: fc.option(fc.string(), { nil: undefined }),
+ message: fc.string(),
+ }),
+ async (input) => {
+ await sendContactMail(input);
+
+ expect(transporterStub.sendMail.calledOnce).to.be.true;
+ expect(fsStub.writeFile.calledOnce).to.be.true;
+
+ const args = transporterStub.sendMail.firstCall.args[0];
+ expect(args).to.include.keys(
+ "from",
+ "to",
+ "replyTo",
+ "subject",
+ "text"
+ );
+
+ transporterStub.sendMail.resetHistory();
+ fsStub.writeFile.resetHistory();
+ }
+ )
+ );
+ });
+
+ it("should throw HttpError on invalid email", async () => {
+ await fc.assert(
+ fc.asyncProperty(
+ fc.record({
+ name: fc.string(),
+ email: fc.string().filter((s) => !/^[^@]+@[^@]+\.[^@]+$/.test(s)), // force invalid
+ subject: fc.string(),
+ message: fc.string(),
+ }),
+ async (input) => {
+ try {
+ await sendContactMail(input);
+ expect.fail("Expected HttpError");
+ } catch (err) {
+ expect(err).to.be.instanceOf(HttpError);
+ expect(err.message).to.equal("Invalid email");
+ expect(err.code).to.equal(400);
+ }
+ }
+ )
+ );
+ });
+});
diff --git a/test/utils/sendContactMail/sendContactMail.unit.test.js b/test/utils/sendContactMail/sendContactMail.unit.test.js
new file mode 100644
index 0000000..b73528b
--- /dev/null
+++ b/test/utils/sendContactMail/sendContactMail.unit.test.js
@@ -0,0 +1,203 @@
+const chai = require("chai");
+const sinon = require("sinon");
+const fs = require("fs").promises;
+const path = require("path");
+
+const chaiAsPromised =
+ require("chai-as-promised").default || require("chai-as-promised");
+
+const HttpError = require("../../../src/utils/HttpError");
+
+chai.use(chaiAsPromised);
+const { expect } = chai;
+
+describe("sendContactMail", () => {
+ let validateAndSanitizeEmailStub;
+ let transporterStub;
+ let loggerStub;
+ let sendContactMail;
+ let fsReadStub;
+ let fsWriteStub;
+
+ const validInput = {
+ name: "Jane Doe",
+ email: "jane@example.com",
+ subject: "Hello",
+ message: "This is a test.",
+ };
+
+ const mockEmailResponse = {
+ accepted: ["admin@example.com"],
+ rejected: [],
+ response: "250 Message accepted",
+ envelope: {
+ from: "no-reply@example.com",
+ to: ["admin@example.com"],
+ },
+ messageId: "",
+ };
+
+ const mockEmailConfig = {
+ MAIL_DOMAIN: "example.com",
+ MAIL_USER: "admin@example.com",
+ DEFAULT_SUBJECT: "New Contact Form Submission",
+ EMAIL_LOG_PATH: path.join(__dirname, "../../../data/emails.json"),
+ };
+
+ beforeEach(() => {
+ // Clear module cache
+ delete require.cache[require.resolve("../../../src/utils/sendContactMail")];
+ delete require.cache[require.resolve("../../../src/utils/emailValidator")];
+ delete require.cache[require.resolve("../../../src/utils/transporter")];
+ delete require.cache[require.resolve("../../../src/utils/logging")];
+ delete require.cache[require.resolve("../../../src/config/emailConfig")];
+
+ // Create stubs
+ validateAndSanitizeEmailStub = sinon.stub().returns({
+ valid: true,
+ email: "jane@example.com",
+ });
+
+ transporterStub = {
+ sendMail: sinon.stub().resolves(mockEmailResponse),
+ };
+
+ loggerStub = {
+ error: sinon.stub(),
+ };
+
+ require.cache[require.resolve("../../../src/config/emailConfig")] = {
+ exports: mockEmailConfig,
+ };
+
+ // Mock modules in require cache
+ require.cache[require.resolve("../../../src/utils/emailValidator")] = {
+ exports: { validateAndSanitizeEmail: validateAndSanitizeEmailStub },
+ };
+
+ require.cache[require.resolve("../../../src/utils/transporter")] = {
+ exports: transporterStub,
+ };
+
+ require.cache[require.resolve("../../../src/utils/logging")] = {
+ exports: { winstonLogger: loggerStub },
+ };
+
+ // Set environment variables
+ process.env.MAIL_DOMAIN = "example.com";
+ process.env.MAIL_USER = "admin@example.com";
+
+ // Create fs stubs
+ fsReadStub = sinon.stub(fs, "readFile");
+ fsWriteStub = sinon.stub(fs, "writeFile");
+
+ // Require the module after mocking
+ const module = require("../../../src/utils/sendContactMail");
+ sendContactMail = module.sendContactMail;
+ });
+
+ afterEach(() => {
+ sinon.restore();
+ });
+
+ it("sends an email with valid input", async () => {
+ fsReadStub.resolves("[]");
+ fsWriteStub.resolves();
+
+ const result = await sendContactMail(validInput);
+
+ // Check the result matches what transporter.sendMail returns
+ expect(result).to.deep.equal(mockEmailResponse);
+
+ // Verify transporter.sendMail was called once
+ expect(transporterStub.sendMail.calledOnce).to.be.true;
+
+ // Check the email parameters
+ const sendArgs = transporterStub.sendMail.getCall(0).args[0];
+ expect(sendArgs.from).to.equal('"Contact Form" ');
+ expect(sendArgs.to).to.equal("admin@example.com");
+ expect(sendArgs.replyTo).to.equal('"Jane Doe" ');
+ expect(sendArgs.subject).to.equal("Hello");
+ expect(sendArgs.text).to.equal("This is a test.");
+
+ // Verify file operations
+ expect(fsReadStub.calledOnce).to.be.true;
+ expect(fsWriteStub.calledOnce).to.be.true;
+ });
+
+ it("uses default subject if none provided", async () => {
+ const input = { ...validInput, subject: undefined };
+ fsReadStub.resolves("[]");
+ fsWriteStub.resolves();
+
+ const result = await sendContactMail(input);
+
+ // Check that we get the mock response, not an empty object
+ expect(result).to.deep.equal(mockEmailResponse);
+
+ const args = transporterStub.sendMail.firstCall.args[0];
+ expect(args.subject).to.equal("New Contact Form Submission");
+ });
+
+ it("throws HttpError on invalid email", async () => {
+ validateAndSanitizeEmailStub.returns({
+ valid: false,
+ message: "Invalid email format",
+ });
+
+ await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
+ });
+
+ it("logs and rethrows on readFile error", async () => {
+ const error = new Error("Disk failure");
+ fsReadStub.rejects(error);
+
+ await expect(sendContactMail(validInput)).to.be.rejectedWith(
+ "Disk failure"
+ );
+
+ // Verify logger was called
+ expect(loggerStub.error.calledOnce).to.be.true;
+ expect(loggerStub.error.calledWith("Failed to log email to file:", error))
+ .to.be.true;
+ });
+
+ it("throws on invalid email without message", async () => {
+ validateAndSanitizeEmailStub.returns({
+ valid: false,
+ message: undefined,
+ });
+
+ await expect(sendContactMail(validInput)).to.be.rejectedWith(HttpError);
+ });
+
+ it("handles non-empty existing log file", async () => {
+ const existingLogs = JSON.stringify([{ test: true }]);
+ fsReadStub.resolves(existingLogs);
+ fsWriteStub.resolves();
+
+ const result = await sendContactMail(validInput);
+ expect(result).to.deep.equal(mockEmailResponse);
+
+ // Verify the log was written with existing data plus new entry
+ expect(fsWriteStub.calledOnce).to.be.true;
+ const writtenData = JSON.parse(fsWriteStub.firstCall.args[1]);
+ expect(writtenData).to.be.an("array").with.lengthOf(2);
+ expect(writtenData[0]).to.deep.equal({ test: true });
+ });
+
+ it("throws if writing log file fails", async () => {
+ fsReadStub.resolves("[]");
+ const error = new Error("Write failed");
+ fsWriteStub.rejects(error);
+
+ await expect(sendContactMail(validInput)).to.be.rejectedWith(
+ "Write failed"
+ );
+
+ // Verify logger was called
+ expect(loggerStub.error.calledOnce).to.be.true;
+ expect(loggerStub.error.calledWith("Failed to log email to file:", error))
+ .to.be.true;
+ });
+});
diff --git a/test/utils/sendNewsletterSubscriptionMail.unit.test.js b/test/utils/sendNewsletterSubscriptionMail.unit.test.js
new file mode 100644
index 0000000..f6b75ea
--- /dev/null
+++ b/test/utils/sendNewsletterSubscriptionMail.unit.test.js
@@ -0,0 +1,51 @@
+// test/units/utils/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);
+ });
+});