diff --git a/.githooks/pre-push b/.githooks/pre-push index 93153bc..92f8d50 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -9,6 +9,23 @@ set -euo pipefail set -x +# Run Mocha directly, fail if any test fails +if ! node_modules/.bin/mocha "test/**/*.unit.test.js" "test/**/*.property.test.js"; then + echo "Initial test suite failed. Skipping prepush and aborting push." + + if kill -0 "$APP_PID" 2>/dev/null; then + echo "Stopping app (PID: $APP_PID)..." + kill "$APP_PID" + sleep 1 + if kill -0 "$APP_PID" 2>/dev/null; then + kill -9 "$APP_PID" 2>/dev/null || true + fi + fi + + wait "$APP_PID" 2>/dev/null || true + exit 1 +fi + node src/app.js >/dev/null 2>&1 & APP_PID=$! diff --git a/scripts/test-prepush.js b/scripts/test-prepush.js index 8d1e15d..7ba07ff 100644 --- a/scripts/test-prepush.js +++ b/scripts/test-prepush.js @@ -1 +1,54 @@ -require("./runTests"); +const Mocha = require("mocha"); +const glob = require("glob").glob; +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +function runMochaWithFiles(files, description) { + console.log(`Running ${description}...`); + const mocha = new Mocha({ + reporter: "spec", + timeout: 5000, + }); + + files.forEach((file) => mocha.addFile(file)); + + return new Promise((resolve, reject) => { + mocha.run((failures) => { + if (failures) { + reject(new Error(`${description} failed with ${failures} failures`)); + } else { + resolve(); + } + }); + }); +} + +async function findTestFiles(pattern) { + return new Promise((resolve, reject) => { + glob(pattern, (err, files) => { + if (err) reject(err); + else resolve(files.map((f) => path.resolve(f))); + }); + }); +} + +async function runTests() { + try { + const unitTestFiles = await findTestFiles("test/**/*.unit.test.js"); + await runMochaWithFiles(unitTestFiles, "unit tests"); + + const propertyTestFiles = await findTestFiles("test/**/*.property.test.js"); + await runMochaWithFiles(propertyTestFiles, "property-based tests"); + + const commitHash = execSync("git rev-parse HEAD").toString().trim(); + fs.writeFileSync(".last_tested_commit", commitHash + "\n"); + + require("./runTests"); + } catch (err) { + console.error("Test execution failed:", err.message); + process.exit(1); + } +} + +runTests(); diff --git a/test/units/utils/logging/config.test.js b/test/units/utils/logging/config.test.js deleted file mode 100644 index c8b2b0c..0000000 --- a/test/units/utils/logging/config.test.js +++ /dev/null @@ -1,79 +0,0 @@ -// test/units/utils/logging/config.test.js -const { expect } = require("chai"); -const fs = require("fs"); -const path = require("path"); -const proxyquire = require("proxyquire").noPreserveCache(); - -const { - projectRoot, - logDir, - sessionTimestamp, - sessionDir, - logFiles, - LOG_LEVELS, -} = require("../../../../src/utils/logging/config"); - -describe("config.js", () => { - it("projectRoot contains package.json", () => { - const pkgJsonPath = path.join(projectRoot, "package.json"); - const exists = fs.existsSync(pkgJsonPath); - expect(exists).to.equal(true, `package.json not found in ${projectRoot}`); - }); - - it("projectRoot matches resolved 3-levels-up path", () => { - const expected = path.resolve(__dirname, "../../../../"); - expect(projectRoot).to.equal(expected); - }); - - it("logDir is within projectRoot and ends with 'logs'", () => { - expect(logDir.startsWith(projectRoot)).to.be.true; - expect(path.basename(logDir)).to.equal("logs"); - }); - - it("sessionTimestamp matches expected ISO pattern with no colons or dots", () => { - expect(sessionTimestamp).to.match( - /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/ - ); - }); - - it("sessionDir is built from logDir and sessionTimestamp", () => { - const expected = path.join(logDir, "sessions", sessionTimestamp); - expect(sessionDir).to.equal(expected); - }); - - it("logFiles.session points to session.log in sessionDir", () => { - expect(logFiles.session).to.equal(path.join(sessionDir, "session.log")); - }); - - ["info", "notice", "error", "warn", "debug"].forEach((level) => { - it(`logFiles.${level} points to ${level}.log in correct subdir`, () => { - expect(logFiles[level]).to.equal( - path.join(logDir, level, `${level}.log`) - ); - }); - }); - - it("LOG_LEVELS defines correct level-to-priority mapping", () => { - expect(LOG_LEVELS).to.deep.equal({ - error: 0, - warn: 1, - security: 2, - notice: 3, - info: 4, - debug: 5, - }); - }); - - it("LOG_LEVEL defaults to 'info' when process.env.LOG_LEVEL is unset", () => { - const original = process.env.LOG_LEVEL; - delete process.env.LOG_LEVEL; - - const { LOG_LEVEL } = proxyquire( - "../../../../src/utils/logging/config", - {} - ); - expect(LOG_LEVEL).to.equal("info"); - - if (original !== undefined) process.env.LOG_LEVEL = original; - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/createLogStreams.test.js deleted file mode 100644 index bb5482c..0000000 --- a/test/units/utils/logging/createLogStreams.test.js +++ /dev/null @@ -1,31 +0,0 @@ -// test/units/utils/logging/createLogStreams.test.js -const fs = require("fs"); -const path = require("path"); -const { expect } = require("chai"); -const { createLogStreams } = require("../../../../src/utils/logging"); - -describe("createLogStreams", () => { - const testDir = path.join(__dirname, "..", "..", "..", "..", "test", "logs"); - const files = { - info: path.join(testDir, "info.log"), - error: path.join(testDir, "error.log"), - warn: path.join(testDir, "warn.log"), - notice: path.join(testDir, "notice.log"), - debug: path.join(testDir, "debug.log"), - }; - - afterEach(() => { - Object.values(files).forEach((file) => { - try { - fs.unlinkSync(file); - } catch (_) {} - }); - }); - - it("should create write streams for all log files", () => { - const streams = createLogStreams(files); - for (const key of Object.keys(files)) { - expect(streams[key]).to.be.an.instanceof(fs.WriteStream); - } - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/formatFunctionName.test.js deleted file mode 100644 index def137f..0000000 --- a/test/units/utils/logging/formatFunctionName.test.js +++ /dev/null @@ -1,14 +0,0 @@ -// test/units/utils/logging/formatFunctionName.test.js -const { expect } = require("chai"); -const path = require("path"); -const { formatFunctionName } = require("../../../../src/utils/logging"); - -describe("formatFunctionName", () => { - it("returns relative path with forward slashes", () => { - const base = path.join(__dirname, "..", "..", "..", ".."); - const testPath = path.join(base, "src", "somefile.js"); - const result = formatFunctionName(testPath, base); - - expect(result).to.equal("src/somefile.js"); - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/formatLog.test.js deleted file mode 100644 index c073a81..0000000 --- a/test/units/utils/logging/formatLog.test.js +++ /dev/null @@ -1,26 +0,0 @@ -const { formatLog } = require("../../../../src/utils/logging/consolePatch"); -const { expect } = require("chai"); - -describe("Logger Format Function Tests", () => { - it("should format circular objects without throwing and stringify correctly", () => { - const circular = { name: "test" }; - circular.ref = circular; - - const methods = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"]; - - methods.forEach((level) => { - const { timestamp, safeArgs, message, logLine } = formatLog( - level, - circular - ); - - expect(timestamp).to.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/); - expect(safeArgs).to.be.an("array"); - expect(message).to.include("name"); - expect(message).to.include("test"); - expect(message).to.not.include("[object Object]"); - expect(logLine).to.include(`[${timestamp}] [${level}]`); - expect(logLine).to.include(message); - }); - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/formatLogMessage.test.js deleted file mode 100644 index 0dc0aeb..0000000 --- a/test/units/utils/logging/formatLogMessage.test.js +++ /dev/null @@ -1,15 +0,0 @@ -// test/units/utils/logging/formatLogMessage.test.js -const { expect } = require("chai"); -const { formatLogMessage } = require("../../../../src/utils/logging"); - -describe("formatLogMessage", () => { - it("formats message with timestamp and args", () => { - const fn = "testFunc.js"; - const args = ["arg1", "arg2"]; - const result = formatLogMessage(fn, args); - - expect(result).to.match(/\[\d{4}-\d{2}-\d{2}T/); // ISO date start - expect(result).to.include("arg1 arg2"); - expect(result).to.match(/\n$/); - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/handleUncaughtException.test.js deleted file mode 100644 index 6e4e3c5..0000000 --- a/test/units/utils/logging/handleUncaughtException.test.js +++ /dev/null @@ -1,28 +0,0 @@ -// test/units/utils/logging/handleUncaughtException.test.js -const { expect } = require("chai"); -const sinon = require("sinon"); -const proxyquire = require("proxyquire").noCallThru(); - -describe("handleUncaughtException", () => { - it("logs error using winstonLogger", () => { - const errorStub = sinon.stub(); - - const fakeLogger = { - winstonLogger: { - error: errorStub, - }, - }; - - const { handleUncaughtException } = proxyquire( - "../../../../src/utils/logging/handlers", - { - "./index": fakeLogger, - } - ); - - const err = new Error("fail"); - handleUncaughtException(err); - - expect(errorStub.calledWith("Uncaught Exception:", err.stack)).to.be.true; - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/handleUnhandledRejection.test.js deleted file mode 100644 index 74e720b..0000000 --- a/test/units/utils/logging/handleUnhandledRejection.test.js +++ /dev/null @@ -1,22 +0,0 @@ -const { expect } = require("chai"); -const sinon = require("sinon"); -const path = require("path"); -const proxyquire = require("proxyquire"); - -describe("handleUnhandledRejection", () => { - it("logs rejection using winstonLogger", () => { - const errorStub = sinon.stub(); - const reason = new Error("rejection"); - - const handlers = proxyquire( - path.resolve(__dirname, "../../../../src/utils/logging/handlers"), - { - "../logging": { winstonLogger: { error: errorStub } }, - } - ); - - handlers.handleUnhandledRejection(reason); - expect(errorStub.calledWith("Unhandled Rejection:", reason.stack)).to.be - .true; - }); -}); diff --git a/test/units/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.test.js b/test/units/utils/logging/initializeLogDirectories.test.js deleted file mode 100644 index 15acc5a..0000000 --- a/test/units/utils/logging/initializeLogDirectories.test.js +++ /dev/null @@ -1,44 +0,0 @@ -// test/units/utils/logging/initializeLogDirectories.test.js -const { expect } = require("chai"); -const fs = require("fs"); -const path = require("path"); -const mockFs = require("mock-fs"); -const { initializeLogDirectories } = require("../../../../src/utils/logging"); - -describe("initializeLogDirectories", () => { - const customLogFiles = { - info: "../test/logs/info/info.log", - error: "../test/logs/error/error.log", - warn: "../test/logs/warn/warn.log", - notice: "../test/logs/notice/notice.log", - debug: "../test/logs/debug/debug.log", - }; - - afterEach(() => mockFs.restore()); - - it("should create all required directories for given log files", () => { - mockFs({}); - const result = initializeLogDirectories(customLogFiles); - - for (const file of Object.values(customLogFiles)) { - const dir = path.dirname(file); - expect(fs.existsSync(dir)).to.be.true; - } - - expect(fs.existsSync(result)).to.be.true; - }); - - it("should not fail if directories already exist", () => { - const dirs = Object.values(customLogFiles).reduce( - (acc, file) => { - acc[path.dirname(file)] = {}; - return acc; - }, - { "../test/logs/functions": {} } - ); - - mockFs(dirs); - - expect(() => initializeLogDirectories(customLogFiles)).to.not.throw(); - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/object-formatting.test.js deleted file mode 100644 index 0649a60..0000000 --- a/test/units/utils/logging/object-formatting.test.js +++ /dev/null @@ -1,521 +0,0 @@ -const { expect } = require("chai"); -const sinon = require("sinon"); -const fs = require("fs"); -const path = require("path"); -const { Writable } = require("stream"); - -// Mock dependencies -const mockLogStreams = { - info: new Writable({ write() {} }), - error: new Writable({ write() {} }), - warn: new Writable({ write() {} }), - debug: new Writable({ write() {} }), - notice: new Writable({ write() {} }), -}; - -const mockSessionTransport = { - write: sinon.stub(), -}; - -// Import the modules under test -const { writeLog } = require("../../../../src/utils/logging/consolePatch"); -const { - manualLogger, - winstonLogger, -} = require("../../../../src/utils/logging/index"); - -describe("Logger Object Expansion Tests", () => { - let streamWriteStubs; - - beforeEach(() => { - // Create fresh stream write stubs for each test - streamWriteStubs = { - info: sinon.stub(mockLogStreams.info, "write"), - error: sinon.stub(mockLogStreams.error, "write"), - warn: sinon.stub(mockLogStreams.warn, "write"), - debug: sinon.stub(mockLogStreams.debug, "write"), - notice: sinon.stub(mockLogStreams.notice, "write"), - }; - - // Reset session transport - mockSessionTransport.write.reset(); - }); - - afterEach(() => { - // Restore all stubs - sinon.restore(); // This restores all stubs created by sinon.stub() - }); - - describe("writeLog function", () => { - let consoleLogStub; // Declare stub for this describe block - - beforeEach(() => { - // Stub console.log specifically for this describe block - consoleLogStub = sinon.stub(console, "log"); - }); - - afterEach(() => { - // Restore console.log stub after each test in this block - consoleLogStub.restore(); - }); - - it("should never log [object Object] for simple objects", () => { - const testObject = { name: "test", value: 42 }; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - testObject - ); - - // Check console output - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("name"); - expect(outputString).to.include("test"); - expect(outputString).to.include("value"); - expect(outputString).to.include("42"); - - // Check stream output - expect(streamWriteStubs.info.called).to.be.true; - const streamOutput = streamWriteStubs.info.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("name"); - expect(streamOutput).to.include("test"); - }); - - it("should properly expand nested objects", () => { - const nestedObject = { - user: { - id: 123, - profile: { - name: "John Doe", - settings: { theme: "dark", notifications: true }, - }, - }, - }; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - nestedObject - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("John Doe"); - expect(outputString).to.include("theme"); - expect(outputString).to.include("dark"); - expect(outputString).to.include("notifications"); - }); - - it("should handle circular references without [object Object]", () => { - const circularObj = { name: "circular" }; - circularObj.self = circularObj; - circularObj.nested = { parent: circularObj }; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - circularObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("name"); - expect(outputString).to.include("circular"); - // Should handle circular reference gracefully - expect(outputString).to.include("self"); - }); - - it("should expand arrays containing objects", () => { - const arrayWithObjects = [ - { id: 1, name: "first" }, - { id: 2, name: "second", nested: { value: "test" } }, - ]; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - arrayWithObjects - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("first"); - expect(outputString).to.include("second"); - expect(outputString).to.include("nested"); - expect(outputString).to.include("test"); - }); - - it("should handle mixed argument types without [object Object]", () => { - const mixedArgs = [ - "String message", - { obj: "value" }, - 42, - ["array", "items"], - { deeply: { nested: { object: "here" } } }, - ]; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - ...mixedArgs - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("String message"); - expect(outputString).to.include("obj"); - expect(outputString).to.include("value"); - expect(outputString).to.include("deeply"); - expect(outputString).to.include("here"); - }); - - it("should handle Error objects without [object Object]", () => { - const error = new Error("Test error"); - error.customProperty = { details: "additional info" }; - - // Stub console.error for this test (local stub, not interfering with console.log stub) - const consoleErrorStub = sinon.stub(console, "error"); - - writeLog( - "ERROR", - mockLogStreams.error, - console.error, - mockSessionTransport, - error - ); - - expect(consoleErrorStub.called).to.be.true; - const consoleArgs = consoleErrorStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("Test error"); - - consoleErrorStub.restore(); - }); - - it("should handle objects with special properties", () => { - const specialObj = { - toString: () => "custom toString", - valueOf: () => 99, - [Symbol.toStringTag]: "CustomObject", - normalProp: "normal value", - }; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - specialObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("normalProp"); - expect(outputString).to.include("normal value"); - }); - }); - - describe("Manual Logger Methods", () => { - let manualLoggerStubs; - // Removed writeLogStub as manualLogger directly interacts with console - - beforeEach(() => { - // Create fresh stubs for manual logger streams if they exist - manualLoggerStubs = {}; - if (manualLogger.streams) { - Object.keys(manualLogger.streams).forEach((level) => { - if ( - manualLogger.streams[level] && - typeof manualLogger.streams[level].write === "function" - ) { - if (!manualLogger.streams[level].write.isSinonProxy) { - manualLoggerStubs[level] = sinon.stub( - manualLogger.streams[level], - "write" - ); - } - } - }); - } - }); - - afterEach(() => { - // Restore all stubs created within this describe block or its tests - sinon.restore(); - }); - - it("should not produce [object Object] in manualLogger.info", () => { - const testObj = { key: "value", nested: { deep: "property" } }; - - // Stub console.log locally for this specific test - const consoleLogStub = sinon.stub(console, "log"); - manualLogger.info(testObj); - - expect(consoleLogStub.called).to.be.true; // Check if console.log was called - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); // Join them to check content - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("key"); - expect(outputString).to.include("nested"); - expect(outputString).to.include("deep"); - consoleLogStub.restore(); // Restore after test - }); - - it("should not produce [object Object] in manualLogger.error", () => { - const errorObj = { - error: "Something went wrong", - context: { userId: 123, action: "login" }, - }; - - // Stub console.error for this test - const consoleErrorStub = sinon.stub(console, "error"); - - manualLogger.error(errorObj); - - expect(consoleErrorStub.called).to.be.true; - const consoleArgs = consoleErrorStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("Something went wrong"); - expect(outputString).to.include("userId"); - expect(outputString).to.include("login"); - - consoleErrorStub.restore(); - }); - }); - - describe("Winston Logger", () => { - let winstonInfoStub; - - beforeEach(() => { - winstonInfoStub = sinon.stub(winstonLogger, "info"); - }); - - afterEach(() => { - winstonInfoStub.restore(); // Ensure winston stub is restored - }); - - it("should not produce [object Object] in winston logs", () => { - const logData = { - user: { id: 456, name: "Jane" }, - action: "update", - metadata: { timestamp: Date.now() }, - }; - - winstonLogger.info("User action", logData); - - // Check that winston was called with properly formatted data - expect(winstonInfoStub.called).to.be.true; - const logCall = winstonInfoStub.getCall(0).args; - // Winston typically stringifies objects, so we check the stringified output - const logString = JSON.stringify(logCall); - expect(logString).to.not.include("[object Object]"); - expect(logString).to.include("Jane"); - expect(logString).to.include("update"); - }); - }); - - describe("Edge Cases", () => { - let consoleLogStub; // Declare stub for this describe block - - beforeEach(() => { - // Stub console.log specifically for this describe block - consoleLogStub = sinon.stub(console, "log"); - }); - - afterEach(() => { - // Restore console.log stub after each test in this block - consoleLogStub.restore(); - }); - - it("should handle null and undefined without [object Object]", () => { - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - null, - undefined - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("null"); - expect(outputString).to.include("undefined"); - }); - - it("should handle objects with null prototype", () => { - const nullProtoObj = Object.create(null); - nullProtoObj.key = "value"; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - nullProtoObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("key"); - expect(outputString).to.include("value"); - }); - - it("should handle Date objects", () => { - const dateObj = new Date("2023-01-01T12:00:00.000Z"); // Use ISO string for consistent output - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - dateObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - // Check for parts of the date string that are likely to be present in ISO format - // Console.log's output for Date objects can vary, but the ISO string is often included or derived. - expect(outputString).to.include("2023"); - // Check for the time part of the ISO string for more robustness - expect(outputString).to.include("T12:00:00.000Z"); - }); - - it("should handle RegExp objects", () => { - const regexObj = /test.*pattern/gi; - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - regexObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("test"); - expect(outputString).to.include("pattern"); - expect(outputString).to.include("gi"); // Check for flags - }); - - it("should handle very deeply nested objects", () => { - let deepObj = { level: 0 }; - let current = deepObj; - - // Create 10 levels deep - for (let i = 1; i <= 10; i++) { - current.next = { level: i }; - current = current.next; - } - - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - deepObj - ); - - expect(consoleLogStub.called).to.be.true; - const consoleArgs = consoleLogStub.getCall(0).args; - expect(consoleArgs).to.exist; - const outputString = consoleArgs.join(" "); - expect(outputString).to.not.include("[object Object]"); - expect(outputString).to.include("level"); - // Check for presence of multiple levels - expect(outputString.match(/level/g).length).to.be.at.least(10); - }); - }); - - describe("Stream Output Validation", () => { - let consoleLogStub; // Declare stub for this describe block - - beforeEach(() => { - // Stub console.log specifically for this describe block - consoleLogStub = sinon.stub(console, "log"); - }); - - afterEach(() => { - // Restore console.log stub after each test in this block - consoleLogStub.restore(); - }); - - it("should ensure stream writes never contain [object Object]", () => { - const testObjects = [ - { simple: "object" }, - { nested: { deep: { value: "test" } } }, - [{ array: "item" }], - { mixed: ["array", { in: "object" }] }, - ]; - - testObjects.forEach((obj, index) => { - streamWriteStubs.info.resetHistory(); // Reset history for each iteration - writeLog( - "INFO", - mockLogStreams.info, - console.log, - mockSessionTransport, - obj - ); - - expect(streamWriteStubs.info.called).to.be.true; - const streamWrites = streamWriteStubs.info.getCalls(); - streamWrites.forEach((call) => { - const writeData = call.args[0]; - // Ensure the written data is a string and does not contain "[object Object]" - expect(typeof writeData).to.equal("string"); - expect(writeData).to.not.include("[object Object]"); - }); - }); - }); - }); -}); diff --git a/test/units/utils/logging/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.test.js b/test/units/utils/logging/writeLog.test.js deleted file mode 100644 index 85070bb..0000000 --- a/test/units/utils/logging/writeLog.test.js +++ /dev/null @@ -1,341 +0,0 @@ -// test/units/utils/logging/writeLog.test.js -const { expect } = require("chai"); -const sinon = require("sinon"); -const { writeLog } = require("../../../../src/utils/logging/consolePatch"); - -describe("writeLog - Object Expansion Tests", () => { - let stream; - let consoleFn; - let sessionTransport; - let clock; - const fixedDate = new Date("2025-07-25T12:00:00.000Z"); - - beforeEach(() => { - stream = { write: sinon.spy() }; - consoleFn = sinon.spy(); - sessionTransport = { write: sinon.spy() }; - clock = sinon.useFakeTimers(fixedDate.getTime()); - }); - - afterEach(() => { - clock.restore(); - sinon.restore(); - }); - - describe("prevents [object Object] output", () => { - it("expands simple objects instead of showing [object Object]", () => { - const testObject = { name: "test", value: 42 }; - - writeLog("INFO", stream, consoleFn, sessionTransport, testObject); - - const expectedTimestamp = fixedDate.toISOString(); - - // Check stream output doesn't contain [object Object] - expect(stream.write.called).to.be.true; - const streamCall = stream.write.getCall(0); - const streamOutput = streamCall.args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("name"); - expect(streamOutput).to.include("test"); - expect(streamOutput).to.include("value"); - expect(streamOutput).to.include("42"); - - // Check console output doesn't contain [object Object] - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleArgs = consoleCall.args; - expect(consoleArgs).to.exist; - expect(Array.isArray(consoleArgs)).to.be.true; - const consoleOutput = consoleArgs.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleArgs).to.include.members([`[${expectedTimestamp}] [INFO]`]); - - // Check sessionTransport message doesn't contain [object Object] - expect(sessionTransport.write.called).to.be.true; - const sessionCall = sessionTransport.write.getCall(0); - const sessionData = sessionCall.args[0]; - expect(sessionData.message).to.not.include("[object Object]"); - expect(sessionData.message).to.include("name"); - expect(sessionData.message).to.include("test"); - }); - - it("expands nested objects completely", () => { - const nestedObject = { - user: { - id: 123, - profile: { - name: "John Doe", - settings: { theme: "dark", notifications: true }, - }, - }, - }; - - writeLog("INFO", stream, consoleFn, sessionTransport, nestedObject); - - // Check all outputs expand the nested structure - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("John Doe"); - expect(streamOutput).to.include("theme"); - expect(streamOutput).to.include("dark"); - expect(streamOutput).to.include("notifications"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleOutput).to.include("John Doe"); - expect(consoleOutput).to.include("theme"); - - expect(sessionTransport.write.called).to.be.true; - const sessionMessage = sessionTransport.write.getCall(0).args[0].message; - expect(sessionMessage).to.not.include("[object Object]"); - expect(sessionMessage).to.include("John Doe"); - }); - - it("expands arrays containing objects", () => { - const arrayWithObjects = [ - { id: 1, name: "first" }, - { id: 2, name: "second", nested: { value: "test" } }, - ]; - - writeLog("INFO", stream, consoleFn, sessionTransport, arrayWithObjects); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("first"); - expect(streamOutput).to.include("second"); - expect(streamOutput).to.include("nested"); - expect(streamOutput).to.include("test"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleOutput).to.include("first"); - expect(consoleOutput).to.include("second"); - }); - - it("handles mixed argument types without [object Object]", () => { - const mixedArgs = [ - "String message", - { obj: "value" }, - 42, - ["array", "items"], - { deeply: { nested: { object: "here" } } }, - ]; - - writeLog("INFO", stream, consoleFn, sessionTransport, ...mixedArgs); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("String message"); - expect(streamOutput).to.include("obj"); - expect(streamOutput).to.include("value"); - expect(streamOutput).to.include("deeply"); - expect(streamOutput).to.include("here"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleOutput).to.include("String message"); - expect(consoleOutput).to.include("obj"); - }); - - it("expands Error objects properly", () => { - const error = new Error("Test error"); - error.customProperty = { details: "additional info" }; - - writeLog("ERROR", stream, consoleFn, sessionTransport, error); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("Test error"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleOutput).to.include("Test error"); - }); - - it("handles objects with special properties", () => { - const specialObj = { - toString: () => "custom toString", - valueOf: () => 99, - normalProp: "normal value", - anotherProp: { nested: "data" }, - }; - - writeLog("INFO", stream, consoleFn, sessionTransport, specialObj); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("normalProp"); - expect(streamOutput).to.include("normal value"); - expect(streamOutput).to.include("nested"); - expect(streamOutput).to.include("data"); - - expect(sessionTransport.write.called).to.be.true; - const sessionMessage = sessionTransport.write.getCall(0).args[0].message; - expect(sessionMessage).to.not.include("[object Object]"); - expect(sessionMessage).to.include("normalProp"); - }); - }); - - describe("edge cases", () => { - it("handles null and undefined without [object Object]", () => { - writeLog("INFO", stream, consoleFn, sessionTransport, null, undefined); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("null"); - expect(streamOutput).to.include("undefined"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - }); - - it("handles Date objects", () => { - const dateObj = new Date("2023-01-01"); - - writeLog("INFO", stream, consoleFn, sessionTransport, dateObj); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("2023"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - }); - - it("handles RegExp objects", () => { - const regexObj = /test.*pattern/gi; - - writeLog("INFO", stream, consoleFn, sessionTransport, regexObj); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("test"); - expect(streamOutput).to.include("pattern"); - }); - - it("handles objects with null prototype", () => { - const nullProtoObj = Object.create(null); - nullProtoObj.key = "value"; - nullProtoObj.nested = { prop: "data" }; - - writeLog("INFO", stream, consoleFn, sessionTransport, nullProtoObj); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("key"); - expect(streamOutput).to.include("value"); - expect(streamOutput).to.include("prop"); - expect(streamOutput).to.include("data"); - }); - - it("handles very deeply nested objects", () => { - let deepObj = { level: 0 }; - let current = deepObj; - - // Create 5 levels deep (reasonable for testing) - for (let i = 1; i <= 5; i++) { - current.next = { level: i, data: `level${i}data` }; - current = current.next; - } - - writeLog("INFO", stream, consoleFn, sessionTransport, deepObj); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("level"); - expect(streamOutput).to.include("level5data"); - }); - }); - - describe("different log levels", () => { - const levels = ["INFO", "WARN", "ERROR", "DEBUG", "NOTICE"]; - - levels.forEach((level) => { - it(`expands objects properly for ${level} level`, () => { - const testObj = { - level: level.toLowerCase(), - data: { nested: "value" }, - array: [{ item: "test" }], - }; - - writeLog(level, stream, consoleFn, sessionTransport, testObj); - - // Only check if the function was called for levels that should log - if (stream.write.called) { - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("nested"); - expect(streamOutput).to.include("value"); - expect(streamOutput).to.include("item"); - expect(streamOutput).to.include("test"); - } - - if (sessionTransport.write.called) { - const sessionMessage = - sessionTransport.write.getCall(0).args[0].message; - expect(sessionMessage).to.not.include("[object Object]"); - expect(sessionMessage).to.include("nested"); - } - }); - }); - }); - - describe("multiple objects in single call", () => { - it("expands all objects in arguments", () => { - const obj1 = { first: "object", nested: { value: 1 } }; - const obj2 = { second: "object", array: [{ item: "test" }] }; - const obj3 = { third: { deeply: { nested: "value" } } }; - - writeLog("INFO", stream, consoleFn, sessionTransport, obj1, obj2, obj3); - - expect(stream.write.called).to.be.true; - const streamOutput = stream.write.getCall(0).args[0]; - expect(streamOutput).to.not.include("[object Object]"); - expect(streamOutput).to.include("first"); - expect(streamOutput).to.include("second"); - expect(streamOutput).to.include("third"); - expect(streamOutput).to.include("deeply"); - expect(streamOutput).to.include("nested"); - expect(streamOutput).to.include("item"); - expect(streamOutput).to.include("test"); - - expect(consoleFn.called).to.be.true; - const consoleCall = consoleFn.getCall(0); - expect(consoleCall).to.exist; - const consoleOutput = consoleCall.args.join(" "); - expect(consoleOutput).to.not.include("[object Object]"); - expect(consoleOutput).to.include("first"); - expect(consoleOutput).to.include("second"); - expect(consoleOutput).to.include("third"); - }); - }); -}); diff --git a/test/units/utils/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 index 23504c5..3620788 100644 --- a/test/units/utils/sendContactMail/sanitizeInput.property.test.js +++ b/test/units/utils/sendContactMail/sanitizeInput.property.test.js @@ -67,7 +67,7 @@ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;'\":,./?`~ \t", // Excludes \r, \n, <, > }), (input) => { - if (/[<>\r\n]/.test(input)) fc.pre(false); + 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. diff --git a/test/units/utils/sendNewsletterSubscriptionMail.test.js b/test/units/utils/sendNewsletterSubscriptionMail.test.js deleted file mode 100644 index 9a30e5d..0000000 --- a/test/units/utils/sendNewsletterSubscriptionMail.test.js +++ /dev/null @@ -1,51 +0,0 @@ -// test/units/utils/sendNewsletterSubscriptionMail.test.js -const sinon = require("sinon"); -const transporter = require("../../../src/utils/transporter"); -const { winstonLogger } = require("../../../src/utils/logging"); -const sendNewsletterSubscriptionMail = require("../../../src/utils/sendNewsletterSubscriptionMail"); - -describe("sendNewsletterSubscriptionMail", () => { - let sendMailStub; - let errorStub; - - beforeEach(() => { - process.env.MAIL_DOMAIN = "example.com"; - process.env.MAIL_NEWSLETTER = "newsletter@example.com"; - - sendMailStub = sinon.stub(transporter, "sendMail"); - errorStub = sinon.stub(winstonLogger, "error"); - }); - - afterEach(() => { - sendMailStub.restore(); - errorStub.restore(); - delete process.env.MAIL_DOMAIN; - delete process.env.MAIL_NEWSLETTER; - }); - - it("calls transporter.sendMail with correct mail data", async () => { - sendMailStub.resolves("sent"); - - const email = "user@example.com"; - const result = await sendNewsletterSubscriptionMail({ email }); - - sinon.assert.calledOnce(sendMailStub); - sinon.assert.calledWith(sendMailStub, { - from: "Newsletter ", - to: email, - subject: "New Newsletter Subscription", - text: "Please add this email to the newsletter list: newsletter@example.com", - }); - sinon.assert.notCalled(errorStub); - }); - - it("logs error when transporter.sendMail rejects", async () => { - const error = new Error("send failed"); - sendMailStub.rejects(error); - - await sendNewsletterSubscriptionMail({ email: "fail@example.com" }); - - sinon.assert.calledOnce(errorStub); - sinon.assert.calledWith(errorStub, error); - }); -}); diff --git a/test/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); + }); +});