refactor!: migrate away from bun
- migrate package management to pnpm - migrate test suite to vitest - also remove Anify integration
This commit is contained in:
@@ -272,7 +272,7 @@ export class AnilistDurableObject extends DurableObject {
|
||||
}
|
||||
|
||||
async fetchFromAnilist<Result = any, Variables = any>(
|
||||
query: TypedDocumentNode<Result, Variables>,
|
||||
queryString: string,
|
||||
variables: Variables,
|
||||
token?: string | undefined,
|
||||
): Promise<Result> {
|
||||
@@ -286,7 +286,7 @@ export class AnilistDurableObject extends DurableObject {
|
||||
|
||||
// Use the query passed in, or fallback if needed (though we expect it to be passed)
|
||||
// We print the query to string
|
||||
const queryString = print(query);
|
||||
// const queryString = print(query);
|
||||
|
||||
const response = await fetch(`${this.env.PROXY_URL}/proxy`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function fetchTitleFromAnilist(
|
||||
token?: string | undefined,
|
||||
): Promise<Title | undefined> {
|
||||
if (useMockData()) {
|
||||
const { mockTitleDetails } = await import("~/mocks");
|
||||
const { mockTitleDetails } = await import("~/mocks/mockData");
|
||||
return mockTitleDetails();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Case, changeStringCase } from "./changeStringCase";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { fetchFromMultipleSources } from "./fetchFromMultipleSources";
|
||||
|
||||
describe("fetchFromMultipleSources", () => {
|
||||
it("no promises, throws exception", () => {
|
||||
expect(() => fetchFromMultipleSources([])).toThrow(
|
||||
it("no promises, throws exception", async () => {
|
||||
await expect(fetchFromMultipleSources([])).rejects.toThrow(
|
||||
"fetchPromises cannot be empty",
|
||||
);
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("fetchFromMultipleSources", () => {
|
||||
() => Promise.resolve(3),
|
||||
]);
|
||||
|
||||
expect(errorOccurred).toBeFalse();
|
||||
expect(errorOccurred).toBe(false);
|
||||
});
|
||||
|
||||
it("has promises that all throw, returns null", async () => {
|
||||
@@ -48,7 +48,7 @@ describe("fetchFromMultipleSources", () => {
|
||||
() => Promise.reject(new Error("error")),
|
||||
]);
|
||||
|
||||
expect(errorOccurred).toBeTrue();
|
||||
expect(errorOccurred).toBe(true);
|
||||
});
|
||||
|
||||
it("has promises but cache has value, returns cached value", async () => {
|
||||
@@ -80,7 +80,7 @@ describe("fetchFromMultipleSources", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(errorOccurred).toBeFalse();
|
||||
expect(errorOccurred).toBe(false);
|
||||
});
|
||||
|
||||
it("has promises, no cached value, no valid response, should not save in cache", async () => {
|
||||
|
||||
@@ -2,13 +2,12 @@ import { env as cloudflareEnv } from "cloudflare:workers";
|
||||
import mapKeys from "lodash.mapkeys";
|
||||
|
||||
import { Case, changeStringCase } from "../changeStringCase";
|
||||
import { readEnvVariable } from "../readEnvVariable";
|
||||
|
||||
export function getAdminSdkCredentials(env: Cloudflare.Env = cloudflareEnv) {
|
||||
return mapKeys(
|
||||
readEnvVariable<AdminSdkCredentials>("ADMIN_SDK_JSON", env),
|
||||
JSON.parse(env.ADMIN_SDK_JSON) as AdminSdkCredentials,
|
||||
(_, key) => changeStringCase(key, Case.snake_case, Case.camelCase),
|
||||
) as unknown as AdminSdkCredentials;
|
||||
);
|
||||
}
|
||||
|
||||
export interface AdminSdkCredentials {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { server } from "~/mocks";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { AdminSdkCredentials } from "./getAdminSdkCredentials";
|
||||
import { verifyFcmToken } from "./verifyFcmToken";
|
||||
|
||||
server.listen();
|
||||
|
||||
const FAKE_ADMIN_SDK_JSON: AdminSdkCredentials = {
|
||||
type: "service_account",
|
||||
@@ -23,29 +18,87 @@ const FAKE_ADMIN_SDK_JSON: AdminSdkCredentials = {
|
||||
};
|
||||
|
||||
describe("verifyFcmToken", () => {
|
||||
// it("valid token, returns true", async () => {
|
||||
// const token =
|
||||
// "7v8sy43aq0re4r8xe7rmr0cn1fsmh6phehnfla2pa73z899zmhyarivmkt4sj6pyv0py43u6p2sim6wz2vg9ypjp9rug1keoth7f6ll3gdvas4q020u3ah51r6bjgn51j6bd92ztmtof3ljpcm8q31njvndy65enm68";
|
||||
// const res = await verifyFcmToken(token, FAKE_ADMIN_SDK_JSON);
|
||||
const fcmToken = "test-token";
|
||||
let verifyFcmToken: typeof import("~/libs/gcloud/verifyFcmToken").verifyFcmToken;
|
||||
let sendFcmMessage: any;
|
||||
|
||||
// expect(res).toBeTrue();
|
||||
// });
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("~/libs/gcloud/getGoogleAuthToken", () => ({
|
||||
getGoogleAuthToken: vi.fn().mockResolvedValue("fake-token"),
|
||||
}));
|
||||
vi.doMock("~/libs/gcloud/sendFcmMessage", () => ({
|
||||
sendFcmMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
it("invalid token, returns false", async () => {
|
||||
const token = "abc123";
|
||||
const res = await verifyFcmToken(token, FAKE_ADMIN_SDK_JSON);
|
||||
// Import the module under test AFTER mocking dependencies
|
||||
const verifyModule = await import("~/libs/gcloud/verifyFcmToken");
|
||||
verifyFcmToken = verifyModule.verifyFcmToken;
|
||||
|
||||
expect(res).toBeFalse();
|
||||
const mockModule = await import("~/libs/gcloud/sendFcmMessage");
|
||||
sendFcmMessage = mockModule.sendFcmMessage;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.doUnmock("~/libs/gcloud/sendFcmMessage");
|
||||
vi.doUnmock("~/libs/gcloud/getGoogleAuthToken");
|
||||
});
|
||||
|
||||
it("returns true for valid token", async () => {
|
||||
sendFcmMessage.mockResolvedValue({
|
||||
name: "projects/test-26g38/messages/fake-message-id",
|
||||
});
|
||||
|
||||
const result = await verifyFcmToken(fcmToken, FAKE_ADMIN_SDK_JSON);
|
||||
|
||||
expect(result).toBe(true);
|
||||
// Since we are mocking the module, we can check if it was called
|
||||
expect(sendFcmMessage).toHaveBeenCalledWith(
|
||||
FAKE_ADMIN_SDK_JSON,
|
||||
{ name: "token_verification", token: fcmToken },
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false for invalid token (400)", async () => {
|
||||
sendFcmMessage.mockResolvedValue({
|
||||
error: {
|
||||
code: 400,
|
||||
message: "The registration token is not a valid FCM registration token",
|
||||
status: "INVALID_ARGUMENT",
|
||||
details: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await verifyFcmToken("invalid-token", FAKE_ADMIN_SDK_JSON);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for not found token (404)", async () => {
|
||||
sendFcmMessage.mockResolvedValue({
|
||||
error: {
|
||||
code: 404,
|
||||
message: "Task not found",
|
||||
status: "NOT_FOUND",
|
||||
details: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await verifyFcmToken("not-found-token", FAKE_ADMIN_SDK_JSON);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("invalid ADMIN_SDK_JSON, returns false", async () => {
|
||||
const token =
|
||||
"7v8sy43aq0re4r8xe7rmr0cn1fsmh6phehnfla2pa73z899zmhyarivmkt4sj6pyv0py43u6p2sim6wz2vg9ypjp9rug1keoth7f6ll3gdvas4q020u3ah51r6bjgn51j6bd92ztmtof3ljpcm8q31njvndy65enm68";
|
||||
const res = await verifyFcmToken(token, {
|
||||
// Simulate error that would occur in sendFcmMessage (e.g. auth failure inside it)
|
||||
sendFcmMessage.mockRejectedValue(new Error("No email provided"));
|
||||
|
||||
const res = await verifyFcmToken("token", {
|
||||
...FAKE_ADMIN_SDK_JSON,
|
||||
clientEmail: "",
|
||||
});
|
||||
|
||||
expect(res).toBeFalse();
|
||||
expect(res).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getCurrentAndNextSeason } from "./getCurrentAndNextSeason";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { lazy } from "./lazy";
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("lazy", () => {
|
||||
return "value";
|
||||
});
|
||||
|
||||
expect(setValue).toBeFalse();
|
||||
expect(setValue).toBe(false);
|
||||
});
|
||||
|
||||
it("lazy function called if get is called", () => {
|
||||
@@ -26,7 +26,7 @@ describe("lazy", () => {
|
||||
return "value";
|
||||
}).get();
|
||||
|
||||
expect(setValue).toBeTrue();
|
||||
expect(setValue).toBe(true);
|
||||
});
|
||||
|
||||
it("lazy function called only once if get is called multiple times", () => {
|
||||
|
||||
116
src/libs/maybeScheduleNextAiringEpisode.spec.ts
Normal file
116
src/libs/maybeScheduleNextAiringEpisode.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { DateTime } from "luxon";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { maybeScheduleNextAiringEpisode } from "./maybeScheduleNextAiringEpisode";
|
||||
|
||||
vi.mock("~/models/unreleasedTitles", () => ({
|
||||
addUnreleasedTitle: vi.fn(),
|
||||
removeUnreleasedTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./anilist/getNextEpisodeAiringAt", () => ({
|
||||
getNextEpisodeTimeUntilAiring: vi.fn(),
|
||||
}));
|
||||
describe("maybeScheduleNextAiringEpisode", () => {
|
||||
let addUnreleasedTitle: any;
|
||||
let removeUnreleasedTitle: any;
|
||||
let getNextEpisodeTimeUntilAiring: any;
|
||||
let queueTask: any;
|
||||
let maybeScheduleNextAiringEpisode: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
|
||||
vi.doMock("~/models/unreleasedTitles", () => ({
|
||||
addUnreleasedTitle: vi.fn(),
|
||||
removeUnreleasedTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("./anilist/getNextEpisodeAiringAt", () => ({
|
||||
getNextEpisodeTimeUntilAiring: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.doMock("./tasks/queueTask", () => ({
|
||||
queueTask: vi.fn(),
|
||||
}));
|
||||
|
||||
maybeScheduleNextAiringEpisode = (
|
||||
await import("./maybeScheduleNextAiringEpisode")
|
||||
).maybeScheduleNextAiringEpisode;
|
||||
|
||||
addUnreleasedTitle = (await import("~/models/unreleasedTitles"))
|
||||
.addUnreleasedTitle;
|
||||
removeUnreleasedTitle = (await import("~/models/unreleasedTitles"))
|
||||
.removeUnreleasedTitle;
|
||||
getNextEpisodeTimeUntilAiring = (
|
||||
await import("./anilist/getNextEpisodeAiringAt")
|
||||
).getNextEpisodeTimeUntilAiring;
|
||||
queueTask = (await import("./tasks/queueTask")).queueTask;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should add to unreleased titles if status is NOT_YET_RELEASED and no next airing", async () => {
|
||||
vi.mocked(getNextEpisodeTimeUntilAiring).mockResolvedValue({
|
||||
nextAiring: null,
|
||||
status: "NOT_YET_RELEASED",
|
||||
});
|
||||
|
||||
await maybeScheduleNextAiringEpisode(1);
|
||||
|
||||
expect(addUnreleasedTitle).toHaveBeenCalledWith(1);
|
||||
expect(queueTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if status is RELEASING but no next airing (e.g. hiatus)", async () => {
|
||||
vi.mocked(getNextEpisodeTimeUntilAiring).mockResolvedValue({
|
||||
nextAiring: null,
|
||||
status: "RELEASING",
|
||||
});
|
||||
|
||||
await maybeScheduleNextAiringEpisode(2);
|
||||
|
||||
expect(addUnreleasedTitle).not.toHaveBeenCalled();
|
||||
expect(queueTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if next airing is more than 30 days away", async () => {
|
||||
const farFuture = DateTime.now().plus({ days: 31 }).toSeconds();
|
||||
vi.mocked(getNextEpisodeTimeUntilAiring).mockResolvedValue({
|
||||
nextAiring: { airingAt: farFuture, episode: 2 },
|
||||
status: "RELEASING",
|
||||
});
|
||||
|
||||
await maybeScheduleNextAiringEpisode(3);
|
||||
|
||||
expect(addUnreleasedTitle).not.toHaveBeenCalled();
|
||||
expect(queueTask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should schedule task and remove from unreleased if next airing is soon", async () => {
|
||||
const nearFuture = Math.floor(DateTime.now().plus({ days: 1 }).toSeconds());
|
||||
vi.mocked(getNextEpisodeTimeUntilAiring).mockResolvedValue({
|
||||
nextAiring: { airingAt: nearFuture, episode: 5 },
|
||||
status: "RELEASING",
|
||||
});
|
||||
|
||||
await maybeScheduleNextAiringEpisode(4);
|
||||
|
||||
expect(queueTask).toHaveBeenCalledWith(
|
||||
"NEW_EPISODE",
|
||||
{ aniListId: 4, episodeNumber: 5 },
|
||||
{ scheduleConfig: { epochTime: nearFuture } },
|
||||
);
|
||||
expect(removeUnreleasedTitle).toHaveBeenCalledWith(4);
|
||||
expect(addUnreleasedTitle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should add to unreleased if next airing is null even with RELEASING status? No code says only NOT_YET_RELEASED", async () => {
|
||||
// Code: if (status === "NOT_YET_RELEASED") await addUnreleasedTitle(aniListId);
|
||||
// So if RELEASING and null, it does nothing.
|
||||
// Verified in second test case.
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { PromiseTimedOutError, promiseTimeout } from "./promiseTimeout";
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
|
||||
import { readEnvVariable } from "./readEnvVariable";
|
||||
|
||||
describe("readEnvVariable", () => {
|
||||
describe("env & variable defined", () => {
|
||||
it("returns boolean", () => {
|
||||
expect(
|
||||
readEnvVariable<boolean>("ENABLE_ANIFY", { ENABLE_ANIFY: "false" }),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns string", () => {
|
||||
expect(
|
||||
readEnvVariable<string>("QSTASH_TOKEN", {
|
||||
QSTASH_TOKEN: "ehf73g8gyriuvnieojwicbg83hc",
|
||||
}),
|
||||
).toBe("ehf73g8gyriuvnieojwicbg83hc");
|
||||
});
|
||||
|
||||
it("returns number", () => {
|
||||
expect(
|
||||
readEnvVariable<number>("NUM_RETRIES", { NUM_RETRIES: "123" }),
|
||||
).toBe(123);
|
||||
});
|
||||
});
|
||||
|
||||
it("env defined but variable not defined, returns default value", () => {
|
||||
expect(readEnvVariable<boolean>("ENABLE_ANIFY", { FOO: "bar" })).toBe(true);
|
||||
});
|
||||
|
||||
it("env not defined, returns default value", () => {
|
||||
expect(readEnvVariable<boolean>("ENABLE_ANIFY", undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { env as cloudflareEnv } from "cloudflare:workers";
|
||||
import type { Bindings } from "hono/types";
|
||||
|
||||
type EnvVariable = keyof Cloudflare.Env;
|
||||
const defaultValues: Record<EnvVariable, any> = {
|
||||
ENABLE_ANIFY: true,
|
||||
};
|
||||
|
||||
export function readEnvVariable<T>(
|
||||
envVariable: EnvVariable,
|
||||
env: Bindings | undefined = cloudflareEnv,
|
||||
): T {
|
||||
try {
|
||||
return JSON.parse(env?.[envVariable] ?? null) ?? defaultValues[envVariable];
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return env![envVariable];
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { sortByProperty } from "./sortByProperty";
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { DelayedTaskMetadata } from "./delayedTask";
|
||||
import {
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { processDelayedTasks } from "./processDelayedTasks";
|
||||
|
||||
describe("processDelayedTasks", () => {
|
||||
let mockEnv: Cloudflare.Env;
|
||||
let mockCtx: ExecutionContext;
|
||||
let kvGetSpy: ReturnType<typeof mock>;
|
||||
let kvDeleteSpy: ReturnType<typeof mock>;
|
||||
let kvPutSpy: ReturnType<typeof mock>;
|
||||
let queueSendSpy: ReturnType<typeof mock>;
|
||||
let kvGetSpy: ReturnType<typeof vi.fn>;
|
||||
let kvDeleteSpy: ReturnType<typeof vi.fn>;
|
||||
let kvPutSpy: ReturnType<typeof vi.fn>;
|
||||
let queueSendSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
kvGetSpy = mock(() => Promise.resolve(null));
|
||||
kvDeleteSpy = mock(() => Promise.resolve());
|
||||
kvPutSpy = mock(() => Promise.resolve());
|
||||
queueSendSpy = mock(() => Promise.resolve());
|
||||
kvGetSpy = vi.fn(() => Promise.resolve(null));
|
||||
kvDeleteSpy = vi.fn(() => Promise.resolve());
|
||||
kvPutSpy = vi.fn(() => Promise.resolve());
|
||||
queueSendSpy = vi.fn(() => Promise.resolve());
|
||||
|
||||
mockEnv = {
|
||||
DELAYED_TASKS: {
|
||||
get: kvGetSpy,
|
||||
delete: kvDeleteSpy,
|
||||
put: kvPutSpy,
|
||||
list: mock(() => Promise.resolve({ keys: [], list_complete: true })),
|
||||
getWithMetadata: mock(() =>
|
||||
list: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [],
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
),
|
||||
getWithMetadata: vi.fn(() =>
|
||||
Promise.resolve({ value: null, metadata: null }),
|
||||
),
|
||||
} as any,
|
||||
@@ -30,13 +36,13 @@ describe("processDelayedTasks", () => {
|
||||
send: queueSendSpy,
|
||||
} as any,
|
||||
ANILIST_UPDATES: {
|
||||
send: mock(() => Promise.resolve()),
|
||||
send: vi.fn(() => Promise.resolve()),
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
mockCtx = {
|
||||
waitUntil: mock(() => {}),
|
||||
passThroughOnException: mock(() => {}),
|
||||
waitUntil: vi.fn(() => {}),
|
||||
passThroughOnException: vi.fn(() => {}),
|
||||
} as any;
|
||||
});
|
||||
|
||||
@@ -61,10 +67,11 @@ describe("processDelayedTasks", () => {
|
||||
retryCount: 0,
|
||||
};
|
||||
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [{ name: `delayed-task:${scheduledTime}:task-1` }],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -93,10 +100,11 @@ describe("processDelayedTasks", () => {
|
||||
retryCount: 0,
|
||||
};
|
||||
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [{ name: `delayed-task:${scheduledTime}:task-2` }],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -122,10 +130,11 @@ describe("processDelayedTasks", () => {
|
||||
retryCount: 0,
|
||||
};
|
||||
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [{ name: `delayed-task:${scheduledTime}:task-3` }],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -141,7 +150,7 @@ describe("processDelayedTasks", () => {
|
||||
});
|
||||
|
||||
it("logs alert after 3 failed attempts", async () => {
|
||||
const consoleErrorSpy = mock(() => {});
|
||||
const consoleErrorSpy = vi.fn(() => {});
|
||||
const originalConsoleError = console.error;
|
||||
console.error = consoleErrorSpy as any;
|
||||
|
||||
@@ -158,10 +167,11 @@ describe("processDelayedTasks", () => {
|
||||
retryCount: 2, // Will become 3 after this failure
|
||||
};
|
||||
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [{ name: `delayed-task:${scheduledTime}:task-4` }],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -202,13 +212,14 @@ describe("processDelayedTasks", () => {
|
||||
retryCount: 0,
|
||||
};
|
||||
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [
|
||||
{ name: `delayed-task:${task1Metadata.scheduledEpochTime}:task-1` },
|
||||
{ name: `delayed-task:${task2Metadata.scheduledEpochTime}:task-2` },
|
||||
],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -223,10 +234,11 @@ describe("processDelayedTasks", () => {
|
||||
});
|
||||
|
||||
it("skips tasks with null values in KV", async () => {
|
||||
mockEnv.DELAYED_TASKS.list = mock(() =>
|
||||
mockEnv.DELAYED_TASKS.list = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
keys: [{ name: "delayed-task:123:invalid" }],
|
||||
list_complete: true,
|
||||
list_complete: true as const,
|
||||
cacheStatus: null,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { queueTask } from "./queueTask";
|
||||
|
||||
@@ -6,20 +6,20 @@ describe("queueTask - delayed task handling", () => {
|
||||
const MAX_DELAY_SECONDS = 12 * 60 * 60; // 43,200 seconds
|
||||
|
||||
let mockEnv: Cloudflare.Env;
|
||||
let kvPutSpy: ReturnType<typeof mock>;
|
||||
let queueSendSpy: ReturnType<typeof mock>;
|
||||
let kvPutSpy: ReturnType<typeof vi.fn>;
|
||||
let queueSendSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
kvPutSpy = mock(() => Promise.resolve());
|
||||
queueSendSpy = mock(() => Promise.resolve());
|
||||
kvPutSpy = vi.fn(() => Promise.resolve());
|
||||
queueSendSpy = vi.fn(() => Promise.resolve());
|
||||
|
||||
mockEnv = {
|
||||
DELAYED_TASKS: {
|
||||
put: kvPutSpy,
|
||||
get: mock(() => Promise.resolve(null)),
|
||||
delete: mock(() => Promise.resolve()),
|
||||
list: mock(() => Promise.resolve({ keys: [], list_complete: true })),
|
||||
getWithMetadata: mock(() =>
|
||||
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,
|
||||
@@ -27,12 +27,12 @@ describe("queueTask - delayed task handling", () => {
|
||||
send: queueSendSpy,
|
||||
} as any,
|
||||
ANILIST_UPDATES: {
|
||||
send: mock(() => Promise.resolve()),
|
||||
send: vi.fn(() => Promise.resolve()),
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
// Mock crypto.randomUUID
|
||||
globalThis.crypto.randomUUID = mock(() => "test-uuid-123");
|
||||
(globalThis as any).crypto = { randomUUID: vi.fn(() => "test-uuid-123") };
|
||||
});
|
||||
|
||||
describe("tasks with delay <= 12 hours", () => {
|
||||
|
||||
47
src/libs/tasks/removeTask.spec.ts
Normal file
47
src/libs/tasks/removeTask.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
|
||||
describe("removeTask", () => {
|
||||
let removeTask: any;
|
||||
let getAdminSdkCredentials: any;
|
||||
let getGoogleAuthToken: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doMock("cloudflare:workers", () => ({ env: {} }));
|
||||
vi.doMock("../gcloud/getAdminSdkCredentials", () => ({
|
||||
getAdminSdkCredentials: vi.fn(),
|
||||
}));
|
||||
vi.doMock("../gcloud/getGoogleAuthToken", () => ({
|
||||
getGoogleAuthToken: vi.fn(),
|
||||
}));
|
||||
|
||||
removeTask = (await import("./removeTask")).removeTask;
|
||||
getAdminSdkCredentials = (await import("../gcloud/getAdminSdkCredentials"))
|
||||
.getAdminSdkCredentials;
|
||||
getGoogleAuthToken = (await import("../gcloud/getGoogleAuthToken"))
|
||||
.getGoogleAuthToken;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should call Google Cloud Tasks API with correct parameters", async () => {
|
||||
const mockCredentials = { projectId: "test-project" };
|
||||
vi.mocked(getAdminSdkCredentials).mockReturnValue(mockCredentials);
|
||||
vi.mocked(getGoogleAuthToken).mockResolvedValue("test-token");
|
||||
vi.mocked(fetch).mockResolvedValue(new Response(""));
|
||||
|
||||
await removeTask("NEW_EPISODE", "task-123");
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://content-cloudtasks.googleapis.com/v2/projects/test-project/locations/northamerica-northeast1/queues/NEW_EPISODE/tasks/task-123",
|
||||
expect.objectContaining({
|
||||
method: "DELETE",
|
||||
headers: { Authorization: "Bearer test-token" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,6 @@ import { getDb } from "~/models/db";
|
||||
|
||||
import { getTestEnv } from "./getTestEnv";
|
||||
|
||||
export function getTestDb() {
|
||||
return getDb(getTestEnv());
|
||||
export function getTestDb(env?: Cloudflare.Env) {
|
||||
return getDb(env ?? getTestEnv());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { env } from "cloudflare:test";
|
||||
|
||||
/** Should only be used when it doesn't make sense for 'Bindings' or 'Variables' to be set. Otherwise, use getTestEnv(). */
|
||||
export function getTestEnvVariables(): Cloudflare.Env {
|
||||
return getTestEnv();
|
||||
@@ -5,14 +7,11 @@ export function getTestEnvVariables(): Cloudflare.Env {
|
||||
|
||||
export function getTestEnv({
|
||||
ADMIN_SDK_JSON = '{"client_email": "test@test.com", "project_id": "test-26g38"}',
|
||||
ENABLE_ANIFY = "true",
|
||||
TURSO_AUTH_TOKEN = "123",
|
||||
TURSO_URL = "http://127.0.0.1:3001",
|
||||
LOG_DB_QUERIES = "false",
|
||||
}: Partial<Cloudflare.Env> = {}): Cloudflare.Env {
|
||||
return {
|
||||
...env,
|
||||
ADMIN_SDK_JSON,
|
||||
ENABLE_ANIFY,
|
||||
TURSO_AUTH_TOKEN,
|
||||
TURSO_URL,
|
||||
LOG_DB_QUERIES,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { tables } from "~/models/schema";
|
||||
|
||||
import { getTestDb } from "./getTestDb";
|
||||
|
||||
export async function resetTestDb() {
|
||||
const db = getTestDb();
|
||||
|
||||
export async function resetTestDb(db = getTestDb()) {
|
||||
for (const table of tables) {
|
||||
await db.delete(table);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user