Files
dokploy-deploy-action/test/client.test.js
Max Vojtkov c23b44983b Deploy Dokploy applications from GitHub and Gitea Actions
A dependency-free node20 action that triggers application.deploy or
compose.deploy, then follows the deployment to a terminal status so a
failed build fails the CI run. Snapshots the deployment list before
triggering, so a concurrent deployment is never mistaken for this one.

Optionally points the application at a freshly built image first, which
is what lets a workflow lint, test, push and deploy the same artifact.
2026-08-09 03:37:35 +03:00

139 lines
4.9 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const { DokployClient, DokployError, normaliseHost, parseError } = require("../src/client");
function response(status, body) {
return {
ok: status < 400,
status,
text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
};
}
function clientWith(fetchImpl, options = {}) {
return new DokployClient({
host: "https://dokploy.example.com",
token: "secret",
fetch: fetchImpl,
sleep: async () => {},
...options,
});
}
test("normaliseHost tolerates trailing slashes and a trailing /api", () => {
const expected = "https://dokploy.example.com/api";
assert.equal(normaliseHost("https://dokploy.example.com"), expected);
assert.equal(normaliseHost("https://dokploy.example.com/"), expected);
assert.equal(normaliseHost("https://dokploy.example.com/api"), expected);
assert.equal(normaliseHost(" https://dokploy.example.com/api/ "), expected);
});
test("normaliseHost rejects empty and non-HTTP hosts", () => {
assert.throws(() => normaliseHost(""), /must not be empty/);
assert.throws(() => normaliseHost("dokploy.example.com"), /http:\/\/ or https:\/\//);
assert.throws(() => normaliseHost("ftp://dokploy.example.com"), /http:\/\/ or https:\/\//);
});
test("query flattens input into query parameters and drops nulls", async () => {
let seen;
const api = clientWith(async (url, init) => {
seen = { url, init };
return response(200, { ok: true });
});
await api.query("deployment.all", { applicationId: "app-1", tail: 100, since: null });
assert.equal(seen.url, "https://dokploy.example.com/api/deployment.all?applicationId=app-1&tail=100");
assert.equal(seen.init.method, "GET");
assert.equal(seen.init.headers["x-api-key"], "secret");
assert.equal(seen.init.body, undefined);
});
test("mutate posts a JSON body", async () => {
let seen;
const api = clientWith(async (url, init) => {
seen = { url, init };
return response(200, true);
});
const result = await api.mutate("application.deploy", { applicationId: "app-1", title: "CI abc1234" });
assert.equal(seen.url, "https://dokploy.example.com/api/application.deploy");
assert.equal(seen.init.method, "POST");
assert.equal(seen.init.headers["content-type"], "application/json");
assert.deepEqual(JSON.parse(seen.init.body), { applicationId: "app-1", title: "CI abc1234" });
assert.equal(result, true);
});
test("an error response becomes a DokployError carrying the Zod field errors", async () => {
const body = {
message: "Invalid input",
code: "BAD_REQUEST",
data: { code: "BAD_REQUEST", path: "application.deploy", zodError: { fieldErrors: { applicationId: ["Required"] } } },
};
const api = clientWith(async () => response(400, body));
await assert.rejects(
() => api.mutate("application.deploy", {}),
(err) => {
assert.ok(err instanceof DokployError);
assert.equal(err.status, 400);
assert.match(err.message, /HTTP 400/);
assert.match(err.message, /BAD_REQUEST/);
assert.match(err.message, /applicationId: Required/);
return true;
},
);
});
test("a 500 whose message says 'not found' still counts as not found", () => {
const err = parseError(500, "application.one", JSON.stringify({ message: "Application not found" }));
assert.equal(err.isNotFound, true);
});
test("queries retry a 5xx, mutations do not", async () => {
let queryAttempts = 0;
const flaky = clientWith(async () => {
queryAttempts += 1;
return queryAttempts < 3 ? response(502, "bad gateway") : response(200, { fine: true });
});
assert.deepEqual(await flaky.query("project.all"), { fine: true });
assert.equal(queryAttempts, 3);
let mutateAttempts = 0;
const failing = clientWith(async () => {
mutateAttempts += 1;
return response(502, "bad gateway");
});
await assert.rejects(() => failing.mutate("application.deploy", {}));
assert.equal(mutateAttempts, 1, "a retried deploy would queue a second build");
});
test("queries give up after the configured number of retries", async () => {
let attempts = 0;
const api = clientWith(
async () => {
attempts += 1;
return response(500, "boom");
},
{ retries: 1 },
);
await assert.rejects(() => api.query("project.all"));
assert.equal(attempts, 2);
});
test("an HTML response explains that the host is probably wrong", async () => {
const api = clientWith(async () => response(200, "<!doctype html><html><body>nginx</body></html>"));
await assert.rejects(() => api.query("project.all"), /is `host` pointing at a Dokploy instance\?/);
});
test("a network failure is reported against the procedure that caused it", async () => {
const api = clientWith(async () => {
throw new Error("ECONNREFUSED");
});
await assert.rejects(() => api.mutate("application.deploy", {}), /Calling application\.deploy: ECONNREFUSED/);
});