/**
* mysql-mcp - Logger Unit Tests
*
* Tests for the centralized logger module including
* sensitive data redaction, log level control, and message formatting.
*/
import {
describe,
it,
expect,
beforeEach,
afterEach,
vi,
type Mock,
} from "vitest";
import { logger } from "../logger.js";
describe("Logger", () => {
let originalConsoleError: typeof console.error;
let consoleErrorSpy: Mock;
beforeEach(() => {
originalConsoleError = console.error;
consoleErrorSpy = vi.fn();
console.error = consoleErrorSpy as unknown as typeof console.error;
// Reset to default level
logger.setLevel("info");
});
afterEach(() => {
console.error = originalConsoleError;
});
describe("log level control", () => {
it("should get current log level", () => {
expect(logger.getLevel()).toBe("info");
});
it("should set log level to debug", () => {
logger.setLevel("debug");
expect(logger.getLevel()).toBe("debug");
});
it("should set log level to warning", () => {
logger.setLevel("warning");
expect(logger.getLevel()).toBe("warning");
});
it("should set log level to error", () => {
logger.setLevel("error");
expect(logger.getLevel()).toBe("error");
});
it("should filter debug messages when level is info", () => {
logger.setLevel("info");
logger.debug("Debug message");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it("should allow debug messages when level is debug", () => {
logger.setLevel("debug");
logger.debug("Debug message");
expect(consoleErrorSpy).toHaveBeenCalled();
});
it("should filter info messages when level is warning", () => {
logger.setLevel("warning");
logger.info("Info message");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it("should filter warn messages when level is error", () => {
logger.setLevel("error");
logger.warn("Warning message");
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it("should always allow error messages", () => {
logger.setLevel("error");
logger.error("Error message");
expect(consoleErrorSpy).toHaveBeenCalled();
});
});
describe("basic logging", () => {
it("should log info messages", () => {
logger.info("Test info message");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
// New structured format: [timestamp] [LEVEL] [MODULE] message
expect(output).toContain("[SERVER]");
expect(output).toContain("[INFO]");
expect(output).toContain("Test info message");
});
it("should log warn messages", () => {
logger.warn("Test warning");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[WARNING]");
});
it("should log error messages", () => {
logger.error("Test error");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[ERROR]");
});
it("should log debug messages when level is debug", () => {
logger.setLevel("debug");
logger.debug("Test debug");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[DEBUG]");
});
});
describe("context logging", () => {
it("should include context in log output", () => {
logger.info("Message with context", { key: "value" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain('"key":"value"');
});
it("should handle empty context", () => {
logger.info("Message", {});
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("{}");
});
});
describe("sensitive data redaction", () => {
it("should redact password in context", () => {
logger.info("Login attempt", { password: "secret123" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[REDACTED]");
expect(output).not.toContain("secret123");
});
it("should redact token in context", () => {
logger.info("Auth check", {
token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
});
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
});
it("should redact secret in context", () => {
logger.info("Config", { secret: "mysecretvalue" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("mysecretvalue");
});
it("should redact apiKey in context", () => {
logger.info("API call", { apiKey: "sk_test_12345" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("sk_test_12345");
});
it("should redact authorization in context", () => {
logger.info("Request", { authorization: "Bearer token123" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("token123");
});
it("should redact nested sensitive values", () => {
logger.info("Config", {
database: {
password: "dbpass123",
},
});
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("dbpass123");
});
it("should redact password= pattern in message", () => {
logger.info("Connection: password=secretpass");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[REDACTED]");
expect(output).not.toContain("secretpass");
});
it("should redact password: pattern in message", () => {
logger.info("Config password: mysecret123");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("mysecret123");
});
it("should redact authorization bearer header in message", () => {
logger.info("Header: authorization: bearer mytoken123");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("mytoken123");
});
it("should redact MySQL connection string in message", () => {
logger.info("Connecting to mysql://root:password123@localhost/db");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("password123");
});
it("should handle very long strings by truncating", () => {
const longString = "a".repeat(15000);
logger.info(longString);
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[TRUNCATED]");
});
it("should preserve safe values in context", () => {
logger.info("Stats", { host: "localhost", port: 3306 });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain('"host":"localhost"');
expect(output).toContain('"port":3306');
});
});
describe("control character sanitization", () => {
it("should remove null characters from message", () => {
logger.info("Message with\x00null");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("Message withnull");
expect(output).not.toContain("\x00");
});
it("should remove control characters from message", () => {
logger.info("Message\x01\x02\x03test");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("Messagetest");
});
it("should preserve tabs, newlines, and carriage returns", () => {
logger.info("Line1\nLine2\tTabbed\rReturn");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("Line1\nLine2\tTabbed\rReturn");
});
it("should remove DEL character (127)", () => {
logger.info("Delete\x7Fme");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("Deleteme");
expect(output).not.toContain("\x7F");
});
it("should handle empty strings", () => {
logger.info("");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("INFO");
});
});
describe("string redaction in context values", () => {
it("should redact sensitive patterns in string context values", () => {
logger.info("Request", {
headers: "authorization: bearer secret_token_value",
});
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).not.toContain("secret_token_value");
});
it("should handle non-string context values", () => {
logger.info("Numbers", { count: 42, enabled: true, data: null });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain('"count":42');
expect(output).toContain('"enabled":true');
});
});
describe("logger configuration", () => {
it("should set and get logger name", () => {
logger.setLoggerName("test-logger");
expect(logger.getLoggerName()).toBe("test-logger");
// Reset
logger.setLoggerName("mysql-mcp");
});
it("should set default module", () => {
logger.setDefaultModule("ADAPTER");
logger.info("Test message");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[ADAPTER]");
// Reset
logger.setDefaultModule("SERVER");
});
});
describe("additional log levels", () => {
it("should log notice messages", () => {
logger.notice("Notice message");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[NOTICE]");
});
it("should log warning messages via warning method", () => {
logger.warning("Warning via warning");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[WARNING]");
});
it("should log critical messages", () => {
logger.critical("Critical message");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[CRITICAL]");
});
it("should log alert messages", () => {
logger.alert("Alert message");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[ALERT]");
});
it("should log emergency messages", () => {
logger.emergency("Emergency message");
expect(consoleErrorSpy).toHaveBeenCalled();
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[EMERGENCY]");
});
});
describe("module-scoped logger", () => {
it("should create module-scoped logger with forModule", () => {
const moduleLogger = logger.forModule("ADAPTER");
moduleLogger.info("Module message");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[ADAPTER]");
});
it("should log debug via module logger", () => {
logger.setLevel("debug");
const moduleLogger = logger.forModule("QUERY");
moduleLogger.debug("Debug from module");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[QUERY]");
expect(output).toContain("[DEBUG]");
});
it("should log notice via module logger", () => {
const moduleLogger = logger.forModule("POOL");
moduleLogger.notice("Pool notice");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[POOL]");
expect(output).toContain("[NOTICE]");
});
it("should log warn via module logger", () => {
const moduleLogger = logger.forModule("AUTH");
moduleLogger.warn("Auth warning");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[AUTH]");
expect(output).toContain("[WARNING]");
});
it("should log warning via module logger", () => {
const moduleLogger = logger.forModule("TRANSPORT");
moduleLogger.warning("Transport warning");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[TRANSPORT]");
expect(output).toContain("[WARNING]");
});
it("should log error via module logger", () => {
const moduleLogger = logger.forModule("TOOLS");
moduleLogger.error("Tool error");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[TOOLS]");
expect(output).toContain("[ERROR]");
});
it("should log critical via module logger", () => {
const moduleLogger = logger.forModule("RESOURCES");
moduleLogger.critical("Resource critical");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[RESOURCES]");
expect(output).toContain("[CRITICAL]");
});
it("should log alert via module logger", () => {
const moduleLogger = logger.forModule("CLI");
moduleLogger.alert("CLI alert");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[CLI]");
expect(output).toContain("[ALERT]");
});
it("should log emergency via module logger", () => {
const moduleLogger = logger.forModule("SERVER");
moduleLogger.emergency("Server emergency");
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[SERVER]");
expect(output).toContain("[EMERGENCY]");
});
});
describe("log formatting with code", () => {
it("should include code in log output when provided", () => {
logger.error("Connection failed", { code: "MYSQL_CONNECT_FAILED" });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[MYSQL_CONNECT_FAILED]");
});
it("should handle module in context", () => {
logger.info("Test", { module: "AUTH" as const });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[AUTH]");
});
});
describe("edge cases", () => {
it("should handle arrays in context", () => {
logger.info("List", { items: [1, 2, 3] });
const output = consoleErrorSpy.mock.calls[0][0];
expect(output).toContain("[1,2,3]");
});
it("should handle null in sensitive context", () => {
logger.info("Test", { password: null });
const output = consoleErrorSpy.mock.calls[0][0];
// null should not be redacted
expect(output).toContain("null");
});
it("should handle undefined in sensitive context", () => {
logger.info("Test", { password: undefined });
// undefined values should not appear in output
expect(consoleErrorSpy).toHaveBeenCalled();
});
});
});