feat: integrate Vitest for testing and update workflows

- Added Vitest as a testing framework in package.json and configured test scripts.
- Created new test files for CLI and client functionalities, ensuring comprehensive coverage.
- Updated GitHub Actions workflows to include testing steps and streamlined release process.
- Removed the deprecated onRelease workflow to consolidate CI/CD processes.
This commit is contained in:
Mauricio Siu
2026-04-15 21:00:48 -06:00
parent 0869e2d69a
commit c073a037b1
8 changed files with 978 additions and 45 deletions

86
tests/cli.test.ts Normal file
View File

@@ -0,0 +1,86 @@
import { execFileSync } from "node:child_process";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
const CLI = path.join(ROOT, "dist", "index.js");
function run(...args: string[]): string {
return execFileSync("node", [CLI, ...args], {
encoding: "utf8",
env: { ...process.env, NO_COLOR: "1" },
});
}
describe("CLI", () => {
it("should show help with --help", () => {
const output = run("--help");
expect(output).toContain("Dokploy CLI");
expect(output).toContain("auth");
expect(output).toContain("application");
expect(output).toContain("project");
});
it("should show version with --version", () => {
const output = run("--version");
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
});
it("should show subcommands for application", () => {
const output = run("application", "--help");
expect(output).toContain("create");
expect(output).toContain("deploy");
expect(output).toContain("delete");
expect(output).toContain("stop");
});
it("should show subcommands for postgres", () => {
const output = run("postgres", "--help");
expect(output).toContain("create");
expect(output).toContain("deploy");
expect(output).toContain("remove");
});
it("should show options for a specific command", () => {
const output = run("application", "create", "--help");
expect(output).toContain("--name");
expect(output).toContain("--environmentId");
});
it("should show auth command options", () => {
const output = run("auth", "--help");
expect(output).toContain("--url");
expect(output).toContain("--token");
});
it("should show all expected command groups", () => {
const output = run("--help");
const expectedGroups = [
"application",
"postgres",
"mysql",
"redis",
"mongo",
"mariadb",
"compose",
"docker",
"project",
"server",
"domain",
"backup",
"settings",
"user",
"environment",
];
for (const group of expectedGroups) {
expect(output).toContain(group);
}
});
it("should exit with 0 when no args provided", () => {
const output = run();
expect(output).toContain("Usage:");
});
});

56
tests/client.test.ts Normal file
View File

@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
describe("readAuthConfig", () => {
const originalEnv = { ...process.env };
beforeEach(() => {
delete process.env.DOKPLOY_URL;
delete process.env.DOKPLOY_API_KEY;
delete process.env.DOKPLOY_AUTH_TOKEN;
});
afterEach(() => {
process.env = { ...originalEnv };
vi.restoreAllMocks();
});
it("should read from DOKPLOY_API_KEY env var", async () => {
process.env.DOKPLOY_URL = "https://test.dokploy.com";
process.env.DOKPLOY_API_KEY = "test-key-123";
const { readAuthConfig } = await import("../src/client.js");
const config = readAuthConfig();
expect(config.url).toBe("https://test.dokploy.com");
expect(config.token).toBe("test-key-123");
});
it("should read from DOKPLOY_AUTH_TOKEN env var as fallback", async () => {
process.env.DOKPLOY_URL = "https://test.dokploy.com";
process.env.DOKPLOY_AUTH_TOKEN = "auth-token-456";
const { readAuthConfig } = await import("../src/client.js");
const config = readAuthConfig();
expect(config.url).toBe("https://test.dokploy.com");
expect(config.token).toBe("auth-token-456");
});
it("should prefer DOKPLOY_API_KEY over DOKPLOY_AUTH_TOKEN", async () => {
process.env.DOKPLOY_URL = "https://test.dokploy.com";
process.env.DOKPLOY_API_KEY = "api-key";
process.env.DOKPLOY_AUTH_TOKEN = "auth-token";
const { readAuthConfig } = await import("../src/client.js");
const config = readAuthConfig();
expect(config.token).toBe("api-key");
});
});
describe("saveAuthConfig", () => {
it("should write config with correct structure", async () => {
const { saveAuthConfig } = await import("../src/client.js");
expect(typeof saveAuthConfig).toBe("function");
});
});

63
tests/generator.test.ts Normal file
View File

@@ -0,0 +1,63 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, "..");
describe("generator", () => {
const specPath = path.join(ROOT, "openapi.json");
const generatedPath = path.join(ROOT, "src", "generated", "commands.ts");
it("openapi.json should exist", () => {
expect(fs.existsSync(specPath)).toBe(true);
});
it("generated commands file should exist", () => {
expect(fs.existsSync(generatedPath)).toBe(true);
});
it("generated file should export registerGeneratedCommands", () => {
const content = fs.readFileSync(generatedPath, "utf8");
expect(content).toContain("export function registerGeneratedCommands");
});
it("generated file should import from client", () => {
const content = fs.readFileSync(generatedPath, "utf8");
expect(content).toContain('from "../client.js"');
});
it("generated file should contain command groups from the spec", () => {
const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
const content = fs.readFileSync(generatedPath, "utf8");
// Only check groups that have a dot-separated action (group.action)
const groups = new Set<string>();
for (const p of Object.keys(spec.paths)) {
const clean = p.replace(/^\//, "");
const [group, ...rest] = clean.split(".");
if (group && rest.length > 0) groups.add(group);
}
for (const group of groups) {
expect(content).toContain(`"${group}.`);
}
});
it("number of apiPost/apiGet calls should match valid endpoints", () => {
const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
const content = fs.readFileSync(generatedPath, "utf8");
// Count only endpoints with group.action pattern
let validEndpoints = 0;
for (const p of Object.keys(spec.paths)) {
const clean = p.replace(/^\//, "");
const [group, ...rest] = clean.split(".");
if (group && rest.length > 0) validEndpoints++;
}
const apiCalls = (content.match(/await api(Post|Get)\(/g) || []).length;
expect(apiCalls).toBe(validEndpoints);
});
});