diff --git a/src/app.js b/src/app.js index 7992412..f26debd 100644 --- a/src/app.js +++ b/src/app.js @@ -4,10 +4,10 @@ const net = require("net"); const setupMiddleware = require("./middleware"); const { - winstonLogger, handleUncaughtException, handleUnhandledRejection, -} = require("./utils/logging"); +} = require("./utils/logging/handlers"); +const { winstonLogger } = require("./utils/logging"); const { startTokenCleanup } = require("./utils/tokenCleanup"); const { cleanupOldSessions } = require("./utils/logManager"); @@ -20,6 +20,8 @@ cleanupOldSessions(); startTokenCleanup(); +// const winstonLogger = createWinstonLogger(); + const app = setupMiddleware(); const server = net.createServer(); diff --git a/src/utils/logging/consolePatch.js b/src/utils/logging/consolePatch.js index 3d26005..0a53ecd 100644 --- a/src/utils/logging/consolePatch.js +++ b/src/utils/logging/consolePatch.js @@ -1,3 +1,4 @@ +// src/utils/logging/consolePatch.js const { LOG_LEVEL, LOG_LEVELS } = require("./config"); function shouldLog(level) { @@ -19,16 +20,28 @@ writeLog("DEBUG", logStreams.debug, originalConsole.debug, ...args); } -function writeLog(level, stream, consoleFn, ...args) { +function writeLog(level, stream, consoleFn, sessionTransport, ...args) { if (!shouldLog(level)) return; const timestamp = new Date().toISOString(); - const message = args.join(" "); + + const safeArgs = args.map((arg) => { + if (typeof arg === "object") { + try { + return JSON.stringify(arg, getCircularReplacer(), 2); + } catch { + return require("util").inspect(arg, { depth: null, colors: false }); + } + } + return String(arg); + }); + + const message = safeArgs.join(" "); const logLine = `[${timestamp}] [${level}] ${message}\n`; stream.write(logLine); sessionTransport.write({ level: level.toLowerCase(), message, timestamp }); - consoleFn(`[${timestamp}] [${level}]`, ...args); + consoleFn(`[${timestamp}] [${level}]`, ...safeArgs); } module.exports = { diff --git a/src/utils/logging/handlers.js b/src/utils/logging/handlers.js index e0b3524..39d4c0c 100644 --- a/src/utils/logging/handlers.js +++ b/src/utils/logging/handlers.js @@ -4,11 +4,23 @@ const { winstonLogger } = require("./index"); function handleUncaughtException(err) { - winstonLogger.error(UNCUGHT_EXCEPTION_MSG, err.stack || err); + const msg = err.stack || err; + try { + console.error(UNCUGHT_EXCEPTION_MSG, msg); + winstonLogger.error(UNCUGHT_EXCEPTION_MSG, msg); + } catch (e) { + console.error(UNCUGHT_EXCEPTION_MSG, msg); + } } function handleUnhandledRejection(reason) { - winstonLogger.error(UNHANDLED_REJECTION_MSG, reason?.stack || reason); + const msg = reason?.stack || reason; + try { + console.error(UNHANDLED_REJECTION_MSG, msg); + winstonLogger.error(UNHANDLED_REJECTION_MSG, msg); + } catch (e) { + console.error(UNHANDLED_REJECTION_MSG, msg); + } } module.exports = { diff --git a/src/utils/logging/index.js b/src/utils/logging/index.js index 6d4d1c8..18bd09d 100644 --- a/src/utils/logging/index.js +++ b/src/utils/logging/index.js @@ -49,14 +49,34 @@ const manualLogger = { streams: logStreams, function: (...args) => functionLog(functionsLogDir, ...args), - info: (...args) => writeLog("INFO", logStreams.info, console.log, ...args), + info: (...args) => + writeLog("INFO", logStreams.info, console.log, sessionTransport, ...args), notice: (...args) => - writeLog("NOTICE", logStreams.notice, console.log, ...args), - warn: (...args) => writeLog("WARN", logStreams.warn, console.warn, ...args), + writeLog( + "NOTICE", + logStreams.notice, + console.log, + sessionTransport, + ...args + ), + warn: (...args) => + writeLog("WARN", logStreams.warn, console.warn, sessionTransport, ...args), error: (...args) => - writeLog("ERROR", logStreams.error, console.error, ...args), + writeLog( + "ERROR", + logStreams.error, + console.error, + sessionTransport, + ...args + ), debug: (...args) => - writeLog("DEBUG", logStreams.debug, console.debug, ...args), + writeLog( + "DEBUG", + logStreams.debug, + console.debug, + sessionTransport, + ...args + ), sessionInfo: () => ({ sessionId: sessionTimestamp, sessionDir, @@ -89,21 +109,14 @@ let stack = meta.stack || ""; if (stack) delete meta.stack; - let outputMsg; - if (typeof message === "string") { - outputMsg = message; - } else { - try { - outputMsg = JSON.stringify(message, null, 2); - } catch { - outputMsg = util.inspect(message, { depth: null, colors: false }); - } - } + const safeInspect = (input) => + typeof input === "string" + ? input + : util.inspect(input, { depth: null, colors: false }); - let metaString = ""; - if (Object.keys(meta).length > 0) { - metaString = util.inspect(meta, { depth: null, colors: false }); - } + const outputMsg = safeInspect(message); + const metaString = + Object.keys(meta).length > 0 ? safeInspect(meta) : ""; return `[${timestamp}] [${level}] ${outputMsg}\n${stack}\n${metaString}`; }) @@ -118,7 +131,7 @@ winstonLogger, initializeLogDirectories, createLogStreams, - createSessionTransport, + sessionTransport, patchConsole, shouldLog, writeLog, diff --git a/test/units/utils/logging/createLogStreams.test.js b/test/units/utils/logging/createLogStreams.test.js index ff2647f..bb5482c 100644 --- a/test/units/utils/logging/createLogStreams.test.js +++ b/test/units/utils/logging/createLogStreams.test.js @@ -1,4 +1,4 @@ -// test/createLogStreams.test.js +// test/units/utils/logging/createLogStreams.test.js const fs = require("fs"); const path = require("path"); const { expect } = require("chai"); diff --git a/test/units/utils/logging/formatFunctionName.test.js b/test/units/utils/logging/formatFunctionName.test.js index 9c847ad..def137f 100644 --- a/test/units/utils/logging/formatFunctionName.test.js +++ b/test/units/utils/logging/formatFunctionName.test.js @@ -1,4 +1,4 @@ -// test/formatFunctionName.test.js +// test/units/utils/logging/formatFunctionName.test.js const { expect } = require("chai"); const path = require("path"); const { formatFunctionName } = require("../../../../src/utils/logging"); diff --git a/test/units/utils/logging/formatLogMessage.test.js b/test/units/utils/logging/formatLogMessage.test.js index 4b30976..0dc0aeb 100644 --- a/test/units/utils/logging/formatLogMessage.test.js +++ b/test/units/utils/logging/formatLogMessage.test.js @@ -1,4 +1,4 @@ -// test/formatLogMessage.test.js +// test/units/utils/logging/formatLogMessage.test.js const { expect } = require("chai"); const { formatLogMessage } = require("../../../../src/utils/logging"); diff --git a/test/units/utils/logging/handleUncaughtException.test.js b/test/units/utils/logging/handleUncaughtException.test.js index 458f05b..6e4e3c5 100644 --- a/test/units/utils/logging/handleUncaughtException.test.js +++ b/test/units/utils/logging/handleUncaughtException.test.js @@ -1,4 +1,4 @@ -// test/handleUncaughtException.test.js +// test/units/utils/logging/handleUncaughtException.test.js const { expect } = require("chai"); const sinon = require("sinon"); const proxyquire = require("proxyquire").noCallThru(); diff --git a/test/units/utils/logging/initializeLogDirectories.test.js b/test/units/utils/logging/initializeLogDirectories.test.js index 89cd398..15acc5a 100644 --- a/test/units/utils/logging/initializeLogDirectories.test.js +++ b/test/units/utils/logging/initializeLogDirectories.test.js @@ -1,4 +1,4 @@ -// test/initializeLogDirectories.test.js +// test/units/utils/logging/initializeLogDirectories.test.js const { expect } = require("chai"); const fs = require("fs"); const path = require("path"); diff --git a/test/units/utils/logging/object-formatting.test.js b/test/units/utils/logging/object-formatting.test.js new file mode 100644 index 0000000..0e1167c --- /dev/null +++ b/test/units/utils/logging/object-formatting.test.js @@ -0,0 +1,501 @@ +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 consoleStub; + let streamWriteStubs; + + beforeEach(() => { + // Stub console methods + consoleStub = sinon.stub(console, "log"); + + // 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(); + }); + + describe("writeLog function", () => { + 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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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 + 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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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; + + 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" + ) { + // Only stub if not already stubbed + if (!manualLogger.streams[level].write.isSinonProxy) { + manualLoggerStubs[level] = sinon.stub( + manualLogger.streams[level], + "write" + ); + } + } + }); + } + }); + + it("should not produce [object Object] in manualLogger.info", () => { + const testObj = { key: "value", nested: { deep: "property" } }; + + manualLogger.info(testObj); + + expect(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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("nested"); + expect(outputString).to.include("deep"); + }); + + it("should not produce [object Object] in manualLogger.error", () => { + const errorObj = { + error: "Something went wrong", + context: { userId: 123, action: "login" }, + }; + + 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(); + }); + + it("should handle circular objects in all manual logger methods", () => { + const circular = { name: "test" }; + circular.ref = circular; + + const methods = ["info", "warn", "error", "debug", "notice"]; + + methods.forEach((method) => { + // Create a fresh sandbox for each method to avoid conflicts + const sandbox = sinon.createSandbox(); + + const actualConsoleMethod = + method === "debug" + ? "debug" + : method === "warn" + ? "warn" + : method === "error" + ? "error" + : "log"; + + const methodStub = sandbox.stub(console, actualConsoleMethod); + + if (typeof manualLogger[method] === "function") { + manualLogger[method](circular); + + expect(methodStub.called).to.be.true; + const consoleArgs = methodStub.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"); + } + + sandbox.restore(); + }); + }); + }); + + describe("Winston Logger", () => { + let winstonInfoStub; + + beforeEach(() => { + winstonInfoStub = sinon.stub(winstonLogger, "info"); + }); + + 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; + const logString = JSON.stringify(logCall); + expect(logString).to.not.include("[object Object]"); + }); + }); + + describe("Edge Cases", () => { + it("should handle null and undefined without [object Object]", () => { + writeLog( + "INFO", + mockLogStreams.info, + console.log, + mockSessionTransport, + null, + undefined + ); + + expect(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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-01"); + + writeLog( + "INFO", + mockLogStreams.info, + console.log, + mockSessionTransport, + dateObj + ); + + expect(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.getCall(0).args; + expect(consoleArgs).to.exist; + const outputString = consoleArgs.join(" "); + expect(outputString).to.not.include("[object Object]"); + expect(outputString).to.include("2023"); + }); + + it("should handle RegExp objects", () => { + const regexObj = /test.*pattern/gi; + + writeLog( + "INFO", + mockLogStreams.info, + console.log, + mockSessionTransport, + regexObj + ); + + expect(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.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"); + }); + + 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(consoleStub.called).to.be.true; + const consoleArgs = consoleStub.getCall(0).args; + expect(consoleArgs).to.exist; + const outputString = consoleArgs.join(" "); + expect(outputString).to.not.include("[object Object]"); + expect(outputString).to.include("level"); + }); + }); + + describe("Stream Output Validation", () => { + 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(); + 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]; + expect(writeData).to.not.include("[object Object]"); + }); + }); + }); + }); +}); diff --git a/test/units/utils/logging/shouldLog.test.js b/test/units/utils/logging/shouldLog.test.js deleted file mode 100644 index 2449594..0000000 --- a/test/units/utils/logging/shouldLog.test.js +++ /dev/null @@ -1,25 +0,0 @@ -// test/shouldLog.test.js -const { expect } = require("chai"); - -describe("shouldLog", () => { - const originalLogLevel = process.env.LOG_LEVEL; - - beforeEach(() => { - process.env.LOG_LEVEL = "warn"; - }); - - afterEach(() => { - process.env.LOG_LEVEL = originalLogLevel; - delete require.cache[ - require.resolve("../../../../src/utils/logging/consolePatch") - ]; - }); - - it("returns true if level is higher or equal to current log level", () => { - const { shouldLog } = require("../../../../src/utils/logging/consolePatch"); - - expect(shouldLog("error")).to.be.true; - expect(shouldLog("warn")).to.be.true; - expect(shouldLog("debug")).to.be.false; - }); -}); diff --git a/test/units/utils/logging/writeLog.test.js b/test/units/utils/logging/writeLog.test.js index 6ba1a61..85070bb 100644 --- a/test/units/utils/logging/writeLog.test.js +++ b/test/units/utils/logging/writeLog.test.js @@ -1,9 +1,9 @@ -// test/writeLog.test.js +// test/units/utils/logging/writeLog.test.js const { expect } = require("chai"); const sinon = require("sinon"); const { writeLog } = require("../../../../src/utils/logging/consolePatch"); -describe("writeLog", () => { +describe("writeLog - Object Expansion Tests", () => { let stream; let consoleFn; let sessionTransport; @@ -13,71 +13,329 @@ beforeEach(() => { stream = { write: sinon.spy() }; consoleFn = sinon.spy(); - - global.sessionTransport = { write: sinon.spy() }; - sessionTransport = global.sessionTransport; - + sessionTransport = { write: sinon.spy() }; clock = sinon.useFakeTimers(fixedDate.getTime()); }); afterEach(() => { clock.restore(); - delete global.sessionTransport; + sinon.restore(); }); - it("does not write when shouldLog returns false", () => { - const originalLogLevel = process.env.LOG_LEVEL; - process.env.LOG_LEVEL = "error"; + describe("prevents [object Object] output", () => { + it("expands simple objects instead of showing [object Object]", () => { + const testObject = { name: "test", value: 42 }; - writeLog("DEBUG", stream, consoleFn, "test message"); + writeLog("INFO", stream, consoleFn, sessionTransport, testObject); - expect(stream.write.called).to.be.false; - expect(consoleFn.called).to.be.false; - expect(sessionTransport.write.called).to.be.false; + const expectedTimestamp = fixedDate.toISOString(); - process.env.LOG_LEVEL = originalLogLevel; + // 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"); + }); }); - it("writes log line to stream and calls consoleFn and sessionTransport.write", () => { - writeLog("INFO", stream, consoleFn, "test", "message"); + describe("edge cases", () => { + it("handles null and undefined without [object Object]", () => { + writeLog("INFO", stream, consoleFn, sessionTransport, null, undefined); - const expectedTimestamp = fixedDate.toISOString(); - const expectedLogLine = `[${expectedTimestamp}] [INFO] test message\n`; + 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(stream.write.calledWith(expectedLogLine)).to.be.true; - expect( - sessionTransport.write.calledWith({ - level: "info", - message: "test message", - timestamp: expectedTimestamp, - }) - ).to.be.true; - expect( - consoleFn.calledWith(`[${expectedTimestamp}] [INFO]`, "test", "message") - ).to.be.true; + 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"); + }); }); - it("joins multiple args correctly in message", () => { - writeLog("WARN", stream, consoleFn, "part1", "part2", "part3"); + describe("different log levels", () => { + const levels = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"]; - const expectedTimestamp = fixedDate.toISOString(); - const expectedLogLine = `[${expectedTimestamp}] [WARN] part1 part2 part3\n`; + levels.forEach((level) => { + it(`expands objects properly for ${level} level`, () => { + const testObj = { + level: level.toLowerCase(), + data: { nested: "value" }, + array: [{ item: "test" }], + }; - expect(stream.write.calledWith(expectedLogLine)).to.be.true; - expect( - sessionTransport.write.calledWith({ - level: "warn", - message: "part1 part2 part3", - timestamp: expectedTimestamp, - }) - ).to.be.true; - expect( - consoleFn.calledWith( - `[${expectedTimestamp}] [WARN]`, - "part1", - "part2", - "part3" - ) - ).to.be.true; + 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/sendNewsletterSubscriptionMail.test.js b/test/units/utils/sendNewsletterSubscriptionMail.test.js index b35252b..9a30e5d 100644 --- a/test/units/utils/sendNewsletterSubscriptionMail.test.js +++ b/test/units/utils/sendNewsletterSubscriptionMail.test.js @@ -1,4 +1,4 @@ -// test/sendNewsletterSubscriptionMail.test.js +// test/units/utils/sendNewsletterSubscriptionMail.test.js const sinon = require("sinon"); const transporter = require("../../../src/utils/transporter"); const { winstonLogger } = require("../../../src/utils/logging");