212 lines
6.1 KiB
TypeScript
212 lines
6.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { queueTask } from "./queueTask";
|
|
|
|
describe("queueTask - delayed task handling", () => {
|
|
const MAX_DELAY_SECONDS = 12 * 60 * 60; // 43,200 seconds
|
|
|
|
let mockEnv: Cloudflare.Env;
|
|
let kvPutSpy: ReturnType<typeof vi.fn>;
|
|
let queueSendSpy: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
kvPutSpy = vi.fn(() => Promise.resolve());
|
|
queueSendSpy = vi.fn(() => Promise.resolve());
|
|
|
|
mockEnv = {
|
|
DELAYED_TASKS: {
|
|
put: kvPutSpy,
|
|
get: vi.fn(() => Promise.resolve(null)),
|
|
delete: vi.fn(() => Promise.resolve()),
|
|
list: vi.fn(() => Promise.resolve({ keys: [], list_complete: true })),
|
|
getWithMetadata: vi.fn(() =>
|
|
Promise.resolve({ value: null, metadata: null }),
|
|
),
|
|
} as any,
|
|
NEW_EPISODE: {
|
|
send: queueSendSpy,
|
|
} as any,
|
|
ANILIST_UPDATES: {
|
|
send: vi.fn(() => Promise.resolve()),
|
|
} as any,
|
|
} as any;
|
|
|
|
// Mock crypto.randomUUID
|
|
(globalThis as any).crypto = { randomUUID: vi.fn(() => "test-uuid-123") };
|
|
});
|
|
|
|
describe("tasks with delay <= 9 hours", () => {
|
|
it("queues task directly when delay is less than 9 hours", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 123, episodeNumber: 1 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 6 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
// Should queue directly
|
|
expect(queueSendSpy).toHaveBeenCalledTimes(1);
|
|
// Should NOT store in KV
|
|
expect(kvPutSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("queues task directly when delay is exactly 9 hours", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 456, episodeNumber: 2 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 9 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
expect(queueSendSpy).toHaveBeenCalledTimes(1);
|
|
expect(kvPutSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("passes correct delay to queue", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 789, episodeNumber: 3 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 3, minutes: 30 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
const callArgs = queueSendSpy.mock.calls[0];
|
|
expect(callArgs[1].delaySeconds).toBe(3 * 3600 + 30 * 60);
|
|
});
|
|
});
|
|
|
|
describe("tasks with delay > 12 hours", () => {
|
|
it("stores task in KV when delay exceeds 12 hours", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 111, episodeNumber: 4 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 24 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
// Should store in KV
|
|
expect(kvPutSpy).toHaveBeenCalledTimes(1);
|
|
// Should NOT queue directly
|
|
expect(queueSendSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores task in KV when delay is 12 hours + 1 second", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 222, episodeNumber: 5 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 12, seconds: 1 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
expect(kvPutSpy).toHaveBeenCalledTimes(1);
|
|
expect(queueSendSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("generates correct KV key format", async () => {
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 333, episodeNumber: 6 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 48 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
const kvKey = kvPutSpy.mock.calls[0][0];
|
|
expect(kvKey).toMatch(/^delayed-task:\d+:test-uuid-123$/);
|
|
|
|
// Verify timestamp is approximately correct (within 1 second)
|
|
const timestampMatch = kvKey.match(/delayed-task:(\d+):/);
|
|
if (timestampMatch) {
|
|
const storedTimestamp = parseInt(timestampMatch[1]);
|
|
const expectedTimestamp = nowSeconds + 48 * 3600;
|
|
expect(Math.abs(storedTimestamp - expectedTimestamp)).toBeLessThan(2);
|
|
}
|
|
});
|
|
|
|
it("stores correct metadata in KV", async () => {
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 444, episodeNumber: 7 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 36 } },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
const kvValue = kvPutSpy.mock.calls[0][1];
|
|
const metadata = JSON.parse(kvValue);
|
|
|
|
expect(metadata.queueName).toBe("NEW_EPISODE");
|
|
expect(metadata.body).toEqual({ aniListId: 444, episodeNumber: 7 });
|
|
expect(metadata.taskId).toBe("test-uuid-123");
|
|
expect(metadata.retryCount).toBe(0);
|
|
expect(metadata.headers).toBeDefined();
|
|
expect(metadata.scheduledEpochTime).toBeTypeOf("number");
|
|
expect(metadata.createdAt).toBeTypeOf("number");
|
|
});
|
|
|
|
it("throws error when DELAYED_TASKS KV is not available", async () => {
|
|
const envWithoutKV = { ...mockEnv };
|
|
delete (envWithoutKV as any).DELAYED_TASKS;
|
|
|
|
await expect(
|
|
queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 555, episodeNumber: 8 },
|
|
{
|
|
scheduleConfig: { delay: { hours: 24 } },
|
|
env: envWithoutKV,
|
|
},
|
|
),
|
|
).rejects.toThrow("DELAYED_TASKS KV namespace not available");
|
|
});
|
|
});
|
|
|
|
describe("epoch time scheduling", () => {
|
|
it("queues directly when epoch time is within 12 hours", async () => {
|
|
const futureTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
|
|
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 666, episodeNumber: 9 },
|
|
{
|
|
scheduleConfig: { epochTime: futureTime },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
expect(queueSendSpy).toHaveBeenCalledTimes(1);
|
|
expect(kvPutSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores in KV when epoch time is beyond 12 hours", async () => {
|
|
const futureTime = Math.floor(Date.now() / 1000) + 24 * 3600; // 24 hours from now
|
|
|
|
await queueTask(
|
|
"NEW_EPISODE",
|
|
{ aniListId: 777, episodeNumber: 10 },
|
|
{
|
|
scheduleConfig: { epochTime: futureTime },
|
|
env: mockEnv,
|
|
},
|
|
);
|
|
|
|
expect(kvPutSpy).toHaveBeenCalledTimes(1);
|
|
expect(queueSendSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|