feat: add periodic database cleanup every 6 hours (TDD)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 18m15s

- Cleans expired verifications and orphaned usage rows
- Previously only ran once on startup (13d+ uptime = accumulation)
- Interval uses .unref() to not block graceful shutdown
- Stopped during shutdown before pool.end()
- Idempotent start (safe to call multiple times)
- 6 TDD tests added (periodic-cleanup.test.ts)
- 663 tests total, all passing
This commit is contained in:
Hoid 2026-03-11 11:06:09 +01:00
parent 75c6a6ce58
commit cc7de5ef49
3 changed files with 135 additions and 0 deletions

View file

@ -0,0 +1,98 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock the db module
vi.mock("../services/db.js", () => ({
cleanupStaleData: vi.fn().mockResolvedValue({ expiredVerifications: 0, orphanedUsage: 0 }),
}));
vi.mock("../services/logger.js", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
import {
startPeriodicCleanup,
stopPeriodicCleanup,
CLEANUP_INTERVAL_MS,
} from "../utils/periodic-cleanup.js";
import { cleanupStaleData } from "../services/db.js";
import logger from "../services/logger.js";
const mockCleanupStaleData = vi.mocked(cleanupStaleData);
describe("periodic-cleanup", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
stopPeriodicCleanup();
});
afterEach(() => {
stopPeriodicCleanup();
vi.useRealTimers();
});
it("should export a 6-hour cleanup interval constant", () => {
expect(CLEANUP_INTERVAL_MS).toBe(6 * 60 * 60 * 1000);
});
it("should call cleanupStaleData after one interval elapses", async () => {
startPeriodicCleanup();
expect(mockCleanupStaleData).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(1);
});
it("should call cleanupStaleData multiple times over multiple intervals", async () => {
startPeriodicCleanup();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(2);
});
it("should log errors but not throw when cleanupStaleData fails", async () => {
mockCleanupStaleData.mockRejectedValueOnce(new Error("DB connection lost"));
startPeriodicCleanup();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(logger.error).toHaveBeenCalledWith(
expect.objectContaining({ err: expect.any(Error) }),
expect.stringContaining("Periodic database cleanup failed")
);
// Next interval should still work
mockCleanupStaleData.mockResolvedValueOnce({ expiredVerifications: 1, orphanedUsage: 0 });
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(2);
});
it("should stop cleanup when stopPeriodicCleanup is called", async () => {
startPeriodicCleanup();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(1);
stopPeriodicCleanup();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(1);
});
it("should be idempotent — calling startPeriodicCleanup twice doesn't create two intervals", async () => {
startPeriodicCleanup();
startPeriodicCleanup();
await vi.advanceTimersByTimeAsync(CLEANUP_INTERVAL_MS);
expect(mockCleanupStaleData).toHaveBeenCalledTimes(1);
});
});