263 lines
8.2 KiB
TypeScript
263 lines
8.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { apiPathPattern, mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
const cronTasks = [
|
|
{
|
|
id: "economic.transfer_queue",
|
|
name: "Process e-conomic transfer queue",
|
|
description: "Processes pending e-conomic transfer queue jobs.",
|
|
module: "economic",
|
|
schedule: { type: "interval", seconds: 30 },
|
|
default_schedule: { type: "interval", seconds: 30 },
|
|
enabled: true,
|
|
default_enabled: true,
|
|
estimated_duration_ms: 2400,
|
|
next_run_at: "2026-07-09 12:01:00",
|
|
last_status: "succeeded",
|
|
due: true,
|
|
},
|
|
{
|
|
id: "system.sync_logs",
|
|
name: "Sync logs",
|
|
description: "Flushes application logs.",
|
|
module: "system",
|
|
schedule: { type: "interval", seconds: 300 },
|
|
default_schedule: { type: "interval", seconds: 300 },
|
|
enabled: false,
|
|
default_enabled: true,
|
|
estimated_duration_ms: 900,
|
|
next_run_at: "2026-07-09 12:05:00",
|
|
last_status: "failed",
|
|
last_error: "Redis unavailable",
|
|
due: false,
|
|
},
|
|
];
|
|
|
|
function cronListPayload(tasks = cronTasks) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
tasks,
|
|
summary: {
|
|
total: tasks.length,
|
|
enabled: tasks.filter((task) => task.enabled).length,
|
|
due: tasks.filter((task) => task.enabled && task.due).length,
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
};
|
|
}
|
|
|
|
test.describe("Superuser cron operations", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: [
|
|
"superuser",
|
|
"superuser_cron_view",
|
|
"SUPERUSER_RUN_CRON",
|
|
"superuser_cron_manage",
|
|
"superuser_coolify_manage",
|
|
],
|
|
});
|
|
await primeMockSession(page, { token: "superuser-cron-token", bootPath: null });
|
|
});
|
|
|
|
test("superusers can inspect, run, toggle, and reschedule cron tasks", async ({ page }) => {
|
|
const patchPayloads: Array<Record<string, unknown>> = [];
|
|
const runPayloads: Array<Record<string, unknown>> = [];
|
|
const deployPayloads: Array<Record<string, unknown>> = [];
|
|
let currentTasks = cronTasks.map((task) => ({ ...task, schedule: { ...task.schedule } }));
|
|
|
|
await page.route(apiPathPattern("/superuser/cron"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload(currentTasks)),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/runs"), async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
runs: [
|
|
{
|
|
id: 41,
|
|
task_id: "economic.transfer_queue",
|
|
source: "automatic",
|
|
status: "succeeded",
|
|
started_at: "2026-07-09 12:00:00",
|
|
duration_ms: 2100,
|
|
},
|
|
],
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
workers: [
|
|
{
|
|
worker_id: "release-stable-cron-worker",
|
|
name: "release-stable-cron-worker",
|
|
hostname: "coolify-cron-1",
|
|
source: "coolify_worker",
|
|
status: "running",
|
|
stale: false,
|
|
last_heartbeat_at: "2026-07-09 12:01:30",
|
|
last_run_count: 1,
|
|
commit_sha: "abcdef1234567890",
|
|
},
|
|
],
|
|
summary: {
|
|
total: 1,
|
|
running: 1,
|
|
stale: 0,
|
|
},
|
|
deployment: {
|
|
ok: true,
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers/deploy"), async (route) => {
|
|
deployPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
ok: true,
|
|
target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/run"), async (route) => {
|
|
runPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
id: 42,
|
|
task_id: "economic.transfer_queue",
|
|
source: "manual",
|
|
status: "succeeded",
|
|
started_at: "2026-07-09 12:02:00",
|
|
duration_ms: 1800,
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/config"), async (route) => {
|
|
const payload = route.request().postDataJSON() as Record<string, unknown>;
|
|
patchPayloads.push(payload);
|
|
currentTasks = currentTasks.map((task) => {
|
|
if (task.id !== payload.task_id) {
|
|
return task;
|
|
}
|
|
return {
|
|
...task,
|
|
...(typeof payload.enabled === "boolean" ? { enabled: payload.enabled } : {}),
|
|
...(payload.schedule ? { schedule: payload.schedule as { type: string; seconds: number } } : {}),
|
|
};
|
|
});
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload(currentTasks)),
|
|
});
|
|
});
|
|
|
|
await page.goto("/superuser/system/cron", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("superuser-cron-page")).toBeVisible();
|
|
await expect(page.getByTestId("cron-task-row-economic.transfer_queue")).toContainText(
|
|
"Process e-conomic transfer queue"
|
|
);
|
|
await expect(page.getByTestId("cron-total-tasks")).toContainText("2");
|
|
await expect(page.getByTestId("cron-worker-panel")).toContainText("release-stable-cron-worker");
|
|
await expect(page.getByTestId("cron-worker-running")).toContainText("1");
|
|
await expect(page.getByTestId("cron-worker-target")).toContainText("cron-worker-uuid");
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/superuser/cron/workers/deploy") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-worker-deploy").click(),
|
|
]);
|
|
expect(deployPayloads).toEqual([{}]);
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) => response.url().includes("/superuser/cron/run") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-task-run-economic.transfer_queue").click(),
|
|
]);
|
|
|
|
expect(runPayloads).toEqual([{ task_id: "economic.transfer_queue", force: false }]);
|
|
await expect(page.getByTestId("cron-run-history")).toContainText("manual");
|
|
|
|
await page.getByTestId("cron-task-toggle-system.sync_logs").click();
|
|
expect(patchPayloads.at(-1)).toMatchObject({ task_id: "system.sync_logs", enabled: true });
|
|
|
|
await page.getByTestId("cron-task-interval-economic.transfer_queue").fill("120");
|
|
await page.getByTestId("cron-task-save-economic.transfer_queue").click();
|
|
expect(patchPayloads.at(-1)).toMatchObject({
|
|
task_id: "economic.transfer_queue",
|
|
schedule: { type: "interval", seconds: 120 },
|
|
});
|
|
});
|
|
});
|