mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const insertedValues = vi.hoisted(() => vi.fn());
|
|
const queueAdd = vi.hoisted(() => vi.fn());
|
|
const getJob = vi.hoisted(() => vi.fn());
|
|
const queueEventClose = vi.hoisted(() => vi.fn());
|
|
const flowProducerClose = vi.hoisted(() => vi.fn());
|
|
|
|
async function loadEnqueueModule() {
|
|
vi.resetModules();
|
|
insertedValues.mockReset();
|
|
queueAdd.mockReset();
|
|
getJob.mockReset();
|
|
queueEventClose.mockReset();
|
|
flowProducerClose.mockReset();
|
|
|
|
queueAdd.mockResolvedValue({ id: "job-1" });
|
|
queueEventClose.mockResolvedValue(undefined);
|
|
flowProducerClose.mockResolvedValue(undefined);
|
|
|
|
vi.doMock("bullmq", () => ({
|
|
QueueEvents: vi.fn(() => ({
|
|
close: queueEventClose,
|
|
waitUntilReady: vi.fn().mockResolvedValue(undefined),
|
|
})),
|
|
FlowProducer: vi.fn(() => ({
|
|
close: flowProducerClose,
|
|
})),
|
|
}));
|
|
|
|
vi.doMock("../../../../apps/api/src/config.js", () => ({
|
|
env: { SYNC_WAIT_MS: 50 },
|
|
}));
|
|
|
|
vi.doMock("../../../../apps/api/src/db/index.js", () => ({
|
|
db: {
|
|
insert: vi.fn(() => ({
|
|
values: insertedValues.mockResolvedValue(undefined),
|
|
})),
|
|
},
|
|
schema: {
|
|
jobs: {},
|
|
},
|
|
}));
|
|
|
|
vi.doMock("../../../../apps/api/src/jobs/connection.js", () => ({
|
|
createBullMQConnection: vi.fn(() => ({ mocked: "connection" })),
|
|
}));
|
|
|
|
vi.doMock("../../../../apps/api/src/jobs/queues.js", () => ({
|
|
getQueue: vi.fn(() => ({
|
|
add: queueAdd,
|
|
getJob,
|
|
})),
|
|
}));
|
|
|
|
return import("../../../../apps/api/src/jobs/enqueue.js");
|
|
}
|
|
|
|
describe("job enqueue helpers", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("strips NUL bytes recursively before persisting settings but keeps queue data intact", async () => {
|
|
const { enqueueToolJob } = await loadEnqueueModule();
|
|
const data = {
|
|
jobId: "job-1",
|
|
userId: null,
|
|
toolId: "tool-a",
|
|
pool: "image",
|
|
kind: "single",
|
|
inputRefs: ["uploads/job-1/input.png"],
|
|
filename: "input.png",
|
|
settings: {
|
|
title: "a\0b",
|
|
nested: { value: "c\0d" },
|
|
list: ["e\0f", 1],
|
|
},
|
|
} as never;
|
|
|
|
await enqueueToolJob(data);
|
|
|
|
expect(insertedValues).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
id: "job-1",
|
|
settings: {
|
|
title: "ab",
|
|
nested: { value: "cd" },
|
|
list: ["ef", 1],
|
|
},
|
|
}),
|
|
);
|
|
expect(queueAdd).toHaveBeenCalledWith(
|
|
"tool-a",
|
|
expect.objectContaining({
|
|
settings: {
|
|
title: "a\0b",
|
|
nested: { value: "c\0d" },
|
|
list: ["e\0f", 1],
|
|
},
|
|
}),
|
|
{ jobId: "job-1" },
|
|
);
|
|
});
|
|
|
|
it("persists redacted dbSettings while enqueueing real settings", async () => {
|
|
const { enqueueToolJob } = await loadEnqueueModule();
|
|
|
|
await enqueueToolJob({
|
|
jobId: "job-2",
|
|
userId: null,
|
|
toolId: "ftp-upload",
|
|
pool: "system",
|
|
kind: "single",
|
|
inputRefs: [],
|
|
filename: "file.txt",
|
|
settings: { password: "secret" },
|
|
dbSettings: { password: "[redacted]" },
|
|
} as never);
|
|
|
|
expect(insertedValues).toHaveBeenCalledWith(
|
|
expect.objectContaining({ settings: { password: "[redacted]" } }),
|
|
);
|
|
expect(queueAdd).toHaveBeenCalledWith(
|
|
"ftp-upload",
|
|
expect.objectContaining({ settings: { password: "secret" } }),
|
|
{ jobId: "job-2" },
|
|
);
|
|
});
|
|
|
|
it("waitForJob returns null when the job is missing or the sync window times out", async () => {
|
|
const { waitForJob } = await loadEnqueueModule();
|
|
|
|
getJob.mockResolvedValueOnce(undefined);
|
|
await expect(waitForJob("image", "missing")).resolves.toBeNull();
|
|
|
|
getJob.mockResolvedValueOnce({
|
|
waitUntilFinished: vi.fn().mockRejectedValue(new Error("job timed out before finishing")),
|
|
});
|
|
await expect(waitForJob("image", "slow", 25)).resolves.toBeNull();
|
|
});
|
|
|
|
it("waitForJob rethrows real job failures", async () => {
|
|
const { waitForJob } = await loadEnqueueModule();
|
|
getJob.mockResolvedValueOnce({
|
|
waitUntilFinished: vi.fn().mockRejectedValue(new Error("processor failed")),
|
|
});
|
|
|
|
await expect(waitForJob("image", "failed")).rejects.toThrow("processor failed");
|
|
});
|
|
|
|
it("closes lazy QueueEvents and FlowProducer singletons", async () => {
|
|
const { closeFlowProducer, closeQueueEvents, getFlowProducer, warmQueueEvents, waitForJob } =
|
|
await loadEnqueueModule();
|
|
|
|
await warmQueueEvents();
|
|
getFlowProducer();
|
|
getJob.mockResolvedValueOnce(undefined);
|
|
await waitForJob("image", "job-1");
|
|
|
|
await closeQueueEvents();
|
|
await closeFlowProducer();
|
|
|
|
expect(queueEventClose).toHaveBeenCalled();
|
|
expect(flowProducerClose).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|