mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: expand coverage across jobs and tools (#380)
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getQueueMock = vi.hoisted(() => vi.fn());
|
||||
const runSiemForwardMock = vi.hoisted(() => vi.fn());
|
||||
const runAuditArchiveMock = vi.hoisted(() => vi.fn());
|
||||
const dbExecuteMock = vi.hoisted(() => vi.fn());
|
||||
const dbUpdateMock = vi.hoisted(() => vi.fn());
|
||||
const storageReconciliationJobMock = vi.hoisted(() => vi.fn());
|
||||
const gdprExportJobMock = vi.hoisted(() => vi.fn());
|
||||
const evaluateAlertsMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
async function loadSystemJobs(cleanupIntervalMinutes = 15) {
|
||||
vi.resetModules();
|
||||
getQueueMock.mockReset();
|
||||
runSiemForwardMock.mockReset();
|
||||
runAuditArchiveMock.mockReset();
|
||||
dbExecuteMock.mockReset();
|
||||
dbUpdateMock.mockReset();
|
||||
storageReconciliationJobMock.mockReset();
|
||||
gdprExportJobMock.mockReset();
|
||||
evaluateAlertsMock.mockReset();
|
||||
|
||||
vi.doMock("drizzle-orm", () => ({
|
||||
and: vi.fn(() => "and"),
|
||||
eq: vi.fn(() => "eq"),
|
||||
inArray: vi.fn(() => "inArray"),
|
||||
isNotNull: vi.fn(() => "isNotNull"),
|
||||
lt: vi.fn(() => "lt"),
|
||||
sql: vi.fn(() => "sql"),
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/config.js", () => ({
|
||||
env: {
|
||||
CLEANUP_INTERVAL_MINUTES: cleanupIntervalMinutes,
|
||||
JOBS_RETENTION_DAYS: 30,
|
||||
AUDIT_RETENTION_DAYS: 90,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/db/index.js", () => ({
|
||||
db: {
|
||||
execute: dbExecuteMock.mockResolvedValue(undefined),
|
||||
update: dbUpdateMock,
|
||||
},
|
||||
schema: {
|
||||
jobs: {
|
||||
id: "jobs.id",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/lib/cleanup.js", () => ({
|
||||
getMaxAgeMs: vi.fn().mockResolvedValue(0),
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/lib/object-storage.js", () => ({
|
||||
deletePrefix: vi.fn(),
|
||||
listJobDirs: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/lib/settings-helpers.js", () => ({
|
||||
getSettingNumber: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/audit-archive.js", () => ({
|
||||
runAuditArchive: runAuditArchiveMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/queues.js", () => ({
|
||||
getQueue: getQueueMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/siem-forward.js", () => ({
|
||||
runSiemForward: runSiemForwardMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/storage-reconciliation.js", () => ({
|
||||
storageReconciliationJob: storageReconciliationJobMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/gdpr-export.js", () => ({
|
||||
gdprExportJob: gdprExportJobMock,
|
||||
}));
|
||||
|
||||
vi.doMock("../../../../apps/api/src/jobs/alert-evaluator.js", () => ({
|
||||
evaluateAlerts: evaluateAlertsMock,
|
||||
}));
|
||||
|
||||
return import("../../../../apps/api/src/jobs/system-jobs.js");
|
||||
}
|
||||
|
||||
describe("system jobs behavior", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("decides expiry from local mtimes, S3 job rows, and rowless S3 directories", async () => {
|
||||
const { decideExpiry } = await loadSystemJobs();
|
||||
const cutoffMs = new Date("2026-06-29T12:00:00.000Z").getTime();
|
||||
const rowsById = new Map([
|
||||
[
|
||||
"job-old",
|
||||
{
|
||||
createdAt: new Date("2026-06-20T12:00:00.000Z"),
|
||||
completedAt: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
"job-new",
|
||||
{
|
||||
createdAt: new Date("2026-06-20T12:00:00.000Z"),
|
||||
completedAt: new Date("2026-06-29T12:01:00.000Z"),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
expect(decideExpiry({ key: "uploads/job-a", mtimeMs: cutoffMs - 1 }, cutoffMs, rowsById)).toBe(
|
||||
"expired",
|
||||
);
|
||||
expect(decideExpiry({ key: "outputs/job-b", mtimeMs: cutoffMs }, cutoffMs, rowsById)).toBe(
|
||||
"keep",
|
||||
);
|
||||
expect(decideExpiry({ key: "uploads/job-old", mtimeMs: 0 }, cutoffMs, rowsById)).toBe(
|
||||
"expired",
|
||||
);
|
||||
expect(decideExpiry({ key: "outputs/job-new", mtimeMs: 0 }, cutoffMs, rowsById)).toBe("keep");
|
||||
expect(decideExpiry({ key: "uploads/orphan", mtimeMs: 0 }, cutoffMs, rowsById)).toBe("skip");
|
||||
});
|
||||
|
||||
it("schedules repeatable jobs and removes storage TTL scheduler when cleanup is disabled", async () => {
|
||||
const queue = {
|
||||
upsertJobScheduler: vi.fn().mockResolvedValue(undefined),
|
||||
removeJobScheduler: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const { SYSTEM_JOBS, scheduleSystemJobs } = await loadSystemJobs(0);
|
||||
getQueueMock.mockReturnValue(queue);
|
||||
|
||||
await scheduleSystemJobs();
|
||||
|
||||
expect(queue.removeJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.storageTtl);
|
||||
expect(queue.upsertJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.sessionPurge, {
|
||||
every: 60 * 60_000,
|
||||
});
|
||||
expect(queue.upsertJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.retention, {
|
||||
every: 6 * 60 * 60_000,
|
||||
});
|
||||
expect(queue.upsertJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.auditArchive, {
|
||||
pattern: "0 2 1 * *",
|
||||
});
|
||||
expect(queue.upsertJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.storageReconciliation, {
|
||||
pattern: "0 3 * * 0",
|
||||
});
|
||||
expect(queue.upsertJobScheduler).toHaveBeenCalledWith(SYSTEM_JOBS.alertEvaluator, {
|
||||
every: 60_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("dispatches one-shot system jobs and updates GDPR export job rows", async () => {
|
||||
const updateWhere = vi.fn().mockResolvedValue(undefined);
|
||||
const updateSet = vi.fn(() => ({ where: updateWhere }));
|
||||
const { SYSTEM_JOBS, runSystemJob } = await loadSystemJobs();
|
||||
dbUpdateMock.mockReturnValue({ set: updateSet });
|
||||
runSiemForwardMock.mockResolvedValue({ forwarded: 2 });
|
||||
gdprExportJobMock.mockResolvedValue({ outputRef: "outputs/export-job/gdpr-export.zip" });
|
||||
storageReconciliationJobMock.mockResolvedValue(undefined);
|
||||
evaluateAlertsMock.mockResolvedValue(undefined);
|
||||
|
||||
await expect(runSystemJob({ name: SYSTEM_JOBS.siemForward } as never)).resolves.toEqual({
|
||||
forwarded: 2,
|
||||
});
|
||||
await expect(runSystemJob({ name: SYSTEM_JOBS.storageReconciliation } as never)).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
await expect(
|
||||
runSystemJob({
|
||||
name: SYSTEM_JOBS.gdprExport,
|
||||
data: { userId: "user-1", jobId: "export-job" },
|
||||
} as never),
|
||||
).resolves.toEqual({ outputRef: "outputs/export-job/gdpr-export.zip" });
|
||||
await expect(runSystemJob({ name: SYSTEM_JOBS.alertEvaluator } as never)).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(gdprExportJobMock).toHaveBeenCalledWith("user-1", "export-job");
|
||||
expect(updateSet).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
status: "completed",
|
||||
outputRefs: ["outputs/export-job/gdpr-export.zip"],
|
||||
}),
|
||||
);
|
||||
await expect(runSystemJob({ name: "system:unknown" } as never)).rejects.toThrow(
|
||||
"Unknown system job: system:unknown",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user