@masonator/coolify-mcp 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/__tests__/coolify-client.test.js +627 -8
- package/dist/__tests__/integration/diagnostics.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/diagnostics.integration.test.js +140 -0
- package/dist/__tests__/mcp-server.test.js +166 -0
- package/dist/lib/coolify-client.d.ts +39 -3
- package/dist/lib/coolify-client.js +403 -15
- package/dist/lib/mcp-server.js +42 -1
- package/dist/types/coolify.d.ts +89 -0
- package/package.json +14 -4
|
@@ -745,14 +745,26 @@ describe('CoolifyClient', () => {
|
|
|
745
745
|
expect(result).toEqual(mockBackups);
|
|
746
746
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups', expect.any(Object));
|
|
747
747
|
});
|
|
748
|
-
it('should
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
748
|
+
it('should get a database backup', async () => {
|
|
749
|
+
const mockBackup = { uuid: 'backup-uuid', enabled: true, frequency: '0 0 * * *' };
|
|
750
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockBackup));
|
|
751
|
+
const result = await client.getDatabaseBackup('db-uuid', 'backup-uuid');
|
|
752
|
+
expect(result).toEqual(mockBackup);
|
|
753
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid', expect.any(Object));
|
|
754
|
+
});
|
|
755
|
+
it('should list backup executions', async () => {
|
|
756
|
+
const mockExecutions = [{ uuid: 'exec-uuid', status: 'success' }];
|
|
757
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockExecutions));
|
|
758
|
+
const result = await client.listBackupExecutions('db-uuid', 'backup-uuid');
|
|
759
|
+
expect(result).toEqual(mockExecutions);
|
|
760
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid/executions', expect.any(Object));
|
|
761
|
+
});
|
|
762
|
+
it('should get a backup execution', async () => {
|
|
763
|
+
const mockExecution = { uuid: 'exec-uuid', status: 'success', size: 1024 };
|
|
764
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockExecution));
|
|
765
|
+
const result = await client.getBackupExecution('db-uuid', 'backup-uuid', 'exec-uuid');
|
|
766
|
+
expect(result).toEqual(mockExecution);
|
|
767
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid/executions/exec-uuid', expect.any(Object));
|
|
756
768
|
});
|
|
757
769
|
});
|
|
758
770
|
// =========================================================================
|
|
@@ -1022,4 +1034,611 @@ describe('CoolifyClient', () => {
|
|
|
1022
1034
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deploy?tag=my-tag&force=false', expect.any(Object));
|
|
1023
1035
|
});
|
|
1024
1036
|
});
|
|
1037
|
+
// ===========================================================================
|
|
1038
|
+
// Database Backup Tests
|
|
1039
|
+
// ===========================================================================
|
|
1040
|
+
describe('Database Backups', () => {
|
|
1041
|
+
const mockBackups = [
|
|
1042
|
+
{
|
|
1043
|
+
id: 1,
|
|
1044
|
+
uuid: 'backup-uuid-1',
|
|
1045
|
+
database_id: 1,
|
|
1046
|
+
database_type: 'postgresql',
|
|
1047
|
+
database_uuid: 'db-uuid',
|
|
1048
|
+
enabled: true,
|
|
1049
|
+
frequency: '0 0 * * *',
|
|
1050
|
+
save_s3: false,
|
|
1051
|
+
created_at: '2024-01-01',
|
|
1052
|
+
updated_at: '2024-01-01',
|
|
1053
|
+
},
|
|
1054
|
+
];
|
|
1055
|
+
const mockExecutions = [
|
|
1056
|
+
{
|
|
1057
|
+
id: 1,
|
|
1058
|
+
uuid: 'exec-uuid-1',
|
|
1059
|
+
scheduled_database_backup_id: 1,
|
|
1060
|
+
status: 'success',
|
|
1061
|
+
message: 'Backup completed',
|
|
1062
|
+
size: 1024,
|
|
1063
|
+
filename: 'backup-20240101.sql',
|
|
1064
|
+
created_at: '2024-01-01',
|
|
1065
|
+
updated_at: '2024-01-01',
|
|
1066
|
+
},
|
|
1067
|
+
];
|
|
1068
|
+
it('should list database backups', async () => {
|
|
1069
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockBackups));
|
|
1070
|
+
const result = await client.listDatabaseBackups('db-uuid');
|
|
1071
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups', expect.any(Object));
|
|
1072
|
+
expect(result).toEqual(mockBackups);
|
|
1073
|
+
});
|
|
1074
|
+
it('should get a specific database backup', async () => {
|
|
1075
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockBackups[0]));
|
|
1076
|
+
const result = await client.getDatabaseBackup('db-uuid', 'backup-uuid-1');
|
|
1077
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid-1', expect.any(Object));
|
|
1078
|
+
expect(result).toEqual(mockBackups[0]);
|
|
1079
|
+
});
|
|
1080
|
+
it('should list backup executions', async () => {
|
|
1081
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockExecutions));
|
|
1082
|
+
const result = await client.listBackupExecutions('db-uuid', 'backup-uuid-1');
|
|
1083
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid-1/executions', expect.any(Object));
|
|
1084
|
+
expect(result).toEqual(mockExecutions);
|
|
1085
|
+
});
|
|
1086
|
+
it('should get a specific backup execution', async () => {
|
|
1087
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockExecutions[0]));
|
|
1088
|
+
const result = await client.getBackupExecution('db-uuid', 'backup-uuid-1', 'exec-uuid-1');
|
|
1089
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid-1/executions/exec-uuid-1', expect.any(Object));
|
|
1090
|
+
expect(result).toEqual(mockExecutions[0]);
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
// ===========================================================================
|
|
1094
|
+
// Deployment Control Tests
|
|
1095
|
+
// ===========================================================================
|
|
1096
|
+
describe('Deployment Control', () => {
|
|
1097
|
+
it('should cancel a deployment', async () => {
|
|
1098
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deployment cancelled' }));
|
|
1099
|
+
const result = await client.cancelDeployment('deploy-uuid');
|
|
1100
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/deployments/deploy-uuid/cancel', expect.objectContaining({ method: 'POST' }));
|
|
1101
|
+
expect(result).toEqual({ message: 'Deployment cancelled' });
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
// ===========================================================================
|
|
1105
|
+
// Smart Lookup Tests
|
|
1106
|
+
// ===========================================================================
|
|
1107
|
+
describe('Smart Lookup', () => {
|
|
1108
|
+
describe('resolveApplicationUuid', () => {
|
|
1109
|
+
const mockApps = [
|
|
1110
|
+
{
|
|
1111
|
+
id: 1,
|
|
1112
|
+
uuid: 'app-uuid-1',
|
|
1113
|
+
name: 'tidylinker',
|
|
1114
|
+
status: 'running',
|
|
1115
|
+
fqdn: 'https://tidylinker.com',
|
|
1116
|
+
created_at: '2024-01-01',
|
|
1117
|
+
updated_at: '2024-01-01',
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
id: 2,
|
|
1121
|
+
uuid: 'app-uuid-2',
|
|
1122
|
+
name: 'my-api',
|
|
1123
|
+
status: 'running',
|
|
1124
|
+
fqdn: 'https://api.example.com',
|
|
1125
|
+
created_at: '2024-01-01',
|
|
1126
|
+
updated_at: '2024-01-01',
|
|
1127
|
+
},
|
|
1128
|
+
];
|
|
1129
|
+
it('should return UUID directly if it looks like a UUID', async () => {
|
|
1130
|
+
// UUIDs are alphanumeric, 20+ chars - no API call should be made
|
|
1131
|
+
const result = await client.resolveApplicationUuid('xs0sgs4gog044s4k4c88kgsc');
|
|
1132
|
+
expect(result).toBe('xs0sgs4gog044s4k4c88kgsc');
|
|
1133
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1134
|
+
});
|
|
1135
|
+
it('should find application by name', async () => {
|
|
1136
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1137
|
+
const result = await client.resolveApplicationUuid('tidylinker');
|
|
1138
|
+
expect(result).toBe('app-uuid-1');
|
|
1139
|
+
});
|
|
1140
|
+
it('should find application by partial name (case-insensitive)', async () => {
|
|
1141
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1142
|
+
const result = await client.resolveApplicationUuid('TidyLink');
|
|
1143
|
+
expect(result).toBe('app-uuid-1');
|
|
1144
|
+
});
|
|
1145
|
+
it('should find application by domain', async () => {
|
|
1146
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1147
|
+
const result = await client.resolveApplicationUuid('tidylinker.com');
|
|
1148
|
+
expect(result).toBe('app-uuid-1');
|
|
1149
|
+
});
|
|
1150
|
+
it('should find application by partial domain', async () => {
|
|
1151
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1152
|
+
const result = await client.resolveApplicationUuid('api.example.com');
|
|
1153
|
+
expect(result).toBe('app-uuid-2');
|
|
1154
|
+
});
|
|
1155
|
+
it('should throw error if no application found', async () => {
|
|
1156
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockApps));
|
|
1157
|
+
await expect(client.resolveApplicationUuid('nonexistent')).rejects.toThrow('No application found matching "nonexistent"');
|
|
1158
|
+
});
|
|
1159
|
+
it('should throw error if multiple applications match', async () => {
|
|
1160
|
+
const multiMatchApps = [
|
|
1161
|
+
{ ...mockApps[0], name: 'test-app-1' },
|
|
1162
|
+
{ ...mockApps[1], name: 'test-app-2' },
|
|
1163
|
+
];
|
|
1164
|
+
mockFetch.mockResolvedValueOnce(mockResponse(multiMatchApps));
|
|
1165
|
+
await expect(client.resolveApplicationUuid('test-app')).rejects.toThrow('Multiple applications match');
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
describe('resolveServerUuid', () => {
|
|
1169
|
+
const mockServers = [
|
|
1170
|
+
{
|
|
1171
|
+
id: 1,
|
|
1172
|
+
uuid: 'server-uuid-1',
|
|
1173
|
+
name: 'coolify-apps',
|
|
1174
|
+
ip: '192.168.1.100',
|
|
1175
|
+
user: 'root',
|
|
1176
|
+
port: 22,
|
|
1177
|
+
created_at: '2024-01-01',
|
|
1178
|
+
updated_at: '2024-01-01',
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
id: 2,
|
|
1182
|
+
uuid: 'server-uuid-2',
|
|
1183
|
+
name: 'production-db',
|
|
1184
|
+
ip: '10.0.0.50',
|
|
1185
|
+
user: 'root',
|
|
1186
|
+
port: 22,
|
|
1187
|
+
created_at: '2024-01-01',
|
|
1188
|
+
updated_at: '2024-01-01',
|
|
1189
|
+
},
|
|
1190
|
+
];
|
|
1191
|
+
it('should return UUID directly if it looks like a UUID', async () => {
|
|
1192
|
+
const result = await client.resolveServerUuid('ggkk8w4c08gw48oowsg4g0oc');
|
|
1193
|
+
expect(result).toBe('ggkk8w4c08gw48oowsg4g0oc');
|
|
1194
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
1195
|
+
});
|
|
1196
|
+
it('should find server by name', async () => {
|
|
1197
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1198
|
+
const result = await client.resolveServerUuid('coolify-apps');
|
|
1199
|
+
expect(result).toBe('server-uuid-1');
|
|
1200
|
+
});
|
|
1201
|
+
it('should find server by partial name (case-insensitive)', async () => {
|
|
1202
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1203
|
+
const result = await client.resolveServerUuid('Coolify');
|
|
1204
|
+
expect(result).toBe('server-uuid-1');
|
|
1205
|
+
});
|
|
1206
|
+
it('should find server by IP address', async () => {
|
|
1207
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1208
|
+
const result = await client.resolveServerUuid('192.168.1.100');
|
|
1209
|
+
expect(result).toBe('server-uuid-1');
|
|
1210
|
+
});
|
|
1211
|
+
it('should find server by partial IP', async () => {
|
|
1212
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1213
|
+
const result = await client.resolveServerUuid('10.0.0');
|
|
1214
|
+
expect(result).toBe('server-uuid-2');
|
|
1215
|
+
});
|
|
1216
|
+
it('should throw error if no server found', async () => {
|
|
1217
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockServers));
|
|
1218
|
+
await expect(client.resolveServerUuid('nonexistent')).rejects.toThrow('No server found matching "nonexistent"');
|
|
1219
|
+
});
|
|
1220
|
+
it('should throw error if multiple servers match', async () => {
|
|
1221
|
+
const multiMatchServers = [
|
|
1222
|
+
{ ...mockServers[0], name: 'prod-server-1' },
|
|
1223
|
+
{ ...mockServers[1], name: 'prod-server-2' },
|
|
1224
|
+
];
|
|
1225
|
+
mockFetch.mockResolvedValueOnce(mockResponse(multiMatchServers));
|
|
1226
|
+
await expect(client.resolveServerUuid('prod-server')).rejects.toThrow('Multiple servers match');
|
|
1227
|
+
});
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
// ===========================================================================
|
|
1231
|
+
// Diagnostic Methods Tests
|
|
1232
|
+
// ===========================================================================
|
|
1233
|
+
describe('Diagnostic Methods', () => {
|
|
1234
|
+
describe('diagnoseApplication', () => {
|
|
1235
|
+
// Use UUID-like format that matches the isLikelyUuid check
|
|
1236
|
+
const testAppUuid = 'app0uuid0test0001234567';
|
|
1237
|
+
const mockApp = {
|
|
1238
|
+
id: 1,
|
|
1239
|
+
uuid: testAppUuid,
|
|
1240
|
+
name: 'test-app',
|
|
1241
|
+
status: 'running:healthy',
|
|
1242
|
+
fqdn: 'https://test.com',
|
|
1243
|
+
git_repository: 'org/repo',
|
|
1244
|
+
git_branch: 'main',
|
|
1245
|
+
created_at: '2024-01-01',
|
|
1246
|
+
updated_at: '2024-01-01',
|
|
1247
|
+
};
|
|
1248
|
+
const mockLogs = 'Log line 1\nLog line 2\nLog line 3';
|
|
1249
|
+
const mockEnvVars = [
|
|
1250
|
+
{
|
|
1251
|
+
id: 1,
|
|
1252
|
+
uuid: 'env-1',
|
|
1253
|
+
key: 'DATABASE_URL',
|
|
1254
|
+
value: 'postgres://...',
|
|
1255
|
+
is_build_time: false,
|
|
1256
|
+
},
|
|
1257
|
+
{ id: 2, uuid: 'env-2', key: 'NODE_ENV', value: 'production', is_build_time: true },
|
|
1258
|
+
];
|
|
1259
|
+
const mockDeployments = [
|
|
1260
|
+
{
|
|
1261
|
+
id: 1,
|
|
1262
|
+
uuid: 'deploy-1',
|
|
1263
|
+
deployment_uuid: 'deploy-1',
|
|
1264
|
+
status: 'finished',
|
|
1265
|
+
force_rebuild: false,
|
|
1266
|
+
is_webhook: false,
|
|
1267
|
+
is_api: false,
|
|
1268
|
+
restart_only: false,
|
|
1269
|
+
created_at: '2024-01-01',
|
|
1270
|
+
updated_at: '2024-01-01',
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
id: 2,
|
|
1274
|
+
uuid: 'deploy-2',
|
|
1275
|
+
deployment_uuid: 'deploy-2',
|
|
1276
|
+
status: 'finished',
|
|
1277
|
+
force_rebuild: false,
|
|
1278
|
+
is_webhook: false,
|
|
1279
|
+
is_api: false,
|
|
1280
|
+
restart_only: false,
|
|
1281
|
+
created_at: '2024-01-02',
|
|
1282
|
+
updated_at: '2024-01-02',
|
|
1283
|
+
},
|
|
1284
|
+
];
|
|
1285
|
+
it('should aggregate all application data successfully', async () => {
|
|
1286
|
+
mockFetch
|
|
1287
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1288
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1289
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1290
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1291
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1292
|
+
expect(result.application).toEqual({
|
|
1293
|
+
uuid: testAppUuid,
|
|
1294
|
+
name: 'test-app',
|
|
1295
|
+
status: 'running:healthy',
|
|
1296
|
+
fqdn: 'https://test.com',
|
|
1297
|
+
git_repository: 'org/repo',
|
|
1298
|
+
git_branch: 'main',
|
|
1299
|
+
});
|
|
1300
|
+
expect(result.health.status).toBe('healthy');
|
|
1301
|
+
expect(result.logs).toBe(mockLogs);
|
|
1302
|
+
expect(result.environment_variables.count).toBe(2);
|
|
1303
|
+
expect(result.environment_variables.variables).toEqual([
|
|
1304
|
+
{ key: 'DATABASE_URL', is_build_time: false },
|
|
1305
|
+
{ key: 'NODE_ENV', is_build_time: true },
|
|
1306
|
+
]);
|
|
1307
|
+
expect(result.recent_deployments).toHaveLength(2);
|
|
1308
|
+
expect(result.errors).toBeUndefined();
|
|
1309
|
+
});
|
|
1310
|
+
it('should detect unhealthy application status', async () => {
|
|
1311
|
+
const unhealthyApp = { ...mockApp, status: 'exited:unhealthy' };
|
|
1312
|
+
mockFetch
|
|
1313
|
+
.mockResolvedValueOnce(mockResponse(unhealthyApp))
|
|
1314
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1315
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1316
|
+
.mockResolvedValueOnce(mockResponse([]));
|
|
1317
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1318
|
+
expect(result.health.status).toBe('unhealthy');
|
|
1319
|
+
expect(result.health.issues).toContain('Status: exited:unhealthy');
|
|
1320
|
+
});
|
|
1321
|
+
it('should detect failed deployments as issues', async () => {
|
|
1322
|
+
const failedDeployments = [
|
|
1323
|
+
{ ...mockDeployments[0], status: 'failed' },
|
|
1324
|
+
{ ...mockDeployments[1], status: 'failed' },
|
|
1325
|
+
];
|
|
1326
|
+
mockFetch
|
|
1327
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1328
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1329
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1330
|
+
.mockResolvedValueOnce(mockResponse(failedDeployments));
|
|
1331
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1332
|
+
expect(result.health.issues).toContain('2 failed deployment(s) in last 5');
|
|
1333
|
+
});
|
|
1334
|
+
it('should handle partial failures gracefully', async () => {
|
|
1335
|
+
mockFetch
|
|
1336
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1337
|
+
.mockRejectedValueOnce(new Error('Logs unavailable'))
|
|
1338
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1339
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1340
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1341
|
+
expect(result.application).not.toBeNull();
|
|
1342
|
+
expect(result.logs).toBeNull();
|
|
1343
|
+
expect(result.errors).toContain('logs: Logs unavailable');
|
|
1344
|
+
});
|
|
1345
|
+
it('should handle complete failure gracefully', async () => {
|
|
1346
|
+
mockFetch
|
|
1347
|
+
.mockRejectedValueOnce(new Error('App not found'))
|
|
1348
|
+
.mockRejectedValueOnce(new Error('Logs unavailable'))
|
|
1349
|
+
.mockRejectedValueOnce(new Error('Env vars unavailable'))
|
|
1350
|
+
.mockRejectedValueOnce(new Error('Deployments unavailable'));
|
|
1351
|
+
const result = await client.diagnoseApplication(testAppUuid);
|
|
1352
|
+
expect(result.application).toBeNull();
|
|
1353
|
+
expect(result.logs).toBeNull();
|
|
1354
|
+
expect(result.health.status).toBe('unknown');
|
|
1355
|
+
expect(result.errors).toHaveLength(4);
|
|
1356
|
+
});
|
|
1357
|
+
it('should find application by name and diagnose it', async () => {
|
|
1358
|
+
const mockApps = [{ ...mockApp, uuid: 'found-uuid', name: 'my-app' }];
|
|
1359
|
+
mockFetch
|
|
1360
|
+
.mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
|
|
1361
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1362
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1363
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1364
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1365
|
+
const result = await client.diagnoseApplication('my-app');
|
|
1366
|
+
expect(result.application).not.toBeNull();
|
|
1367
|
+
// First call should be to list apps for lookup
|
|
1368
|
+
expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/applications', expect.any(Object));
|
|
1369
|
+
});
|
|
1370
|
+
it('should find application by domain and diagnose it', async () => {
|
|
1371
|
+
const mockApps = [{ ...mockApp, uuid: 'found-uuid', fqdn: 'https://tidylinker.com' }];
|
|
1372
|
+
mockFetch
|
|
1373
|
+
.mockResolvedValueOnce(mockResponse(mockApps)) // listApplications for lookup
|
|
1374
|
+
.mockResolvedValueOnce(mockResponse(mockApp))
|
|
1375
|
+
.mockResolvedValueOnce(mockResponse(mockLogs))
|
|
1376
|
+
.mockResolvedValueOnce(mockResponse(mockEnvVars))
|
|
1377
|
+
.mockResolvedValueOnce(mockResponse(mockDeployments));
|
|
1378
|
+
const result = await client.diagnoseApplication('tidylinker.com');
|
|
1379
|
+
expect(result.application).not.toBeNull();
|
|
1380
|
+
});
|
|
1381
|
+
it('should return error in result when application not found by name', async () => {
|
|
1382
|
+
mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty app list
|
|
1383
|
+
const result = await client.diagnoseApplication('nonexistent-app');
|
|
1384
|
+
expect(result.application).toBeNull();
|
|
1385
|
+
expect(result.errors).toContain('No application found matching "nonexistent-app"');
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
describe('diagnoseServer', () => {
|
|
1389
|
+
// Use UUID-like format that matches the isLikelyUuid check
|
|
1390
|
+
const testServerUuid = 'srv0uuid0test0001234567';
|
|
1391
|
+
const mockServer = {
|
|
1392
|
+
id: 1,
|
|
1393
|
+
uuid: testServerUuid,
|
|
1394
|
+
name: 'test-server',
|
|
1395
|
+
ip: '192.168.1.1',
|
|
1396
|
+
user: 'root',
|
|
1397
|
+
port: 22,
|
|
1398
|
+
status: 'running',
|
|
1399
|
+
is_reachable: true,
|
|
1400
|
+
is_usable: true,
|
|
1401
|
+
created_at: '2024-01-01',
|
|
1402
|
+
updated_at: '2024-01-01',
|
|
1403
|
+
};
|
|
1404
|
+
const mockResources = [
|
|
1405
|
+
{
|
|
1406
|
+
id: 1,
|
|
1407
|
+
uuid: 'res-1',
|
|
1408
|
+
name: 'app-1',
|
|
1409
|
+
type: 'application',
|
|
1410
|
+
status: 'running:healthy',
|
|
1411
|
+
created_at: '2024-01-01',
|
|
1412
|
+
updated_at: '2024-01-01',
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
id: 2,
|
|
1416
|
+
uuid: 'res-2',
|
|
1417
|
+
name: 'db-1',
|
|
1418
|
+
type: 'database',
|
|
1419
|
+
status: 'running:healthy',
|
|
1420
|
+
created_at: '2024-01-01',
|
|
1421
|
+
updated_at: '2024-01-01',
|
|
1422
|
+
},
|
|
1423
|
+
];
|
|
1424
|
+
const mockDomains = [{ ip: '192.168.1.1', domains: ['example.com', 'api.example.com'] }];
|
|
1425
|
+
const mockValidation = { message: 'Server is reachable and validated' };
|
|
1426
|
+
it('should aggregate all server data successfully', async () => {
|
|
1427
|
+
mockFetch
|
|
1428
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1429
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1430
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1431
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1432
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1433
|
+
expect(result.server).toEqual({
|
|
1434
|
+
uuid: testServerUuid,
|
|
1435
|
+
name: 'test-server',
|
|
1436
|
+
ip: '192.168.1.1',
|
|
1437
|
+
status: 'running',
|
|
1438
|
+
is_reachable: true,
|
|
1439
|
+
});
|
|
1440
|
+
expect(result.health.status).toBe('healthy');
|
|
1441
|
+
expect(result.resources).toHaveLength(2);
|
|
1442
|
+
expect(result.domains).toHaveLength(1);
|
|
1443
|
+
expect(result.validation?.message).toBe('Server is reachable and validated');
|
|
1444
|
+
expect(result.errors).toBeUndefined();
|
|
1445
|
+
});
|
|
1446
|
+
it('should detect unreachable server', async () => {
|
|
1447
|
+
const unreachableServer = { ...mockServer, is_reachable: false };
|
|
1448
|
+
mockFetch
|
|
1449
|
+
.mockResolvedValueOnce(mockResponse(unreachableServer))
|
|
1450
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1451
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1452
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1453
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1454
|
+
expect(result.health.status).toBe('unhealthy');
|
|
1455
|
+
expect(result.health.issues).toContain('Server is not reachable');
|
|
1456
|
+
});
|
|
1457
|
+
it('should detect unhealthy resources', async () => {
|
|
1458
|
+
const unhealthyResources = [
|
|
1459
|
+
{ ...mockResources[0], status: 'exited:unhealthy' },
|
|
1460
|
+
{ ...mockResources[1], status: 'running:healthy' },
|
|
1461
|
+
];
|
|
1462
|
+
mockFetch
|
|
1463
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1464
|
+
.mockResolvedValueOnce(mockResponse(unhealthyResources))
|
|
1465
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1466
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1467
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1468
|
+
expect(result.health.issues).toContain('1 unhealthy resource(s)');
|
|
1469
|
+
});
|
|
1470
|
+
it('should handle partial failures gracefully', async () => {
|
|
1471
|
+
mockFetch
|
|
1472
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1473
|
+
.mockRejectedValueOnce(new Error('Resources unavailable'))
|
|
1474
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1475
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1476
|
+
const result = await client.diagnoseServer(testServerUuid);
|
|
1477
|
+
expect(result.server).not.toBeNull();
|
|
1478
|
+
expect(result.resources).toEqual([]);
|
|
1479
|
+
expect(result.errors).toContain('resources: Resources unavailable');
|
|
1480
|
+
});
|
|
1481
|
+
it('should find server by name and diagnose it', async () => {
|
|
1482
|
+
const mockServers = [{ ...mockServer, uuid: 'found-uuid', name: 'coolify-apps' }];
|
|
1483
|
+
mockFetch
|
|
1484
|
+
.mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
|
|
1485
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1486
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1487
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1488
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1489
|
+
const result = await client.diagnoseServer('coolify-apps');
|
|
1490
|
+
expect(result.server).not.toBeNull();
|
|
1491
|
+
// First call should be to list servers for lookup
|
|
1492
|
+
expect(mockFetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/v1/servers', expect.any(Object));
|
|
1493
|
+
});
|
|
1494
|
+
it('should find server by IP and diagnose it', async () => {
|
|
1495
|
+
const mockServers = [{ ...mockServer, uuid: 'found-uuid', ip: '10.0.0.5' }];
|
|
1496
|
+
mockFetch
|
|
1497
|
+
.mockResolvedValueOnce(mockResponse(mockServers)) // listServers for lookup
|
|
1498
|
+
.mockResolvedValueOnce(mockResponse(mockServer))
|
|
1499
|
+
.mockResolvedValueOnce(mockResponse(mockResources))
|
|
1500
|
+
.mockResolvedValueOnce(mockResponse(mockDomains))
|
|
1501
|
+
.mockResolvedValueOnce(mockResponse(mockValidation));
|
|
1502
|
+
const result = await client.diagnoseServer('10.0.0.5');
|
|
1503
|
+
expect(result.server).not.toBeNull();
|
|
1504
|
+
});
|
|
1505
|
+
it('should return error in result when server not found by name', async () => {
|
|
1506
|
+
mockFetch.mockResolvedValueOnce(mockResponse([])); // Empty server list
|
|
1507
|
+
const result = await client.diagnoseServer('nonexistent-server');
|
|
1508
|
+
expect(result.server).toBeNull();
|
|
1509
|
+
expect(result.errors).toContain('No server found matching "nonexistent-server"');
|
|
1510
|
+
});
|
|
1511
|
+
});
|
|
1512
|
+
describe('findInfrastructureIssues', () => {
|
|
1513
|
+
const mockServers = [
|
|
1514
|
+
{
|
|
1515
|
+
id: 1,
|
|
1516
|
+
uuid: 'server-1',
|
|
1517
|
+
name: 'healthy-server',
|
|
1518
|
+
ip: '1.1.1.1',
|
|
1519
|
+
user: 'root',
|
|
1520
|
+
port: 22,
|
|
1521
|
+
is_reachable: true,
|
|
1522
|
+
created_at: '2024-01-01',
|
|
1523
|
+
updated_at: '2024-01-01',
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
id: 2,
|
|
1527
|
+
uuid: 'server-2',
|
|
1528
|
+
name: 'unreachable-server',
|
|
1529
|
+
ip: '2.2.2.2',
|
|
1530
|
+
user: 'root',
|
|
1531
|
+
port: 22,
|
|
1532
|
+
is_reachable: false,
|
|
1533
|
+
status: 'error',
|
|
1534
|
+
created_at: '2024-01-01',
|
|
1535
|
+
updated_at: '2024-01-01',
|
|
1536
|
+
},
|
|
1537
|
+
];
|
|
1538
|
+
const mockApplications = [
|
|
1539
|
+
{
|
|
1540
|
+
id: 1,
|
|
1541
|
+
uuid: 'app-1',
|
|
1542
|
+
name: 'healthy-app',
|
|
1543
|
+
status: 'running:healthy',
|
|
1544
|
+
created_at: '2024-01-01',
|
|
1545
|
+
updated_at: '2024-01-01',
|
|
1546
|
+
},
|
|
1547
|
+
{
|
|
1548
|
+
id: 2,
|
|
1549
|
+
uuid: 'app-2',
|
|
1550
|
+
name: 'unhealthy-app',
|
|
1551
|
+
status: 'exited:unhealthy',
|
|
1552
|
+
created_at: '2024-01-01',
|
|
1553
|
+
updated_at: '2024-01-01',
|
|
1554
|
+
},
|
|
1555
|
+
];
|
|
1556
|
+
const mockDatabases = [
|
|
1557
|
+
{
|
|
1558
|
+
id: 1,
|
|
1559
|
+
uuid: 'db-1',
|
|
1560
|
+
name: 'healthy-db',
|
|
1561
|
+
type: 'postgresql',
|
|
1562
|
+
status: 'running:healthy',
|
|
1563
|
+
is_public: false,
|
|
1564
|
+
image: 'postgres:16',
|
|
1565
|
+
created_at: '2024-01-01',
|
|
1566
|
+
updated_at: '2024-01-01',
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
id: 2,
|
|
1570
|
+
uuid: 'db-2',
|
|
1571
|
+
name: 'stopped-db',
|
|
1572
|
+
type: 'redis',
|
|
1573
|
+
status: 'exited:unhealthy',
|
|
1574
|
+
is_public: false,
|
|
1575
|
+
image: 'redis:7',
|
|
1576
|
+
created_at: '2024-01-01',
|
|
1577
|
+
updated_at: '2024-01-01',
|
|
1578
|
+
},
|
|
1579
|
+
];
|
|
1580
|
+
const mockServices = [
|
|
1581
|
+
{
|
|
1582
|
+
id: 1,
|
|
1583
|
+
uuid: 'svc-1',
|
|
1584
|
+
name: 'healthy-service',
|
|
1585
|
+
type: 'pocketbase',
|
|
1586
|
+
status: 'running:healthy',
|
|
1587
|
+
created_at: '2024-01-01',
|
|
1588
|
+
updated_at: '2024-01-01',
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
id: 2,
|
|
1592
|
+
uuid: 'svc-2',
|
|
1593
|
+
name: 'exited-service',
|
|
1594
|
+
type: 'n8n',
|
|
1595
|
+
status: 'exited',
|
|
1596
|
+
created_at: '2024-01-01',
|
|
1597
|
+
updated_at: '2024-01-01',
|
|
1598
|
+
},
|
|
1599
|
+
];
|
|
1600
|
+
it('should find all infrastructure issues', async () => {
|
|
1601
|
+
mockFetch
|
|
1602
|
+
.mockResolvedValueOnce(mockResponse(mockServers))
|
|
1603
|
+
.mockResolvedValueOnce(mockResponse(mockApplications))
|
|
1604
|
+
.mockResolvedValueOnce(mockResponse(mockDatabases))
|
|
1605
|
+
.mockResolvedValueOnce(mockResponse(mockServices));
|
|
1606
|
+
const result = await client.findInfrastructureIssues();
|
|
1607
|
+
expect(result.summary.total_issues).toBe(4);
|
|
1608
|
+
expect(result.summary.unreachable_servers).toBe(1);
|
|
1609
|
+
expect(result.summary.unhealthy_applications).toBe(1);
|
|
1610
|
+
expect(result.summary.unhealthy_databases).toBe(1);
|
|
1611
|
+
expect(result.summary.unhealthy_services).toBe(1);
|
|
1612
|
+
expect(result.issues).toHaveLength(4);
|
|
1613
|
+
expect(result.errors).toBeUndefined();
|
|
1614
|
+
});
|
|
1615
|
+
it('should return empty issues when everything is healthy', async () => {
|
|
1616
|
+
const healthyServers = [mockServers[0]];
|
|
1617
|
+
const healthyApps = [mockApplications[0]];
|
|
1618
|
+
const healthyDbs = [mockDatabases[0]];
|
|
1619
|
+
const healthySvcs = [mockServices[0]];
|
|
1620
|
+
mockFetch
|
|
1621
|
+
.mockResolvedValueOnce(mockResponse(healthyServers))
|
|
1622
|
+
.mockResolvedValueOnce(mockResponse(healthyApps))
|
|
1623
|
+
.mockResolvedValueOnce(mockResponse(healthyDbs))
|
|
1624
|
+
.mockResolvedValueOnce(mockResponse(healthySvcs));
|
|
1625
|
+
const result = await client.findInfrastructureIssues();
|
|
1626
|
+
expect(result.summary.total_issues).toBe(0);
|
|
1627
|
+
expect(result.issues).toHaveLength(0);
|
|
1628
|
+
});
|
|
1629
|
+
it('should handle partial failures and still report issues', async () => {
|
|
1630
|
+
mockFetch
|
|
1631
|
+
.mockResolvedValueOnce(mockResponse(mockServers))
|
|
1632
|
+
.mockRejectedValueOnce(new Error('Applications unavailable'))
|
|
1633
|
+
.mockResolvedValueOnce(mockResponse(mockDatabases))
|
|
1634
|
+
.mockResolvedValueOnce(mockResponse(mockServices));
|
|
1635
|
+
const result = await client.findInfrastructureIssues();
|
|
1636
|
+
expect(result.summary.unreachable_servers).toBe(1);
|
|
1637
|
+
expect(result.summary.unhealthy_databases).toBe(1);
|
|
1638
|
+
expect(result.summary.unhealthy_services).toBe(1);
|
|
1639
|
+
expect(result.summary.unhealthy_applications).toBe(0); // Failed to fetch
|
|
1640
|
+
expect(result.errors).toContain('applications: Applications unavailable');
|
|
1641
|
+
});
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1025
1644
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for diagnostic tools.
|
|
3
|
+
*
|
|
4
|
+
* These tests hit the real Coolify API to verify diagnostic methods work correctly.
|
|
5
|
+
* They are skipped in CI and should be run manually when testing against a real instance.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* - COOLIFY_URL and COOLIFY_TOKEN environment variables set (from .env)
|
|
9
|
+
* - Access to a running Coolify instance
|
|
10
|
+
*
|
|
11
|
+
* Run with: npm run test:integration
|
|
12
|
+
*/
|
|
13
|
+
export {};
|