Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
- name: Run integration tests
run: bun run test:integration:run

- name: Dump container logs on failure
if: failure()
run: docker compose -f docker-compose.test.yml logs --tail=200

- name: Teardown containers
if: always()
run: bun run test:integration:teardown
5 changes: 3 additions & 2 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ services:
- "${COOLIFY_TEST_PORT:-8099}:8080"
environment:
APP_NAME: "Coolify"
APP_ENV: local
APP_DEBUG: "true"
APP_ENV: production
APP_DEBUG: "false"
TELESCOPE_ENABLED: "false"
APP_ID: "test-integration-id-1234567890abcdef"
APP_KEY: "base64:MUScccH4Baj16/FXv2FjY0t6U5fse9zivTxus2AC7XI="
APP_URL: "http://localhost:8099"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mcp-coolify",
"version": "2.3.0",
"version": "3.0.0",
"description": "MCP server for Coolify. Manage applications, databases, services, deployments, and servers from any MCP-compatible client.",
"type": "module",
"main": "dist/index.js",
Expand Down
4 changes: 4 additions & 0 deletions scripts/integration-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const files = [
"11-service-envs",
"12-deployments",
"13-logs",
"15-scheduled-tasks",
"16-storages",
"17-github-apps",
"18-resources",
"14-cleanup",
];

Expand Down
2 changes: 1 addition & 1 deletion scripts/integration-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { writeFileSync } from "node:fs";

const COOLIFY_URL = process.env.COOLIFY_TEST_URL ?? "http://localhost:8099";
const MAX_WAIT_MS = 120_000;
const MAX_WAIT_MS = 300_000;
const POLL_INTERVAL_MS = 3_000;
const STATE_FILE = "/tmp/coolify-integration-state.json";

Expand Down
58 changes: 58 additions & 0 deletions src/__integration__/15-scheduled-tasks.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, expect, test } from "bun:test";
import { createTestClient, readState } from "./setup";

const client = createTestClient();

describe("15 - Scheduled Tasks", () => {
let taskUuid: string;

test("createApplicationScheduledTask creates a task", async () => {
const { applicationUuid } = readState();
if (!applicationUuid) return;
const result = await client.createApplicationScheduledTask(applicationUuid, {
name: "integration-test-task",
command: "echo hello",
frequency: "* * * * *",
enabled: false,
});
expect(result).toHaveProperty("uuid");
expect(result.uuid.length).toBeGreaterThan(0);
taskUuid = result.uuid;
});

test("listApplicationScheduledTasks includes the created task", async () => {
const { applicationUuid } = readState();
if (!applicationUuid) return;
const tasks = await client.listApplicationScheduledTasks(applicationUuid);
expect(Array.isArray(tasks)).toBe(true);
const found = tasks.find((t) => t.uuid === taskUuid);
expect(found).toBeDefined();
expect(found?.name).toBe("integration-test-task");
});

test("updateApplicationScheduledTask updates the task", async () => {
const { applicationUuid } = readState();
if (!applicationUuid || !taskUuid) return;
const result = await client.updateApplicationScheduledTask(applicationUuid, taskUuid, {
name: "integration-test-task-updated",
});
expect(result).toBeDefined();
});

test("listApplicationScheduledTaskExecutions returns an array", async () => {
const { applicationUuid } = readState();
if (!applicationUuid || !taskUuid) return;
const executions = await client.listApplicationScheduledTaskExecutions(
applicationUuid,
taskUuid,
);
expect(Array.isArray(executions)).toBe(true);
});

test("deleteApplicationScheduledTask deletes the task", async () => {
const { applicationUuid } = readState();
if (!applicationUuid || !taskUuid) return;
const result = await client.deleteApplicationScheduledTask(applicationUuid, taskUuid);
expect(result).toBeDefined();
});
});
88 changes: 88 additions & 0 deletions src/__integration__/16-storages.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from "bun:test";
import { CoolifyApiError } from "../lib/errors";
import { createTestClient, readState } from "./setup";

const client = createTestClient();

describe("16 - Storages", () => {
let storageUuid: string;

test("listApplicationStorages returns data", async () => {
const { applicationUuid } = readState();
if (!applicationUuid) return;
try {
const storages = await client.listApplicationStorages(applicationUuid);
expect(storages).toBeDefined();
} catch (error) {
if (error instanceof CoolifyApiError && error.statusCode === 404) {
console.log("Storages endpoint not available on this Coolify version, skipping");
return;
}
throw error;
}
});

test("createApplicationStorage creates a storage mount", async () => {
const { applicationUuid } = readState();
if (!applicationUuid) return;
try {
const result = await client.createApplicationStorage(applicationUuid, {
name: "integration-test-storage",
mount_path: "/test-data",
});
expect(result).toHaveProperty("uuid");
storageUuid = result.uuid;
} catch (error) {
if (error instanceof CoolifyApiError && [404, 422].includes(error.statusCode)) {
console.log(`Storages create skipped (${error.statusCode}): ${error.responseBody}`);
return;
}
throw error;
}
});

test("deleteApplicationStorage cleans up", async () => {
const { applicationUuid } = readState();
if (!applicationUuid || !storageUuid) return;
try {
const result = await client.deleteApplicationStorage(applicationUuid, storageUuid);
expect(result).toBeDefined();
} catch (error) {
if (error instanceof CoolifyApiError && [404, 422].includes(error.statusCode)) {
console.log(`Storages delete skipped (${error.statusCode})`);
return;
}
throw error;
}
});

test("listDatabaseStorages returns data", async () => {
const { databaseUuid } = readState();
if (!databaseUuid) return;
try {
const storages = await client.listDatabaseStorages(databaseUuid);
expect(storages).toBeDefined();
} catch (error) {
if (error instanceof CoolifyApiError && error.statusCode === 404) {
console.log("Database storages endpoint not available, skipping");
return;
}
throw error;
}
});

test("listServiceStorages returns data", async () => {
const { serviceUuid } = readState();
if (!serviceUuid) return;
try {
const storages = await client.listServiceStorages(serviceUuid);
expect(storages).toBeDefined();
} catch (error) {
if (error instanceof CoolifyApiError && error.statusCode === 404) {
console.log("Service storages endpoint not available, skipping");
return;
}
throw error;
}
});
});
11 changes: 11 additions & 0 deletions src/__integration__/17-github-apps.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, test } from "bun:test";
import { createTestClient } from "./setup";

const client = createTestClient();

describe("17 - GitHub Apps", () => {
test("listGitHubApps returns an array", async () => {
const apps = await client.listGitHubApps();
expect(Array.isArray(apps)).toBe(true);
});
});
11 changes: 11 additions & 0 deletions src/__integration__/18-resources.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, test } from "bun:test";
import { createTestClient } from "./setup";

const client = createTestClient();

describe("18 - Resources", () => {
test("listResources returns an array", async () => {
const resources = await client.listResources();
expect(Array.isArray(resources)).toBe(true);
});
});
Loading
Loading