Newer
Older
express-blog / test / utils / logging / object-formatting.unit.test.js
@Jason Jason on 26 Jul 16 KB modified: package.json
const { expect } = require("chai");
const sinon = require("sinon");
const fs = require("fs");
const path = require("path");
const { Writable } = require("stream");

// Mock dependencies
const mockLogStreams = {
  info: new Writable({ write() {} }),
  error: new Writable({ write() {} }),
  warn: new Writable({ write() {} }),
  debug: new Writable({ write() {} }),
  notice: new Writable({ write() {} }),
  security: new Writable({ write() {} }),
  analytics: new Writable({ write() {} }),
  event: new Writable({ write() {} }),
};

const mockSessionTransport = {
  write: sinon.stub(),
};

// Import the modules under test
const { writeLog } = require("../../../src/utils/logging/consolePatch");
const {
  manualLogger,
  winstonLogger,
} = require("../../../src/utils/logging/index");

describe("Logger Object Expansion Tests", () => {
  let streamWriteStubs;

  beforeEach(() => {
    // Create fresh stream write stubs for each test
    streamWriteStubs = {
      info: sinon.stub(mockLogStreams.info, "write"),
      error: sinon.stub(mockLogStreams.error, "write"),
      warn: sinon.stub(mockLogStreams.warn, "write"),
      security: sinon.stub(mockLogStreams.security, "write"),
      event: sinon.stub(mockLogStreams.event, "write"),
      analytics: sinon.stub(mockLogStreams.analytics, "write"),
      debug: sinon.stub(mockLogStreams.debug, "write"),
      notice: sinon.stub(mockLogStreams.notice, "write"),
    };

    // Reset session transport
    mockSessionTransport.write.reset();
  });

  afterEach(() => {
    // Restore all stubs
    sinon.restore(); // This restores all stubs created by sinon.stub()
  });

  describe("writeLog function", () => {
    let consoleLogStub; // Declare stub for this describe block

    beforeEach(() => {
      // Stub console.log specifically for this describe block
      consoleLogStub = sinon.stub(console, "log");
    });

    afterEach(() => {
      // Restore console.log stub after each test in this block
      consoleLogStub.restore();
    });

    it("should never log [object Object] for simple objects", () => {
      const testObject = { name: "test", value: 42 };

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        testObject
      );

      // Check console output
      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("name");
      expect(outputString).to.include("test");
      expect(outputString).to.include("value");
      expect(outputString).to.include("42");

      // Check stream output
      expect(streamWriteStubs.info.called).to.be.true;
      const streamOutput = streamWriteStubs.info.getCall(0).args[0];
      expect(streamOutput).to.not.include("[object Object]");
      expect(streamOutput).to.include("name");
      expect(streamOutput).to.include("test");
    });

    it("should properly expand nested objects", () => {
      const nestedObject = {
        user: {
          id: 123,
          profile: {
            name: "John Doe",
            settings: { theme: "dark", notifications: true },
          },
        },
      };

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        nestedObject
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("John Doe");
      expect(outputString).to.include("theme");
      expect(outputString).to.include("dark");
      expect(outputString).to.include("notifications");
    });

    it("should handle circular references without [object Object]", () => {
      const circularObj = { name: "circular" };
      circularObj.self = circularObj;
      circularObj.nested = { parent: circularObj };

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        circularObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("name");
      expect(outputString).to.include("circular");
      // Should handle circular reference gracefully
      expect(outputString).to.include("self");
    });

    it("should expand arrays containing objects", () => {
      const arrayWithObjects = [
        { id: 1, name: "first" },
        { id: 2, name: "second", nested: { value: "test" } },
      ];

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        arrayWithObjects
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("first");
      expect(outputString).to.include("second");
      expect(outputString).to.include("nested");
      expect(outputString).to.include("test");
    });

    it("should handle mixed argument types without [object Object]", () => {
      const mixedArgs = [
        "String message",
        { obj: "value" },
        42,
        ["array", "items"],
        { deeply: { nested: { object: "here" } } },
      ];

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        ...mixedArgs
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("String message");
      expect(outputString).to.include("obj");
      expect(outputString).to.include("value");
      expect(outputString).to.include("deeply");
      expect(outputString).to.include("here");
    });

    it("should handle Error objects without [object Object]", () => {
      const error = new Error("Test error");
      error.customProperty = { details: "additional info" };

      // Stub console.error for this test (local stub, not interfering with console.log stub)
      const consoleErrorStub = sinon.stub(console, "error");

      writeLog(
        "ERROR",
        mockLogStreams.error,
        mockSessionTransport,
        console.error,
        error
      );

      expect(consoleErrorStub.called).to.be.true;
      const consoleArgs = consoleErrorStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("Test error");

      consoleErrorStub.restore();
    });

    it("should handle objects with special properties", () => {
      const specialObj = {
        toString: () => "custom toString",
        valueOf: () => 99,
        [Symbol.toStringTag]: "CustomObject",
        normalProp: "normal value",
      };

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        specialObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("normalProp");
      expect(outputString).to.include("normal value");
    });
  });

  describe("Manual Logger Methods", () => {
    let manualLoggerStubs;
    // Removed writeLogStub as manualLogger directly interacts with console

    beforeEach(() => {
      // Create fresh stubs for manual logger streams if they exist
      manualLoggerStubs = {};
      if (manualLogger.streams) {
        Object.keys(manualLogger.streams).forEach((level) => {
          if (
            manualLogger.streams[level] &&
            typeof manualLogger.streams[level].write === "function"
          ) {
            if (!manualLogger.streams[level].write.isSinonProxy) {
              manualLoggerStubs[level] = sinon.stub(
                manualLogger.streams[level],
                "write"
              );
            }
          }
        });
      }
    });

    afterEach(() => {
      // Restore all stubs created within this describe block or its tests
      sinon.restore();
    });

    it("should not produce [object Object] in manualLogger.info", () => {
      const testObj = { key: "value", nested: { deep: "property" } };

      // Stub console.log locally for this specific test
      const consoleLogStub = sinon.stub(console, "log");
      manualLogger.info(testObj);

      expect(consoleLogStub.called).to.be.true; // Check if console.log was called
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" "); // Join them to check content
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("key");
      expect(outputString).to.include("nested");
      expect(outputString).to.include("deep");
      consoleLogStub.restore(); // Restore after test
    });

    it("should not produce [object Object] in manualLogger.error", () => {
      const errorObj = {
        error: "Something went wrong",
        context: { userId: 123, action: "login" },
      };

      // Stub console.error for this test
      const consoleErrorStub = sinon.stub(console, "error");

      manualLogger.error(errorObj);

      expect(consoleErrorStub.called).to.be.true;
      const consoleArgs = consoleErrorStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("Something went wrong");
      expect(outputString).to.include("userId");
      expect(outputString).to.include("login");

      consoleErrorStub.restore();
    });
  });

  describe("Winston Logger", () => {
    let winstonInfoStub;

    beforeEach(() => {
      winstonInfoStub = sinon.stub(winstonLogger, "info");
    });

    afterEach(() => {
      winstonInfoStub.restore(); // Ensure winston stub is restored
    });

    it("should not produce [object Object] in winston logs", () => {
      const logData = {
        user: { id: 456, name: "Jane" },
        action: "update",
        metadata: { timestamp: Date.now() },
      };

      winstonLogger.info("User action", logData);

      // Check that winston was called with properly formatted data
      expect(winstonInfoStub.called).to.be.true;
      const logCall = winstonInfoStub.getCall(0).args;
      // Winston typically stringifies objects, so we check the stringified output
      const logString = JSON.stringify(logCall);
      expect(logString).to.not.include("[object Object]");
      expect(logString).to.include("Jane");
      expect(logString).to.include("update");
    });
  });

  describe("Edge Cases", () => {
    let consoleLogStub; // Declare stub for this describe block

    beforeEach(() => {
      // Stub console.log specifically for this describe block
      consoleLogStub = sinon.stub(console, "log");
    });

    afterEach(() => {
      // Restore console.log stub after each test in this block
      consoleLogStub.restore();
    });

    it("should handle null and undefined without [object Object]", () => {
      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        null,
        undefined
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("null");
      expect(outputString).to.include("undefined");
    });

    it("should handle objects with null prototype", () => {
      const nullProtoObj = Object.create(null);
      nullProtoObj.key = "value";

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        nullProtoObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("key");
      expect(outputString).to.include("value");
    });

    it("should handle Date objects", () => {
      const dateObj = new Date("2023-01-01T12:00:00.000Z"); // Use ISO string for consistent output

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        dateObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      // Check for parts of the date string that are likely to be present in ISO format
      // Console.log's output for Date objects can vary, but the ISO string is often included or derived.
      expect(outputString).to.include("2023");
      // Check for the time part of the ISO string for more robustness
      expect(outputString).to.include("T12:00:00.000Z");
    });

    it("should handle RegExp objects", () => {
      const regexObj = /test.*pattern/gi;

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        regexObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("test");
      expect(outputString).to.include("pattern");
      expect(outputString).to.include("gi"); // Check for flags
    });

    it("should handle very deeply nested objects", () => {
      let deepObj = { level: 0 };
      let current = deepObj;

      // Create 10 levels deep
      for (let i = 1; i <= 10; i++) {
        current.next = { level: i };
        current = current.next;
      }

      writeLog(
        "INFO",
        mockLogStreams.info,
        mockSessionTransport,
        console.log,
        deepObj
      );

      expect(consoleLogStub.called).to.be.true;
      const consoleArgs = consoleLogStub.getCall(0).args;
      expect(consoleArgs).to.exist;
      const outputString = consoleArgs.join(" ");
      expect(outputString).to.not.include("[object Object]");
      expect(outputString).to.include("level");
      // Check for presence of multiple levels
      expect(outputString.match(/level/g).length).to.be.at.least(10);
    });
  });

  describe("Stream Output Validation", () => {
    let consoleLogStub; // Declare stub for this describe block

    beforeEach(() => {
      // Stub console.log specifically for this describe block
      consoleLogStub = sinon.stub(console, "log");
    });

    afterEach(() => {
      // Restore console.log stub after each test in this block
      consoleLogStub.restore();
    });

    it("should ensure stream writes never contain [object Object]", () => {
      const testObjects = [
        { simple: "object" },
        { nested: { deep: { value: "test" } } },
        [{ array: "item" }],
        { mixed: ["array", { in: "object" }] },
      ];

      testObjects.forEach((obj, index) => {
        streamWriteStubs.info.resetHistory(); // Reset history for each iteration
        writeLog(
          "INFO",
          mockLogStreams.info,
          mockSessionTransport,
          console.log,
          obj
        );

        expect(streamWriteStubs.info.called).to.be.true;
        const streamWrites = streamWriteStubs.info.getCalls();
        streamWrites.forEach((call) => {
          const writeData = call.args[0];
          // Ensure the written data is a string and does not contain "[object Object]"
          expect(typeof writeData).to.equal("string");
          expect(writeData).to.not.include("[object Object]");
        });
      });
    });
  });
});