diff --git a/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.property.test.js
new file mode 100644
index 0000000..e07bac2
--- /dev/null
+++ b/test/units/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/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js b/test/units/utils/emailValidator/validaterAndSanitizeEmail.unit.test.js
new file mode 100644
index 0000000..5c052a3
--- /dev/null
+++ b/test/units/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/units/utils/logging/config.unit.test.js b/test/units/utils/logging/config.unit.test.js
new file mode 100644
index 0000000..c8b2b0c
--- /dev/null
+++ b/test/units/utils/logging/config.unit.test.js
@@ -0,0 +1,79 @@
+// test/units/utils/logging/config.test.js
+const { expect } = require("chai");
+const fs = require("fs");
+const path = require("path");
+const proxyquire = require("proxyquire").noPreserveCache();
+
+const {
+ projectRoot,
+ logDir,
+ sessionTimestamp,
+ sessionDir,
+ logFiles,
+ LOG_LEVELS,
+} = require("../../../../src/utils/logging/config");
+
+describe("config.js", () => {
+ it("projectRoot contains package.json", () => {
+ const pkgJsonPath = path.join(projectRoot, "package.json");
+ const exists = fs.existsSync(pkgJsonPath);
+ expect(exists).to.equal(true, `package.json not found in ${projectRoot}`);
+ });
+
+ it("projectRoot matches resolved 3-levels-up path", () => {
+ const expected = path.resolve(__dirname, "../../../../");
+ expect(projectRoot).to.equal(expected);
+ });
+
+ it("logDir is within projectRoot and ends with 'logs'", () => {
+ expect(logDir.startsWith(projectRoot)).to.be.true;
+ expect(path.basename(logDir)).to.equal("logs");
+ });
+
+ it("sessionTimestamp matches expected ISO pattern with no colons or dots", () => {
+ expect(sessionTimestamp).to.match(
+ /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/
+ );
+ });
+
+ it("sessionDir is built from logDir and sessionTimestamp", () => {
+ const expected = path.join(logDir, "sessions", sessionTimestamp);
+ expect(sessionDir).to.equal(expected);
+ });
+
+ it("logFiles.session points to session.log in sessionDir", () => {
+ expect(logFiles.session).to.equal(path.join(sessionDir, "session.log"));
+ });
+
+ ["info", "notice", "error", "warn", "debug"].forEach((level) => {
+ it(`logFiles.${level} points to ${level}.log in correct subdir`, () => {
+ expect(logFiles[level]).to.equal(
+ path.join(logDir, level, `${level}.log`)
+ );
+ });
+ });
+
+ it("LOG_LEVELS defines correct level-to-priority mapping", () => {
+ expect(LOG_LEVELS).to.deep.equal({
+ error: 0,
+ warn: 1,
+ security: 2,
+ notice: 3,
+ info: 4,
+ debug: 5,
+ });
+ });
+
+ it("LOG_LEVEL defaults to 'info' when process.env.LOG_LEVEL is unset", () => {
+ const original = process.env.LOG_LEVEL;
+ delete process.env.LOG_LEVEL;
+
+ const { LOG_LEVEL } = proxyquire(
+ "../../../../src/utils/logging/config",
+ {}
+ );
+ expect(LOG_LEVEL).to.equal("info");
+
+ if (original !== undefined) process.env.LOG_LEVEL = original;
+ });
+});
diff --git a/test/units/utils/logging/createLogStreams.unit.test.js b/test/units/utils/logging/createLogStreams.unit.test.js
new file mode 100644
index 0000000..bb5482c
--- /dev/null
+++ b/test/units/utils/logging/createLogStreams.unit.test.js
@@ -0,0 +1,31 @@
+// 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
new file mode 100644
index 0000000..def137f
--- /dev/null
+++ b/test/units/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/units/utils/logging/formatLog.unit.test.js b/test/units/utils/logging/formatLog.unit.test.js
new file mode 100644
index 0000000..c073a81
--- /dev/null
+++ b/test/units/utils/logging/formatLog.unit.test.js
@@ -0,0 +1,26 @@
+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
new file mode 100644
index 0000000..0dc0aeb
--- /dev/null
+++ b/test/units/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/units/utils/logging/handleUncaughtException.unit.test.js b/test/units/utils/logging/handleUncaughtException.unit.test.js
new file mode 100644
index 0000000..6e4e3c5
--- /dev/null
+++ b/test/units/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/units/utils/logging/handleUnhandledRejection.unit.test.js b/test/units/utils/logging/handleUnhandledRejection.unit.test.js
new file mode 100644
index 0000000..74e720b
--- /dev/null
+++ b/test/units/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/units/utils/logging/initializeLogDirectories.unit.test.js b/test/units/utils/logging/initializeLogDirectories.unit.test.js
new file mode 100644
index 0000000..15acc5a
--- /dev/null
+++ b/test/units/utils/logging/initializeLogDirectories.unit.test.js
@@ -0,0 +1,44 @@
+// 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
new file mode 100644
index 0000000..0649a60
--- /dev/null
+++ b/test/units/utils/logging/object-formatting.unit.test.js
@@ -0,0 +1,521 @@
+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
new file mode 100644
index 0000000..85070bb
--- /dev/null
+++ b/test/units/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, 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
new file mode 100644
index 0000000..3620788
--- /dev/null
+++ b/test/units/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/units/utils/sendContactMail/sendContactMail.property.test.js b/test/units/utils/sendContactMail/sendContactMail.property.test.js
new file mode 100644
index 0000000..e164235
--- /dev/null
+++ b/test/units/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/units/utils/sendContactMail/sendContactMail.unit.test.js b/test/units/utils/sendContactMail/sendContactMail.unit.test.js
new file mode 100644
index 0000000..f8e3a43
--- /dev/null
+++ b/test/units/utils/sendContactMail/sendContactMail.unit.test.js
@@ -0,0 +1,207 @@
+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
new file mode 100644
index 0000000..9a30e5d
--- /dev/null
+++ b/test/units/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);
+ });
+});