Files
discord-rpg-concept/shared/lib/logger.test.ts

119 lines
4.4 KiB
TypeScript

import { expect, test, describe, beforeAll, afterAll, spyOn } from "bun:test";
import { logger } from "./logger";
import { existsSync, unlinkSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
describe("Logger", () => {
const logDir = join(process.cwd(), "logs");
const logFile = join(logDir, "error.log");
beforeAll(() => {
// Cleanup if exists
try {
if (existsSync(logFile)) unlinkSync(logFile);
} catch (e) {}
});
test("should log info messages to console with correct format", () => {
const spy = spyOn(console, "log");
const message = "Formatting test";
logger.info("system", message);
expect(spy).toHaveBeenCalled();
const callArgs = spy.mock.calls[0]?.[0];
expect(callArgs).toBeDefined();
if (callArgs) {
// Strict regex check for ISO timestamp and format
const regex = /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[INFO\] \[SYSTEM\] Formatting test$/;
expect(callArgs).toMatch(regex);
}
spy.mockRestore();
});
test("should write error logs to file with stack trace", async () => {
const errorMessage = "Test error message";
const testError = new Error("Source error");
logger.error("system", errorMessage, testError);
// Polling for file write instead of fixed timeout
let content = "";
for (let i = 0; i < 20; i++) {
if (existsSync(logFile)) {
content = readFileSync(logFile, "utf-8");
if (content.includes("Source error")) break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
expect(content).toMatch(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[ERROR\] \[SYSTEM\] Test error message: Source error/);
expect(content).toContain("Stack Trace:");
expect(content).toContain("Error: Source error");
expect(content).toContain("logger.test.ts");
});
test("should handle log directory creation failures gracefully", async () => {
const consoleSpy = spyOn(console, "error");
// We trigger an error by trying to use a path that is a file where a directory should be
const triggerFile = join(process.cwd(), "logs_fail_trigger");
try {
writeFileSync(triggerFile, "not a directory");
// Manually override paths for this test instance
const originalLogDir = (logger as any).logDir;
const originalLogPath = (logger as any).errorLogPath;
(logger as any).logDir = triggerFile;
(logger as any).errorLogPath = join(triggerFile, "error.log");
(logger as any).initialized = false;
logger.error("system", "This should fail directory creation");
// Wait for async initialization attempt
await new Promise(resolve => setTimeout(resolve, 100));
expect(consoleSpy).toHaveBeenCalled();
expect(consoleSpy.mock.calls.some(call =>
String(call[0]).includes("Failed to initialize logger directory")
)).toBe(true);
// Reset logger state
(logger as any).logDir = originalLogDir;
(logger as any).errorLogPath = originalLogPath;
(logger as any).initialized = false;
} finally {
if (existsSync(triggerFile)) unlinkSync(triggerFile);
consoleSpy.mockRestore();
}
});
test("should include complex data objects in logs", () => {
const spy = spyOn(console, "log");
const data = { userId: "123", tags: ["test"] };
logger.info("bot", "Message with data", data);
expect(spy).toHaveBeenCalled();
const callArgs = spy.mock.calls[0]?.[0];
expect(callArgs).toBeDefined();
if (callArgs) {
expect(callArgs).toContain(` | Data: ${JSON.stringify(data)}`);
}
spy.mockRestore();
});
test("should handle circular references in data objects", () => {
const spy = spyOn(console, "log");
const data: any = { name: "circular" };
data.self = data;
logger.info("bot", "Circular test", data);
expect(spy).toHaveBeenCalled();
const callArgs = spy.mock.calls[0]?.[0];
expect(callArgs).toContain("[Circular]");
spy.mockRestore();
});
});