160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
unlinkSync,
|
|
readdirSync,
|
|
mkdtempSync,
|
|
rmdirSync,
|
|
} from "fs";
|
|
import { join } from "path";
|
|
import { tmpdir } from "os";
|
|
|
|
// Dynamically import the module under test so we can control env vars per subtest.
|
|
const { getAppLogRotationCheckInterval } = await import("../../src/lib/logRotation.ts");
|
|
|
|
test("getAppLogRotationCheckInterval — default", () => {
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
assert.equal(getAppLogRotationCheckInterval(), 60_000);
|
|
});
|
|
|
|
test("getAppLogRotationCheckInterval — custom ms", () => {
|
|
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "30000";
|
|
try {
|
|
assert.equal(getAppLogRotationCheckInterval(), 30_000);
|
|
} finally {
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
}
|
|
});
|
|
|
|
test("getAppLogRotationCheckInterval — invalid value falls back to default", () => {
|
|
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "not-a-number";
|
|
try {
|
|
assert.equal(getAppLogRotationCheckInterval(), 60_000);
|
|
} finally {
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
}
|
|
});
|
|
|
|
test("getAppLogRotationCheckInterval — zero/negative falls back to default", () => {
|
|
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "0";
|
|
try {
|
|
assert.equal(getAppLogRotationCheckInterval(), 60_000);
|
|
} finally {
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
}
|
|
});
|
|
|
|
// ── rotateIfNeeded ────────────────────────────────────────────────────────────
|
|
|
|
test("rotateIfNeeded — skips when file does not exist", async () => {
|
|
const { rotateIfNeeded } = await import("../../src/lib/logRotation.ts");
|
|
rotateIfNeeded("/non/existent/path.log", 50 * 1024 * 1024); // must not throw
|
|
});
|
|
|
|
test("rotateIfNeeded — skips when file is below max size", async () => {
|
|
const { rotateIfNeeded } = await import("../../src/lib/logRotation.ts");
|
|
const dir = mkdtempSync(join(tmpdir(), `rot-test-small-${Date.now()}-`));
|
|
const logPath = join(dir, "app.log");
|
|
writeFileSync(logPath, "small content\n");
|
|
|
|
try {
|
|
// Call with explicit maxSize (50 MB) — bypasses any cached env-level config.
|
|
rotateIfNeeded(logPath, 50 * 1024 * 1024);
|
|
// File must still exist and not be rotated
|
|
assert.ok(existsSync(logPath), "log file should still exist");
|
|
// rotated = files that match the rotated pattern AND are NOT the active log itself
|
|
const rotated = readdirSync(dir).filter(
|
|
(f) => f.startsWith("app.") && f.endsWith(".log") && f !== "app.log"
|
|
);
|
|
assert.equal(rotated.length, 0, "no rotated files expected");
|
|
} finally {
|
|
unlinkSync(logPath);
|
|
rmdirSync(dir);
|
|
}
|
|
});
|
|
|
|
test("rotateIfNeeded — rotates when file exceeds max size", async () => {
|
|
const { rotateIfNeeded } = await import("../../src/lib/logRotation.ts");
|
|
const dir = mkdtempSync(join(tmpdir(), `rot-test-large-${Date.now()}-`));
|
|
const logPath = join(dir, "app.log");
|
|
// Write content larger than 100 bytes to trigger rotation (maxSize = 100)
|
|
writeFileSync(logPath, "x".repeat(150));
|
|
|
|
try {
|
|
rotateIfNeeded(logPath, 100);
|
|
// Original file must have been renamed
|
|
assert.ok(!existsSync(logPath), "original log file should be renamed");
|
|
const rotated = readdirSync(dir).find(
|
|
(f) => f.startsWith("app.") && f.endsWith(".log") && f !== "app.log"
|
|
);
|
|
assert.ok(rotated, "a rotated file should exist");
|
|
} finally {
|
|
for (const f of readdirSync(dir)) {
|
|
unlinkSync(join(dir, f));
|
|
}
|
|
rmdirSync(dir);
|
|
}
|
|
});
|
|
|
|
// ── closeLogRotation ───────────────────────────────────────────────────────────
|
|
|
|
test("closeLogRotation — idempotent (safe to call twice)", async () => {
|
|
// Temporarily set a very short check interval so initLogRotation starts a timer.
|
|
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "10000";
|
|
const { initLogRotation, closeLogRotation } = await import("../../src/lib/logRotation.ts");
|
|
|
|
try {
|
|
initLogRotation(); // starts a timer
|
|
closeLogRotation(); // stops it
|
|
closeLogRotation(); // must not throw — idempotent
|
|
} finally {
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
}
|
|
});
|
|
|
|
// ── Periodic rotation timer — integration ──────────────────────────────────────
|
|
|
|
test("periodic timer actually fires and rotates a large log", async () => {
|
|
// Use a short interval (250 ms) and small max size to trigger rotation quickly.
|
|
process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS = "250";
|
|
const { initLogRotation, closeLogRotation } = await import("../../src/lib/logRotation.ts");
|
|
|
|
const dir = mkdtempSync(join(tmpdir(), `rot-test-periodic-${Date.now()}-`));
|
|
const logPath = join(dir, "app.log");
|
|
// Write a file just under the 1 MB threshold
|
|
writeFileSync(logPath, "y".repeat(900 * 1024));
|
|
|
|
try {
|
|
// Override max file size to 1 MB via env (parseFileSize handles the suffix)
|
|
process.env.APP_LOG_MAX_FILE_SIZE = "1M";
|
|
initLogRotation();
|
|
|
|
// Append enough to exceed 1 MB — after the next timer tick (≤250 ms)
|
|
// the rotation should trigger.
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 600));
|
|
writeFileSync(logPath, "y".repeat(200 * 1024)); // now 1.1 MB total
|
|
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 600));
|
|
|
|
const rotatedExists =
|
|
readdirSync(dir).filter((f) => f.startsWith("app.") && f.endsWith(".log")).length > 0;
|
|
assert.ok(
|
|
rotatedExists,
|
|
"a rotated file should exist after periodic check fired on an oversized log"
|
|
);
|
|
} finally {
|
|
closeLogRotation();
|
|
delete process.env.APP_LOG_ROTATION_CHECK_INTERVAL_MS;
|
|
delete process.env.APP_LOG_MAX_FILE_SIZE;
|
|
for (const f of readdirSync(dir)) {
|
|
unlinkSync(join(dir, f));
|
|
}
|
|
rmdirSync(dir);
|
|
}
|
|
});
|
|
|
|
// (cleanup done inline with rmdirSync)
|