diff --git a/server/routes/api/cron.test.ts b/server/routes/api/cron.test.ts deleted file mode 100644 index a49411933..000000000 --- a/server/routes/api/cron.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getTestServer } from "@server/test/support"; - -const server = getTestServer(); - -describe("#cron.daily", () => { - it("should require authentication", async () => { - const res = await server.post("/api/cron.daily"); - expect(res.status).toEqual(401); - }); -}); diff --git a/server/routes/api/cron/cron.test.ts b/server/routes/api/cron/cron.test.ts new file mode 100644 index 000000000..c17d92a63 --- /dev/null +++ b/server/routes/api/cron/cron.test.ts @@ -0,0 +1,111 @@ +import { getTestServer } from "@server/test/support"; + +const server = getTestServer(); + +describe("POST /api/cron.daily", () => { + it("should require token", async () => { + const res = await server.post("/api/cron.daily"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("token is required"); + }); + + it("should validate token", async () => { + const res = await server.post("/api/cron.daily", { + body: { + token: "token", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(401); + expect(body.message).toBe("Invalid secret token"); + }); + + it("should validate limit", async () => { + const res = await server.post("/api/cron.daily", { + body: { + limit: -1, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("limit: Number must be greater than 0"); + }); +}); + +describe("GET /api/cron.daily", () => { + it("should require token", async () => { + const res = await server.get("/api/cron.daily"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("token is required"); + }); + + it("should validate token", async () => { + const res = await server.get("/api/cron.daily?token=token"); + const body = await res.json(); + expect(res.status).toEqual(401); + expect(body.message).toBe("Invalid secret token"); + }); + + it("should validate limit", async () => { + const res = await server.get("/api/cron.daily?limit=-1"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("limit: Number must be greater than 0"); + }); +}); + +describe("POST /api/utils.gc", () => { + it("should require token", async () => { + const res = await server.post("/api/utils.gc"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("token is required"); + }); + + it("should validate token", async () => { + const res = await server.post("/api/utils.gc", { + body: { + token: "token", + }, + }); + const body = await res.json(); + expect(res.status).toEqual(401); + expect(body.message).toBe("Invalid secret token"); + }); + + it("should validate limit", async () => { + const res = await server.post("/api/utils.gc", { + body: { + limit: -1, + }, + }); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("limit: Number must be greater than 0"); + }); +}); + +describe("GET /api/utils.gc", () => { + it("should require token", async () => { + const res = await server.get("/api/utils.gc"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("token is required"); + }); + + it("should validate token", async () => { + const res = await server.get("/api/utils.gc?token=token"); + const body = await res.json(); + expect(res.status).toEqual(401); + expect(body.message).toBe("Invalid secret token"); + }); + + it("should validate limit", async () => { + const res = await server.get("/api/utils.gc?limit=-1"); + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toBe("limit: Number must be greater than 0"); + }); +}); diff --git a/server/routes/api/cron.ts b/server/routes/api/cron/cron.ts similarity index 54% rename from server/routes/api/cron.ts rename to server/routes/api/cron/cron.ts index 7a46b9bce..ba0051591 100644 --- a/server/routes/api/cron.ts +++ b/server/routes/api/cron/cron.ts @@ -1,21 +1,17 @@ import crypto from "crypto"; -import { Context } from "koa"; import Router from "koa-router"; import env from "@server/env"; import { AuthenticationError } from "@server/errors"; +import validate from "@server/middlewares/validate"; import tasks from "@server/queues/tasks"; +import { APIContext } from "@server/types"; +import * as T from "./schema"; const router = new Router(); -const cronHandler = async (ctx: Context) => { - const token = - ctx.method === "POST" ? ctx.request.body?.token : ctx.query.token; - const limit = - (ctx.method === "POST" ? ctx.request.body?.limit : ctx.query.limit) ?? 500; - - if (!token || typeof token !== "string") { - throw AuthenticationError("Token is required"); - } +const cronHandler = async (ctx: APIContext) => { + const token = (ctx.input.body.token ?? ctx.input.query.token) as string; + const limit = ctx.input.body.limit ?? ctx.input.query.limit; if ( token.length !== env.UTILS_SECRET.length || @@ -39,11 +35,11 @@ const cronHandler = async (ctx: Context) => { }; }; -router.get("cron.:period", cronHandler); -router.post("cron.:period", cronHandler); +router.get("cron.:period", validate(T.CronSchema), cronHandler); +router.post("cron.:period", validate(T.CronSchema), cronHandler); // For backwards compatibility -router.get("utils.gc", cronHandler); -router.post("utils.gc", cronHandler); +router.get("utils.gc", validate(T.CronSchema), cronHandler); +router.post("utils.gc", validate(T.CronSchema), cronHandler); export default router; diff --git a/server/routes/api/cron/index.ts b/server/routes/api/cron/index.ts new file mode 100644 index 000000000..fb17a6155 --- /dev/null +++ b/server/routes/api/cron/index.ts @@ -0,0 +1 @@ +export { default } from "./cron"; diff --git a/server/routes/api/cron/schema.ts b/server/routes/api/cron/schema.ts new file mode 100644 index 000000000..7f6125385 --- /dev/null +++ b/server/routes/api/cron/schema.ts @@ -0,0 +1,18 @@ +import { isEmpty } from "lodash"; +import { z } from "zod"; +import BaseSchema from "../BaseSchema"; + +export const CronSchema = BaseSchema.extend({ + body: z.object({ + token: z.string().optional(), + limit: z.coerce.number().gt(0).default(500), + }), + query: z.object({ + token: z.string().optional(), + limit: z.coerce.number().gt(0).default(500), + }), +}).refine((req) => !(isEmpty(req.body.token) && isEmpty(req.query.token)), { + message: "token is required", +}); + +export type CronSchemaReq = z.infer;