From 7020d84869cb025f765131233a382e269fe8442b Mon Sep 17 00:00:00 2001 From: fliptrigga13 <76012365+fliptrigga13@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:52:33 -0400 Subject: [PATCH] fix(schedule): await cloud schedule registration and validate jobs service response (#5139) --- .../cloud-schedule-registration.test.ts | 180 ++++++++++++++++++ apps/dokploy/server/api/routers/schedule.ts | 2 +- apps/dokploy/server/utils/backup.ts | 20 +- 3 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 apps/dokploy/__test__/schedule/cloud-schedule-registration.test.ts diff --git a/apps/dokploy/__test__/schedule/cloud-schedule-registration.test.ts b/apps/dokploy/__test__/schedule/cloud-schedule-registration.test.ts new file mode 100644 index 00000000000..bf697bdd7b2 --- /dev/null +++ b/apps/dokploy/__test__/schedule/cloud-schedule-registration.test.ts @@ -0,0 +1,180 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@dokploy/server", () => ({ + IS_CLOUD: true, + removeScheduleBackup: vi.fn(), +})); + +import { + cancelJobs, + removeJob, + schedule, + updateJob, +} from "../../server/utils/backup"; + +describe("Cloud Schedule Job Registration & Error Propagation (#5139)", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { + ...originalEnv, + JOBS_URL: "https://jobs.example.com", + API_KEY: "test-secret-key", + }; + }); + + afterEach(() => { + process.env = originalEnv; + vi.restoreAllMocks(); + }); + + it("should send correct payload and return response on successful schedule creation", async () => { + const mockResponseData = { success: true, jobId: "job-123" }; + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockResponseData, + } as unknown as Response); + + const jobPayload = { + scheduleId: "sched-123", + type: "schedule" as const, + cronSchedule: "*/5 * * * *", + timezone: "UTC", + }; + + const result = await schedule(jobPayload); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + "https://jobs.example.com/create-backup", + { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-Key": "test-secret-key", + }, + body: JSON.stringify(jobPayload), + }, + ); + expect(result).toEqual(mockResponseData); + }); + + it("should throw a descriptive error when cloud schedule creation returns non-2xx status", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: "Internal Server Error", + text: async () => "Scheduler worker unavailable", + } as unknown as Response); + + const jobPayload = { + scheduleId: "sched-500", + type: "schedule" as const, + cronSchedule: "* * * * *", + timezone: "UTC", + }; + + await expect(schedule(jobPayload)).rejects.toThrow( + "Failed to create schedule job: Internal Server Error Scheduler worker unavailable", + ); + }); + + it("should throw a descriptive error when updateJob returns non-2xx status", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 400, + statusText: "Bad Request", + text: async () => "Invalid cron expression", + } as unknown as Response); + + const jobPayload = { + scheduleId: "sched-400", + type: "schedule" as const, + cronSchedule: "invalid-cron", + timezone: "UTC", + }; + + await expect(updateJob(jobPayload)).rejects.toThrow( + "Failed to update schedule job: Bad Request Invalid cron expression", + ); + }); + + it("should throw a descriptive error when removeJob returns non-2xx status", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + text: async () => "Job not registered", + } as unknown as Response); + + const jobPayload = { + scheduleId: "sched-404", + type: "schedule" as const, + cronSchedule: "0 0 * * *", + timezone: "UTC", + }; + + await expect(removeJob(jobPayload)).rejects.toThrow( + "Failed to remove schedule job: Not Found Job not registered", + ); + }); + + it("should propagate network failure when fetch rejects", async () => { + global.fetch = vi + .fn() + .mockRejectedValue(new Error("Network connection refused")); + + const jobPayload = { + scheduleId: "sched-net-err", + type: "schedule" as const, + cronSchedule: "*/10 * * * *", + timezone: "UTC", + }; + + await expect(schedule(jobPayload)).rejects.toThrow( + "Network connection refused", + ); + }); + + describe("cancelJobs", () => { + it("should remove all enabled backup jobs when in cloud mode", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: true }), + } as unknown as Response); + + const backups = [ + { backupId: "b1", schedule: "0 0 * * *", enabled: true }, + { backupId: "b2", schedule: "0 12 * * *", enabled: false }, + { backupId: "b3", schedule: "0 6 * * *", enabled: true }, + ]; + + await cancelJobs(backups as any); + + // b1 and b3 are enabled, b2 is disabled + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(global.fetch).toHaveBeenCalledWith( + "https://jobs.example.com/remove-job", + expect.objectContaining({ + body: JSON.stringify({ + cronSchedule: "0 0 * * *", + backupId: "b1", + type: "backup", + }), + }), + ); + expect(global.fetch).toHaveBeenCalledWith( + "https://jobs.example.com/remove-job", + expect.objectContaining({ + body: JSON.stringify({ + cronSchedule: "0 6 * * *", + backupId: "b3", + type: "backup", + }), + }), + ); + }); + }); +}); diff --git a/apps/dokploy/server/api/routers/schedule.ts b/apps/dokploy/server/api/routers/schedule.ts index a7417a92c97..aae503e5013 100644 --- a/apps/dokploy/server/api/routers/schedule.ts +++ b/apps/dokploy/server/api/routers/schedule.ts @@ -66,7 +66,7 @@ export const scheduleRouter = createTRPCRouter({ if (newSchedule?.enabled) { if (IS_CLOUD) { - schedule({ + await schedule({ scheduleId: newSchedule.scheduleId, type: "schedule", cronSchedule: newSchedule.cronExpression, diff --git a/apps/dokploy/server/utils/backup.ts b/apps/dokploy/server/utils/backup.ts index 588f6636b82..470a884e91c 100644 --- a/apps/dokploy/server/utils/backup.ts +++ b/apps/dokploy/server/utils/backup.ts @@ -2,7 +2,7 @@ import { type BackupScheduleList, IS_CLOUD, removeScheduleBackup, -} from "@dokploy/server/index"; +} from "@dokploy/server"; type QueueJob = | { @@ -36,6 +36,12 @@ export const schedule = async (job: QueueJob) => { }, body: JSON.stringify(job), }); + if (!result.ok) { + const errorText = await result.text().catch(() => ""); + throw new Error( + `Failed to create schedule job: ${result.statusText} ${errorText}`.trim(), + ); + } const data = await result.json(); return data; } catch (error) { @@ -53,6 +59,12 @@ export const removeJob = async (job: QueueJob) => { }, body: JSON.stringify(job), }); + if (!result.ok) { + const errorText = await result.text().catch(() => ""); + throw new Error( + `Failed to remove schedule job: ${result.statusText} ${errorText}`.trim(), + ); + } const data = await result.json(); return data; } catch (error) { @@ -70,6 +82,12 @@ export const updateJob = async (job: QueueJob) => { }, body: JSON.stringify(job), }); + if (!result.ok) { + const errorText = await result.text().catch(() => ""); + throw new Error( + `Failed to update schedule job: ${result.statusText} ${errorText}`.trim(), + ); + } const data = await result.json(); return data; } catch (error) {