"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, "nginx")); 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/); });