@masonator/coolify-mcp 2.10.0 → 2.12.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 +27 -22
- package/dist/__tests__/coolify-client.test.js +637 -0
- package/dist/__tests__/mcp-server.test.js +71 -0
- package/dist/lib/coolify-client.d.ts +60 -1
- package/dist/lib/coolify-client.js +296 -0
- package/dist/lib/mcp-server.js +369 -20
- package/dist/types/coolify.d.ts +152 -0
- package/package.json +2 -2
|
@@ -3039,4 +3039,641 @@ describe('CoolifyClient', () => {
|
|
|
3039
3039
|
});
|
|
3040
3040
|
});
|
|
3041
3041
|
});
|
|
3042
|
+
// ===========================================================================
|
|
3043
|
+
// Application Storage endpoints
|
|
3044
|
+
// ===========================================================================
|
|
3045
|
+
describe('listApplicationStorages', () => {
|
|
3046
|
+
it('should list application storages', async () => {
|
|
3047
|
+
const mockData = { persistent_storages: [], file_storages: [] };
|
|
3048
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockData));
|
|
3049
|
+
const result = await client.listApplicationStorages('app-uuid');
|
|
3050
|
+
expect(result).toEqual(mockData);
|
|
3051
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/storages', expect.any(Object));
|
|
3052
|
+
});
|
|
3053
|
+
});
|
|
3054
|
+
describe('createApplicationStorage', () => {
|
|
3055
|
+
it('should create application storage', async () => {
|
|
3056
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Storage created.' }));
|
|
3057
|
+
const result = await client.createApplicationStorage('app-uuid', {
|
|
3058
|
+
type: 'persistent',
|
|
3059
|
+
mount_path: '/data',
|
|
3060
|
+
name: 'my-vol',
|
|
3061
|
+
});
|
|
3062
|
+
expect(result).toEqual({ message: 'Storage created.' });
|
|
3063
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/storages', expect.objectContaining({ method: 'POST' }));
|
|
3064
|
+
});
|
|
3065
|
+
});
|
|
3066
|
+
describe('updateApplicationStorage', () => {
|
|
3067
|
+
it('should update application storage', async () => {
|
|
3068
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Storage updated.' }));
|
|
3069
|
+
const result = await client.updateApplicationStorage('app-uuid', {
|
|
3070
|
+
uuid: 'stor-uuid',
|
|
3071
|
+
type: 'persistent',
|
|
3072
|
+
name: 'renamed',
|
|
3073
|
+
});
|
|
3074
|
+
expect(result).toEqual({ message: 'Storage updated.' });
|
|
3075
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/storages', expect.objectContaining({ method: 'PATCH' }));
|
|
3076
|
+
});
|
|
3077
|
+
});
|
|
3078
|
+
describe('deleteApplicationStorage', () => {
|
|
3079
|
+
it('should delete application storage', async () => {
|
|
3080
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Storage deleted.' }));
|
|
3081
|
+
const result = await client.deleteApplicationStorage('app-uuid', 'stor-uuid');
|
|
3082
|
+
expect(result).toEqual({ message: 'Storage deleted.' });
|
|
3083
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/storages/stor-uuid', expect.objectContaining({ method: 'DELETE' }));
|
|
3084
|
+
});
|
|
3085
|
+
});
|
|
3086
|
+
// ===========================================================================
|
|
3087
|
+
// Application Scheduled Task endpoints
|
|
3088
|
+
// ===========================================================================
|
|
3089
|
+
describe('listApplicationScheduledTasks', () => {
|
|
3090
|
+
it('should list scheduled tasks', async () => {
|
|
3091
|
+
const mockTasks = [
|
|
3092
|
+
{
|
|
3093
|
+
id: 1,
|
|
3094
|
+
uuid: 'task-1',
|
|
3095
|
+
name: 'backup',
|
|
3096
|
+
command: 'pg_dump',
|
|
3097
|
+
frequency: '0 * * * *',
|
|
3098
|
+
enabled: true,
|
|
3099
|
+
timeout: 300,
|
|
3100
|
+
created_at: '2024-01-01',
|
|
3101
|
+
updated_at: '2024-01-01',
|
|
3102
|
+
},
|
|
3103
|
+
];
|
|
3104
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockTasks));
|
|
3105
|
+
const result = await client.listApplicationScheduledTasks('app-uuid');
|
|
3106
|
+
expect(result).toEqual(mockTasks);
|
|
3107
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/scheduled-tasks', expect.any(Object));
|
|
3108
|
+
});
|
|
3109
|
+
});
|
|
3110
|
+
describe('createApplicationScheduledTask', () => {
|
|
3111
|
+
it('should create a scheduled task', async () => {
|
|
3112
|
+
const mockTask = {
|
|
3113
|
+
id: 1,
|
|
3114
|
+
uuid: 'task-1',
|
|
3115
|
+
name: 'backup',
|
|
3116
|
+
command: 'pg_dump',
|
|
3117
|
+
frequency: '0 * * * *',
|
|
3118
|
+
enabled: true,
|
|
3119
|
+
timeout: 300,
|
|
3120
|
+
created_at: '2024-01-01',
|
|
3121
|
+
updated_at: '2024-01-01',
|
|
3122
|
+
};
|
|
3123
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockTask));
|
|
3124
|
+
const result = await client.createApplicationScheduledTask('app-uuid', {
|
|
3125
|
+
name: 'backup',
|
|
3126
|
+
command: 'pg_dump',
|
|
3127
|
+
frequency: '0 * * * *',
|
|
3128
|
+
});
|
|
3129
|
+
expect(result).toEqual(mockTask);
|
|
3130
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/scheduled-tasks', expect.objectContaining({ method: 'POST' }));
|
|
3131
|
+
});
|
|
3132
|
+
});
|
|
3133
|
+
describe('updateApplicationScheduledTask', () => {
|
|
3134
|
+
it('should update a scheduled task', async () => {
|
|
3135
|
+
const mockTask = {
|
|
3136
|
+
id: 1,
|
|
3137
|
+
uuid: 'task-1',
|
|
3138
|
+
name: 'backup-v2',
|
|
3139
|
+
command: 'pg_dump -Fc',
|
|
3140
|
+
frequency: '0 * * * *',
|
|
3141
|
+
enabled: true,
|
|
3142
|
+
timeout: 300,
|
|
3143
|
+
created_at: '2024-01-01',
|
|
3144
|
+
updated_at: '2024-01-01',
|
|
3145
|
+
};
|
|
3146
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockTask));
|
|
3147
|
+
const result = await client.updateApplicationScheduledTask('app-uuid', 'task-1', {
|
|
3148
|
+
name: 'backup-v2',
|
|
3149
|
+
});
|
|
3150
|
+
expect(result).toEqual(mockTask);
|
|
3151
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/scheduled-tasks/task-1', expect.objectContaining({ method: 'PATCH' }));
|
|
3152
|
+
});
|
|
3153
|
+
});
|
|
3154
|
+
describe('deleteApplicationScheduledTask', () => {
|
|
3155
|
+
it('should delete a scheduled task', async () => {
|
|
3156
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Scheduled task deleted.' }));
|
|
3157
|
+
const result = await client.deleteApplicationScheduledTask('app-uuid', 'task-1');
|
|
3158
|
+
expect(result).toEqual({ message: 'Scheduled task deleted.' });
|
|
3159
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/scheduled-tasks/task-1', expect.objectContaining({ method: 'DELETE' }));
|
|
3160
|
+
});
|
|
3161
|
+
});
|
|
3162
|
+
describe('listApplicationScheduledTaskExecutions', () => {
|
|
3163
|
+
it('should list task executions', async () => {
|
|
3164
|
+
const mockExecs = [
|
|
3165
|
+
{
|
|
3166
|
+
uuid: 'exec-1',
|
|
3167
|
+
status: 'success',
|
|
3168
|
+
retry_count: 0,
|
|
3169
|
+
created_at: '2024-01-01',
|
|
3170
|
+
updated_at: '2024-01-01',
|
|
3171
|
+
},
|
|
3172
|
+
];
|
|
3173
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockExecs));
|
|
3174
|
+
const result = await client.listApplicationScheduledTaskExecutions('app-uuid', 'task-1');
|
|
3175
|
+
expect(result).toEqual(mockExecs);
|
|
3176
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/scheduled-tasks/task-1/executions', expect.any(Object));
|
|
3177
|
+
});
|
|
3178
|
+
});
|
|
3179
|
+
// ===========================================================================
|
|
3180
|
+
// Application Preview endpoint
|
|
3181
|
+
// ===========================================================================
|
|
3182
|
+
describe('deleteApplicationPreview', () => {
|
|
3183
|
+
it('should delete a preview deployment', async () => {
|
|
3184
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Preview deleted.' }));
|
|
3185
|
+
const result = await client.deleteApplicationPreview('app-uuid', 42);
|
|
3186
|
+
expect(result).toEqual({ message: 'Preview deleted.' });
|
|
3187
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/applications/app-uuid/previews/42', expect.objectContaining({ method: 'DELETE' }));
|
|
3188
|
+
});
|
|
3189
|
+
});
|
|
3190
|
+
// ===========================================================================
|
|
3191
|
+
// Database Environment Variable endpoints
|
|
3192
|
+
// ===========================================================================
|
|
3193
|
+
describe('listDatabaseEnvVars', () => {
|
|
3194
|
+
it('should list database env vars', async () => {
|
|
3195
|
+
const mockEnvs = [
|
|
3196
|
+
{
|
|
3197
|
+
id: 1,
|
|
3198
|
+
uuid: 'env-1',
|
|
3199
|
+
key: 'DB_HOST',
|
|
3200
|
+
value: 'localhost',
|
|
3201
|
+
is_buildtime: false,
|
|
3202
|
+
is_literal: false,
|
|
3203
|
+
is_multiline: false,
|
|
3204
|
+
is_preview: false,
|
|
3205
|
+
is_shared: false,
|
|
3206
|
+
is_shown_once: false,
|
|
3207
|
+
created_at: '2024-01-01',
|
|
3208
|
+
updated_at: '2024-01-01',
|
|
3209
|
+
},
|
|
3210
|
+
];
|
|
3211
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockEnvs));
|
|
3212
|
+
const result = await client.listDatabaseEnvVars('db-uuid');
|
|
3213
|
+
expect(result).toEqual(mockEnvs);
|
|
3214
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/envs', expect.any(Object));
|
|
3215
|
+
});
|
|
3216
|
+
});
|
|
3217
|
+
describe('createDatabaseEnvVar', () => {
|
|
3218
|
+
it('should create a database env var', async () => {
|
|
3219
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ uuid: 'env-1' }));
|
|
3220
|
+
const result = await client.createDatabaseEnvVar('db-uuid', {
|
|
3221
|
+
key: 'DB_HOST',
|
|
3222
|
+
value: 'localhost',
|
|
3223
|
+
});
|
|
3224
|
+
expect(result).toEqual({ uuid: 'env-1' });
|
|
3225
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/envs', expect.objectContaining({ method: 'POST' }));
|
|
3226
|
+
});
|
|
3227
|
+
});
|
|
3228
|
+
describe('updateDatabaseEnvVar', () => {
|
|
3229
|
+
it('should update a database env var', async () => {
|
|
3230
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated.' }));
|
|
3231
|
+
const result = await client.updateDatabaseEnvVar('db-uuid', {
|
|
3232
|
+
key: 'DB_HOST',
|
|
3233
|
+
value: '127.0.0.1',
|
|
3234
|
+
});
|
|
3235
|
+
expect(result).toEqual({ message: 'Updated.' });
|
|
3236
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/envs', expect.objectContaining({ method: 'PATCH' }));
|
|
3237
|
+
});
|
|
3238
|
+
});
|
|
3239
|
+
describe('bulkUpdateDatabaseEnvVars', () => {
|
|
3240
|
+
it('should bulk update database env vars', async () => {
|
|
3241
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Bulk updated.' }));
|
|
3242
|
+
const result = await client.bulkUpdateDatabaseEnvVars('db-uuid', {
|
|
3243
|
+
data: [{ key: 'A', value: '1' }],
|
|
3244
|
+
});
|
|
3245
|
+
expect(result).toEqual({ message: 'Bulk updated.' });
|
|
3246
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/envs/bulk', expect.objectContaining({ method: 'PATCH' }));
|
|
3247
|
+
});
|
|
3248
|
+
});
|
|
3249
|
+
describe('deleteDatabaseEnvVar', () => {
|
|
3250
|
+
it('should delete a database env var', async () => {
|
|
3251
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted.' }));
|
|
3252
|
+
const result = await client.deleteDatabaseEnvVar('db-uuid', 'env-uuid');
|
|
3253
|
+
expect(result).toEqual({ message: 'Deleted.' });
|
|
3254
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/envs/env-uuid', expect.objectContaining({ method: 'DELETE' }));
|
|
3255
|
+
});
|
|
3256
|
+
});
|
|
3257
|
+
// ===========================================================================
|
|
3258
|
+
// Database Storage endpoints
|
|
3259
|
+
// ===========================================================================
|
|
3260
|
+
describe('listDatabaseStorages', () => {
|
|
3261
|
+
it('should list database storages', async () => {
|
|
3262
|
+
const mockData = { persistent_storages: [], file_storages: [] };
|
|
3263
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockData));
|
|
3264
|
+
const result = await client.listDatabaseStorages('db-uuid');
|
|
3265
|
+
expect(result).toEqual(mockData);
|
|
3266
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/storages', expect.any(Object));
|
|
3267
|
+
});
|
|
3268
|
+
});
|
|
3269
|
+
describe('createDatabaseStorage', () => {
|
|
3270
|
+
it('should create database storage', async () => {
|
|
3271
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Created.' }));
|
|
3272
|
+
const result = await client.createDatabaseStorage('db-uuid', {
|
|
3273
|
+
type: 'persistent',
|
|
3274
|
+
mount_path: '/data',
|
|
3275
|
+
});
|
|
3276
|
+
expect(result).toEqual({ message: 'Created.' });
|
|
3277
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/storages', expect.objectContaining({ method: 'POST' }));
|
|
3278
|
+
});
|
|
3279
|
+
});
|
|
3280
|
+
describe('updateDatabaseStorage', () => {
|
|
3281
|
+
it('should update database storage', async () => {
|
|
3282
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated.' }));
|
|
3283
|
+
const result = await client.updateDatabaseStorage('db-uuid', {
|
|
3284
|
+
type: 'persistent',
|
|
3285
|
+
name: 'new-name',
|
|
3286
|
+
});
|
|
3287
|
+
expect(result).toEqual({ message: 'Updated.' });
|
|
3288
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/storages', expect.objectContaining({ method: 'PATCH' }));
|
|
3289
|
+
});
|
|
3290
|
+
});
|
|
3291
|
+
describe('deleteDatabaseStorage', () => {
|
|
3292
|
+
it('should delete database storage', async () => {
|
|
3293
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted.' }));
|
|
3294
|
+
const result = await client.deleteDatabaseStorage('db-uuid', 'stor-uuid');
|
|
3295
|
+
expect(result).toEqual({ message: 'Deleted.' });
|
|
3296
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/storages/stor-uuid', expect.objectContaining({ method: 'DELETE' }));
|
|
3297
|
+
});
|
|
3298
|
+
});
|
|
3299
|
+
// ===========================================================================
|
|
3300
|
+
// Delete Backup Execution endpoint
|
|
3301
|
+
// ===========================================================================
|
|
3302
|
+
describe('deleteBackupExecution', () => {
|
|
3303
|
+
it('should delete a backup execution', async () => {
|
|
3304
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted.' }));
|
|
3305
|
+
const result = await client.deleteBackupExecution('db-uuid', 'backup-uuid', 'exec-uuid');
|
|
3306
|
+
expect(result).toEqual({ message: 'Deleted.' });
|
|
3307
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/databases/db-uuid/backups/backup-uuid/executions/exec-uuid', expect.objectContaining({ method: 'DELETE' }));
|
|
3308
|
+
});
|
|
3309
|
+
});
|
|
3310
|
+
// ===========================================================================
|
|
3311
|
+
// Service Environment Variable bulk endpoint
|
|
3312
|
+
// ===========================================================================
|
|
3313
|
+
describe('bulkUpdateServiceEnvVars', () => {
|
|
3314
|
+
it('should bulk update service env vars', async () => {
|
|
3315
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Bulk updated.' }));
|
|
3316
|
+
const result = await client.bulkUpdateServiceEnvVars('svc-uuid', {
|
|
3317
|
+
data: [{ key: 'A', value: '1' }],
|
|
3318
|
+
});
|
|
3319
|
+
expect(result).toEqual({ message: 'Bulk updated.' });
|
|
3320
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/envs/bulk', expect.objectContaining({ method: 'PATCH' }));
|
|
3321
|
+
});
|
|
3322
|
+
});
|
|
3323
|
+
// ===========================================================================
|
|
3324
|
+
// Service Storage endpoints
|
|
3325
|
+
// ===========================================================================
|
|
3326
|
+
describe('listServiceStorages', () => {
|
|
3327
|
+
it('should list service storages', async () => {
|
|
3328
|
+
const mockData = { persistent_storages: [], file_storages: [] };
|
|
3329
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockData));
|
|
3330
|
+
const result = await client.listServiceStorages('svc-uuid');
|
|
3331
|
+
expect(result).toEqual(mockData);
|
|
3332
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/storages', expect.any(Object));
|
|
3333
|
+
});
|
|
3334
|
+
});
|
|
3335
|
+
describe('createServiceStorage', () => {
|
|
3336
|
+
it('should create service storage', async () => {
|
|
3337
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Created.' }));
|
|
3338
|
+
const result = await client.createServiceStorage('svc-uuid', {
|
|
3339
|
+
type: 'file',
|
|
3340
|
+
mount_path: '/config',
|
|
3341
|
+
});
|
|
3342
|
+
expect(result).toEqual({ message: 'Created.' });
|
|
3343
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/storages', expect.objectContaining({ method: 'POST' }));
|
|
3344
|
+
});
|
|
3345
|
+
});
|
|
3346
|
+
describe('updateServiceStorage', () => {
|
|
3347
|
+
it('should update service storage', async () => {
|
|
3348
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Updated.' }));
|
|
3349
|
+
const result = await client.updateServiceStorage('svc-uuid', {
|
|
3350
|
+
type: 'file',
|
|
3351
|
+
content: 'new content',
|
|
3352
|
+
});
|
|
3353
|
+
expect(result).toEqual({ message: 'Updated.' });
|
|
3354
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/storages', expect.objectContaining({ method: 'PATCH' }));
|
|
3355
|
+
});
|
|
3356
|
+
});
|
|
3357
|
+
describe('deleteServiceStorage', () => {
|
|
3358
|
+
it('should delete service storage', async () => {
|
|
3359
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted.' }));
|
|
3360
|
+
const result = await client.deleteServiceStorage('svc-uuid', 'stor-uuid');
|
|
3361
|
+
expect(result).toEqual({ message: 'Deleted.' });
|
|
3362
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/storages/stor-uuid', expect.objectContaining({ method: 'DELETE' }));
|
|
3363
|
+
});
|
|
3364
|
+
});
|
|
3365
|
+
// ===========================================================================
|
|
3366
|
+
// Service Scheduled Task endpoints
|
|
3367
|
+
// ===========================================================================
|
|
3368
|
+
describe('listServiceScheduledTasks', () => {
|
|
3369
|
+
it('should list service scheduled tasks', async () => {
|
|
3370
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3371
|
+
const result = await client.listServiceScheduledTasks('svc-uuid');
|
|
3372
|
+
expect(result).toEqual([]);
|
|
3373
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/scheduled-tasks', expect.any(Object));
|
|
3374
|
+
});
|
|
3375
|
+
});
|
|
3376
|
+
describe('createServiceScheduledTask', () => {
|
|
3377
|
+
it('should create a service scheduled task', async () => {
|
|
3378
|
+
const mockTask = {
|
|
3379
|
+
id: 1,
|
|
3380
|
+
uuid: 'task-1',
|
|
3381
|
+
name: 'cleanup',
|
|
3382
|
+
command: 'rm -rf /tmp/*',
|
|
3383
|
+
frequency: '0 0 * * *',
|
|
3384
|
+
enabled: true,
|
|
3385
|
+
timeout: 300,
|
|
3386
|
+
created_at: '2024-01-01',
|
|
3387
|
+
updated_at: '2024-01-01',
|
|
3388
|
+
};
|
|
3389
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockTask));
|
|
3390
|
+
const result = await client.createServiceScheduledTask('svc-uuid', {
|
|
3391
|
+
name: 'cleanup',
|
|
3392
|
+
command: 'rm -rf /tmp/*',
|
|
3393
|
+
frequency: '0 0 * * *',
|
|
3394
|
+
});
|
|
3395
|
+
expect(result).toEqual(mockTask);
|
|
3396
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/scheduled-tasks', expect.objectContaining({ method: 'POST' }));
|
|
3397
|
+
});
|
|
3398
|
+
});
|
|
3399
|
+
describe('updateServiceScheduledTask', () => {
|
|
3400
|
+
it('should update a service scheduled task', async () => {
|
|
3401
|
+
const mockTask = {
|
|
3402
|
+
id: 1,
|
|
3403
|
+
uuid: 'task-1',
|
|
3404
|
+
name: 'cleanup-v2',
|
|
3405
|
+
command: 'rm -rf /tmp/*',
|
|
3406
|
+
frequency: '0 0 * * *',
|
|
3407
|
+
enabled: true,
|
|
3408
|
+
timeout: 600,
|
|
3409
|
+
created_at: '2024-01-01',
|
|
3410
|
+
updated_at: '2024-01-01',
|
|
3411
|
+
};
|
|
3412
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockTask));
|
|
3413
|
+
const result = await client.updateServiceScheduledTask('svc-uuid', 'task-1', {
|
|
3414
|
+
timeout: 600,
|
|
3415
|
+
});
|
|
3416
|
+
expect(result).toEqual(mockTask);
|
|
3417
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/scheduled-tasks/task-1', expect.objectContaining({ method: 'PATCH' }));
|
|
3418
|
+
});
|
|
3419
|
+
});
|
|
3420
|
+
describe('deleteServiceScheduledTask', () => {
|
|
3421
|
+
it('should delete a service scheduled task', async () => {
|
|
3422
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'Deleted.' }));
|
|
3423
|
+
const result = await client.deleteServiceScheduledTask('svc-uuid', 'task-1');
|
|
3424
|
+
expect(result).toEqual({ message: 'Deleted.' });
|
|
3425
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/scheduled-tasks/task-1', expect.objectContaining({ method: 'DELETE' }));
|
|
3426
|
+
});
|
|
3427
|
+
});
|
|
3428
|
+
describe('listServiceScheduledTaskExecutions', () => {
|
|
3429
|
+
it('should list service task executions', async () => {
|
|
3430
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3431
|
+
const result = await client.listServiceScheduledTaskExecutions('svc-uuid', 'task-1');
|
|
3432
|
+
expect(result).toEqual([]);
|
|
3433
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/services/svc-uuid/scheduled-tasks/task-1/executions', expect.any(Object));
|
|
3434
|
+
});
|
|
3435
|
+
});
|
|
3436
|
+
// ===========================================================================
|
|
3437
|
+
// Hetzner Cloud endpoints
|
|
3438
|
+
// ===========================================================================
|
|
3439
|
+
describe('listHetznerLocations', () => {
|
|
3440
|
+
it('should list Hetzner locations', async () => {
|
|
3441
|
+
const mockLocs = [
|
|
3442
|
+
{
|
|
3443
|
+
id: 1,
|
|
3444
|
+
name: 'nbg1',
|
|
3445
|
+
description: 'Nuremberg',
|
|
3446
|
+
country: 'DE',
|
|
3447
|
+
city: 'Nuremberg',
|
|
3448
|
+
latitude: 49.45,
|
|
3449
|
+
longitude: 11.08,
|
|
3450
|
+
},
|
|
3451
|
+
];
|
|
3452
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockLocs));
|
|
3453
|
+
const result = await client.listHetznerLocations('token-uuid');
|
|
3454
|
+
expect(result).toEqual(mockLocs);
|
|
3455
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/hetzner/locations?cloud_provider_token_uuid=token-uuid', expect.any(Object));
|
|
3456
|
+
});
|
|
3457
|
+
});
|
|
3458
|
+
describe('listHetznerServerTypes', () => {
|
|
3459
|
+
it('should list Hetzner server types', async () => {
|
|
3460
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3461
|
+
const result = await client.listHetznerServerTypes('token-uuid');
|
|
3462
|
+
expect(result).toEqual([]);
|
|
3463
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/hetzner/server-types?cloud_provider_token_uuid=token-uuid', expect.any(Object));
|
|
3464
|
+
});
|
|
3465
|
+
});
|
|
3466
|
+
describe('listHetznerImages', () => {
|
|
3467
|
+
it('should list Hetzner images', async () => {
|
|
3468
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3469
|
+
const result = await client.listHetznerImages('token-uuid');
|
|
3470
|
+
expect(result).toEqual([]);
|
|
3471
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/hetzner/images?cloud_provider_token_uuid=token-uuid', expect.any(Object));
|
|
3472
|
+
});
|
|
3473
|
+
});
|
|
3474
|
+
describe('listHetznerSSHKeys', () => {
|
|
3475
|
+
it('should list Hetzner SSH keys', async () => {
|
|
3476
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3477
|
+
const result = await client.listHetznerSSHKeys('token-uuid');
|
|
3478
|
+
expect(result).toEqual([]);
|
|
3479
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/hetzner/ssh-keys?cloud_provider_token_uuid=token-uuid', expect.any(Object));
|
|
3480
|
+
});
|
|
3481
|
+
});
|
|
3482
|
+
describe('createHetznerServer', () => {
|
|
3483
|
+
it('should create a Hetzner server', async () => {
|
|
3484
|
+
const mockResp = { uuid: 'srv-uuid', hetzner_server_id: 12345, ip: '1.2.3.4' };
|
|
3485
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockResp));
|
|
3486
|
+
const result = await client.createHetznerServer({
|
|
3487
|
+
location: 'nbg1',
|
|
3488
|
+
server_type: 'cx11',
|
|
3489
|
+
image: 15512617,
|
|
3490
|
+
private_key_uuid: 'key-uuid',
|
|
3491
|
+
});
|
|
3492
|
+
expect(result).toEqual(mockResp);
|
|
3493
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/servers/hetzner', expect.objectContaining({ method: 'POST' }));
|
|
3494
|
+
});
|
|
3495
|
+
});
|
|
3496
|
+
// ===========================================================================
|
|
3497
|
+
// GitHub App Repository endpoints
|
|
3498
|
+
// ===========================================================================
|
|
3499
|
+
describe('listGitHubAppRepositories', () => {
|
|
3500
|
+
it('should list GitHub App repositories', async () => {
|
|
3501
|
+
const mockRepos = [
|
|
3502
|
+
{
|
|
3503
|
+
id: 1,
|
|
3504
|
+
name: 'my-repo',
|
|
3505
|
+
full_name: 'org/my-repo',
|
|
3506
|
+
private: true,
|
|
3507
|
+
html_url: 'https://github.com/org/my-repo',
|
|
3508
|
+
default_branch: 'main',
|
|
3509
|
+
},
|
|
3510
|
+
];
|
|
3511
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ repositories: mockRepos }));
|
|
3512
|
+
const result = await client.listGitHubAppRepositories(123);
|
|
3513
|
+
expect(result).toEqual(mockRepos);
|
|
3514
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps/123/repositories', expect.any(Object));
|
|
3515
|
+
});
|
|
3516
|
+
it('should return empty array when no repositories', async () => {
|
|
3517
|
+
mockFetch.mockResolvedValueOnce(mockResponse({}));
|
|
3518
|
+
const result = await client.listGitHubAppRepositories(123);
|
|
3519
|
+
expect(result).toEqual([]);
|
|
3520
|
+
});
|
|
3521
|
+
});
|
|
3522
|
+
describe('listGitHubAppBranches', () => {
|
|
3523
|
+
it('should list branches for a repo', async () => {
|
|
3524
|
+
const mockBranches = [{ name: 'main' }, { name: 'develop' }];
|
|
3525
|
+
mockFetch.mockResolvedValueOnce(mockResponse(mockBranches));
|
|
3526
|
+
const result = await client.listGitHubAppBranches(123, 'org', 'my-repo');
|
|
3527
|
+
expect(result).toEqual(mockBranches);
|
|
3528
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/github-apps/123/repositories/org/my-repo/branches', expect.any(Object));
|
|
3529
|
+
});
|
|
3530
|
+
});
|
|
3531
|
+
// ===========================================================================
|
|
3532
|
+
// Resources endpoint
|
|
3533
|
+
// ===========================================================================
|
|
3534
|
+
describe('listResources', () => {
|
|
3535
|
+
const fluffyResource = {
|
|
3536
|
+
uuid: 'r1',
|
|
3537
|
+
name: 'my-app',
|
|
3538
|
+
type: 'application',
|
|
3539
|
+
status: 'running:healthy',
|
|
3540
|
+
// ---- fluff that the essential projection should drop ----
|
|
3541
|
+
id: 6,
|
|
3542
|
+
config_hash: 'd1b84af30038c5902cced139b19e5f6f',
|
|
3543
|
+
build_pack: 'dockerfile',
|
|
3544
|
+
health_check_path: '/',
|
|
3545
|
+
ports_exposes: '3500',
|
|
3546
|
+
dockerfile_location: '/Dockerfile',
|
|
3547
|
+
custom_labels: 'dHJhZWZpa...',
|
|
3548
|
+
docker_compose: null,
|
|
3549
|
+
};
|
|
3550
|
+
it('returns essential projection by default (uuid/name/type/status only)', async () => {
|
|
3551
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fluffyResource]));
|
|
3552
|
+
const result = await client.listResources();
|
|
3553
|
+
expect(result).toEqual([
|
|
3554
|
+
{ uuid: 'r1', name: 'my-app', type: 'application', status: 'running:healthy' },
|
|
3555
|
+
]);
|
|
3556
|
+
const [first] = result;
|
|
3557
|
+
expect(Object.keys(first).sort()).toEqual(['name', 'status', 'type', 'uuid']);
|
|
3558
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/resources', expect.any(Object));
|
|
3559
|
+
});
|
|
3560
|
+
it('omits status when the resource has none', async () => {
|
|
3561
|
+
const noStatus = { ...fluffyResource };
|
|
3562
|
+
delete noStatus.status;
|
|
3563
|
+
mockFetch.mockResolvedValueOnce(mockResponse([noStatus]));
|
|
3564
|
+
const result = await client.listResources();
|
|
3565
|
+
expect(result).toEqual([{ uuid: 'r1', name: 'my-app', type: 'application' }]);
|
|
3566
|
+
expect('status' in result[0]).toBe(false);
|
|
3567
|
+
});
|
|
3568
|
+
it('returns the raw Coolify payload when include_full is true', async () => {
|
|
3569
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fluffyResource]));
|
|
3570
|
+
const result = await client.listResources({ include_full: true });
|
|
3571
|
+
expect(result).toEqual([fluffyResource]);
|
|
3572
|
+
const [first] = result;
|
|
3573
|
+
expect(first.config_hash).toBe(fluffyResource.config_hash);
|
|
3574
|
+
expect(first.custom_labels).toBe(fluffyResource.custom_labels);
|
|
3575
|
+
});
|
|
3576
|
+
it('treats include_full=false as the default essential projection', async () => {
|
|
3577
|
+
mockFetch.mockResolvedValueOnce(mockResponse([fluffyResource]));
|
|
3578
|
+
const result = await client.listResources({ include_full: false });
|
|
3579
|
+
expect(Object.keys(result[0]).sort()).toEqual(['name', 'status', 'type', 'uuid']);
|
|
3580
|
+
});
|
|
3581
|
+
it('returns [] on an empty payload (default projection)', async () => {
|
|
3582
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3583
|
+
const result = await client.listResources();
|
|
3584
|
+
expect(result).toEqual([]);
|
|
3585
|
+
});
|
|
3586
|
+
it('returns [] on an empty payload (include_full=true, mask path)', async () => {
|
|
3587
|
+
mockFetch.mockResolvedValueOnce(mockResponse([]));
|
|
3588
|
+
const result = await client.listResources({ include_full: true });
|
|
3589
|
+
expect(result).toEqual([]);
|
|
3590
|
+
});
|
|
3591
|
+
it('drops status when Coolify returns a non-string shape (defensive)', async () => {
|
|
3592
|
+
// Hypothetical future Coolify divergence: status emitted as an object/null.
|
|
3593
|
+
// The essential projection must not propagate a non-string into a `string`-typed field.
|
|
3594
|
+
const objStatus = { ...fluffyResource, status: { state: 'running', healthy: true } };
|
|
3595
|
+
mockFetch.mockResolvedValueOnce(mockResponse([objStatus]));
|
|
3596
|
+
const result = await client.listResources();
|
|
3597
|
+
expect(result).toEqual([{ uuid: 'r1', name: 'my-app', type: 'application' }]);
|
|
3598
|
+
expect('status' in result[0]).toBe(false);
|
|
3599
|
+
});
|
|
3600
|
+
describe('sensitive-field masking on include_full', () => {
|
|
3601
|
+
const sensitiveResource = {
|
|
3602
|
+
...fluffyResource,
|
|
3603
|
+
manual_webhook_secret_github: 'real-github-secret',
|
|
3604
|
+
manual_webhook_secret_gitlab: 'real-gitlab-secret',
|
|
3605
|
+
manual_webhook_secret_gitea: 'real-gitea-secret',
|
|
3606
|
+
manual_webhook_secret_bitbucket: 'real-bitbucket-secret',
|
|
3607
|
+
http_basic_auth_password: 'real-basic-auth-pwd',
|
|
3608
|
+
};
|
|
3609
|
+
it('masks webhook secrets and basic-auth password by default on include_full=true', async () => {
|
|
3610
|
+
mockFetch.mockResolvedValueOnce(mockResponse([sensitiveResource]));
|
|
3611
|
+
const [first] = (await client.listResources({ include_full: true }));
|
|
3612
|
+
expect(first.manual_webhook_secret_github).toBe('***');
|
|
3613
|
+
expect(first.manual_webhook_secret_gitlab).toBe('***');
|
|
3614
|
+
expect(first.manual_webhook_secret_gitea).toBe('***');
|
|
3615
|
+
expect(first.manual_webhook_secret_bitbucket).toBe('***');
|
|
3616
|
+
expect(first.http_basic_auth_password).toBe('***');
|
|
3617
|
+
expect(first.config_hash).toBe(sensitiveResource.config_hash);
|
|
3618
|
+
});
|
|
3619
|
+
it('round-trips plaintext when reveal=true', async () => {
|
|
3620
|
+
mockFetch.mockResolvedValueOnce(mockResponse([sensitiveResource]));
|
|
3621
|
+
const [first] = (await client.listResources({
|
|
3622
|
+
include_full: true,
|
|
3623
|
+
reveal: true,
|
|
3624
|
+
}));
|
|
3625
|
+
expect(first.manual_webhook_secret_github).toBe('real-github-secret');
|
|
3626
|
+
expect(first.http_basic_auth_password).toBe('real-basic-auth-pwd');
|
|
3627
|
+
});
|
|
3628
|
+
it('leaves null secret values untouched (Coolify uses null when unset)', async () => {
|
|
3629
|
+
const noSecrets = {
|
|
3630
|
+
...fluffyResource,
|
|
3631
|
+
manual_webhook_secret_github: null,
|
|
3632
|
+
manual_webhook_secret_gitlab: null,
|
|
3633
|
+
manual_webhook_secret_gitea: null,
|
|
3634
|
+
manual_webhook_secret_bitbucket: null,
|
|
3635
|
+
http_basic_auth_password: null,
|
|
3636
|
+
};
|
|
3637
|
+
mockFetch.mockResolvedValueOnce(mockResponse([noSecrets]));
|
|
3638
|
+
const [first] = (await client.listResources({ include_full: true }));
|
|
3639
|
+
expect(first.manual_webhook_secret_github).toBeNull();
|
|
3640
|
+
expect(first.http_basic_auth_password).toBeNull();
|
|
3641
|
+
});
|
|
3642
|
+
it('reveal=true on the default projection is a no-op (no fluff to reveal)', async () => {
|
|
3643
|
+
mockFetch.mockResolvedValueOnce(mockResponse([sensitiveResource]));
|
|
3644
|
+
const result = await client.listResources({ reveal: true });
|
|
3645
|
+
expect(Object.keys(result[0]).sort()).toEqual(['name', 'status', 'type', 'uuid']);
|
|
3646
|
+
});
|
|
3647
|
+
});
|
|
3648
|
+
});
|
|
3649
|
+
// ===========================================================================
|
|
3650
|
+
// Health endpoint
|
|
3651
|
+
// ===========================================================================
|
|
3652
|
+
describe('getHealth', () => {
|
|
3653
|
+
it('should check API health', async () => {
|
|
3654
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'OK' }));
|
|
3655
|
+
const result = await client.getHealth();
|
|
3656
|
+
expect(result).toEqual({ message: 'OK' });
|
|
3657
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/health', expect.any(Object));
|
|
3658
|
+
});
|
|
3659
|
+
});
|
|
3660
|
+
// ===========================================================================
|
|
3661
|
+
// API Enable/Disable endpoints
|
|
3662
|
+
// ===========================================================================
|
|
3663
|
+
describe('enableApi', () => {
|
|
3664
|
+
it('should enable the API', async () => {
|
|
3665
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'API enabled.' }));
|
|
3666
|
+
const result = await client.enableApi();
|
|
3667
|
+
expect(result).toEqual({ message: 'API enabled.' });
|
|
3668
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/enable', expect.objectContaining({ method: 'GET' }));
|
|
3669
|
+
});
|
|
3670
|
+
});
|
|
3671
|
+
describe('disableApi', () => {
|
|
3672
|
+
it('should disable the API', async () => {
|
|
3673
|
+
mockFetch.mockResolvedValueOnce(mockResponse({ message: 'API disabled.' }));
|
|
3674
|
+
const result = await client.disableApi();
|
|
3675
|
+
expect(result).toEqual({ message: 'API disabled.' });
|
|
3676
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:3000/api/v1/disable', expect.objectContaining({ method: 'GET' }));
|
|
3677
|
+
});
|
|
3678
|
+
});
|
|
3042
3679
|
});
|