@geekmidas/cli 0.13.0 → 0.15.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/dist/{bundler-DskIqW2t.mjs → bundler-D7cM_FWw.mjs} +34 -10
- package/dist/bundler-D7cM_FWw.mjs.map +1 -0
- package/dist/{bundler-B1qy9b-j.cjs → bundler-Nuew7Xcn.cjs} +33 -9
- package/dist/bundler-Nuew7Xcn.cjs.map +1 -0
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/dokploy-api-B7KxOQr3.cjs +3 -0
- package/dist/dokploy-api-C7F9VykY.cjs +317 -0
- package/dist/dokploy-api-C7F9VykY.cjs.map +1 -0
- package/dist/dokploy-api-CaETb2L6.mjs +305 -0
- package/dist/dokploy-api-CaETb2L6.mjs.map +1 -0
- package/dist/dokploy-api-DHvfmWbi.mjs +3 -0
- package/dist/{encryption-Dyf_r1h-.cjs → encryption-D7Efcdi9.cjs} +1 -1
- package/dist/{encryption-Dyf_r1h-.cjs.map → encryption-D7Efcdi9.cjs.map} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs → encryption-h4Nb6W-M.mjs} +1 -1
- package/dist/{encryption-C8H-38Yy.mjs.map → encryption-h4Nb6W-M.mjs.map} +1 -1
- package/dist/index.cjs +1508 -1073
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1508 -1073
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-Bt_1FDpT.cjs → openapi-C89hhkZC.cjs} +3 -3
- package/dist/{openapi-Bt_1FDpT.cjs.map → openapi-C89hhkZC.cjs.map} +1 -1
- package/dist/{openapi-BfFlOBCG.mjs → openapi-CZVcfxk-.mjs} +3 -3
- package/dist/{openapi-BfFlOBCG.mjs.map → openapi-CZVcfxk-.mjs.map} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs → openapi-react-query-CM2_qlW9.mjs} +1 -1
- package/dist/{openapi-react-query-B6XTeGqS.mjs.map → openapi-react-query-CM2_qlW9.mjs.map} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs → openapi-react-query-iKjfLzff.cjs} +1 -1
- package/dist/{openapi-react-query-B-sNWHFU.cjs.map → openapi-react-query-iKjfLzff.cjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +1 -1
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +1 -1
- package/dist/{storage-kSxTjkNb.mjs → storage-BaOP55oq.mjs} +16 -2
- package/dist/storage-BaOP55oq.mjs.map +1 -0
- package/dist/{storage-Bj1E26lU.cjs → storage-Bn3K9Ccu.cjs} +21 -1
- package/dist/storage-Bn3K9Ccu.cjs.map +1 -0
- package/dist/storage-UfyTn7Zm.cjs +7 -0
- package/dist/storage-nkGIjeXt.mjs +3 -0
- package/dist/{types-BhkZc-vm.d.cts → types-BgaMXsUa.d.cts} +3 -1
- package/dist/{types-BR0M2v_c.d.mts.map → types-BgaMXsUa.d.cts.map} +1 -1
- package/dist/{types-BR0M2v_c.d.mts → types-iFk5ms7y.d.mts} +3 -1
- package/dist/{types-BhkZc-vm.d.cts.map → types-iFk5ms7y.d.mts.map} +1 -1
- package/package.json +4 -4
- package/src/auth/__tests__/credentials.spec.ts +127 -0
- package/src/auth/__tests__/index.spec.ts +69 -0
- package/src/auth/credentials.ts +33 -0
- package/src/auth/index.ts +57 -50
- package/src/build/__tests__/bundler.spec.ts +5 -4
- package/src/build/__tests__/endpoint-analyzer.spec.ts +623 -0
- package/src/build/__tests__/handler-templates.spec.ts +272 -0
- package/src/build/bundler.ts +61 -8
- package/src/build/index.ts +21 -0
- package/src/build/types.ts +6 -0
- package/src/deploy/__tests__/docker.spec.ts +44 -6
- package/src/deploy/__tests__/dokploy-api.spec.ts +698 -0
- package/src/deploy/__tests__/dokploy.spec.ts +196 -6
- package/src/deploy/__tests__/index.spec.ts +401 -0
- package/src/deploy/__tests__/init.spec.ts +147 -16
- package/src/deploy/docker.ts +109 -5
- package/src/deploy/dokploy-api.ts +581 -0
- package/src/deploy/dokploy.ts +66 -93
- package/src/deploy/index.ts +630 -32
- package/src/deploy/init.ts +192 -249
- package/src/deploy/types.ts +24 -2
- package/src/dev/__tests__/index.spec.ts +95 -0
- package/src/docker/__tests__/templates.spec.ts +144 -0
- package/src/docker/index.ts +96 -6
- package/src/docker/templates.ts +114 -27
- package/src/generators/EndpointGenerator.ts +2 -2
- package/src/index.ts +34 -13
- package/src/secrets/storage.ts +15 -0
- package/src/types.ts +2 -0
- package/dist/bundler-B1qy9b-j.cjs.map +0 -1
- package/dist/bundler-DskIqW2t.mjs.map +0 -1
- package/dist/storage-BOOpAF8N.cjs +0 -5
- package/dist/storage-Bj1E26lU.cjs.map +0 -1
- package/dist/storage-kSxTjkNb.mjs.map +0 -1
- package/dist/storage-tgZSUnKl.mjs +0 -3
|
@@ -2,11 +2,13 @@ import { HttpResponse, http } from 'msw';
|
|
|
2
2
|
import { setupServer } from 'msw/node';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { deployDokploy, validateDokployConfig } from '../dokploy';
|
|
5
|
+
import { generateTag } from '../index';
|
|
5
6
|
import type { DokployDeployConfig } from '../types';
|
|
6
7
|
|
|
7
|
-
// Mock
|
|
8
|
+
// Mock auth functions
|
|
8
9
|
vi.mock('../../auth', () => ({
|
|
9
10
|
getDokployToken: vi.fn().mockResolvedValue('test-api-token'),
|
|
11
|
+
getDokployRegistryId: vi.fn().mockResolvedValue(null),
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
14
|
// MSW server for mocking Dokploy API
|
|
@@ -112,7 +114,17 @@ describe('deployDokploy', () => {
|
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
it('should deploy successfully without master key', async () => {
|
|
117
|
+
const saveDockerCalls: unknown[] = [];
|
|
118
|
+
|
|
115
119
|
server.use(
|
|
120
|
+
http.post(
|
|
121
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
122
|
+
async ({ request }) => {
|
|
123
|
+
const body = await request.json();
|
|
124
|
+
saveDockerCalls.push(body);
|
|
125
|
+
return HttpResponse.json({ success: true });
|
|
126
|
+
},
|
|
127
|
+
),
|
|
116
128
|
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
117
129
|
return HttpResponse.json({ success: true });
|
|
118
130
|
}),
|
|
@@ -132,17 +144,28 @@ describe('deployDokploy', () => {
|
|
|
132
144
|
expect(result.imageRef).toBe('ghcr.io/myorg/app:v1.0.0');
|
|
133
145
|
expect(result.masterKey).toBeUndefined();
|
|
134
146
|
expect(result.url).toBe('https://dokploy.example.com/project/proj_123');
|
|
147
|
+
expect(saveDockerCalls).toHaveLength(1);
|
|
148
|
+
expect(saveDockerCalls[0]).toMatchObject({
|
|
149
|
+
applicationId: 'app_456',
|
|
150
|
+
dockerImage: 'ghcr.io/myorg/app:v1.0.0',
|
|
151
|
+
});
|
|
135
152
|
});
|
|
136
153
|
|
|
137
154
|
it('should deploy with master key and update environment', async () => {
|
|
138
|
-
const
|
|
155
|
+
const envCalls: unknown[] = [];
|
|
139
156
|
|
|
140
157
|
server.use(
|
|
141
158
|
http.post(
|
|
142
|
-
'https://dokploy.example.com/api/application.
|
|
159
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
160
|
+
() => {
|
|
161
|
+
return HttpResponse.json({ success: true });
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
http.post(
|
|
165
|
+
'https://dokploy.example.com/api/application.saveEnvironment',
|
|
143
166
|
async ({ request }) => {
|
|
144
167
|
const body = await request.json();
|
|
145
|
-
|
|
168
|
+
envCalls.push(body);
|
|
146
169
|
return HttpResponse.json({ success: true });
|
|
147
170
|
},
|
|
148
171
|
),
|
|
@@ -164,8 +187,8 @@ describe('deployDokploy', () => {
|
|
|
164
187
|
});
|
|
165
188
|
|
|
166
189
|
expect(result.masterKey).toBe('secret-master-key');
|
|
167
|
-
expect(
|
|
168
|
-
expect(
|
|
190
|
+
expect(envCalls).toHaveLength(1);
|
|
191
|
+
expect(envCalls[0]).toMatchObject({
|
|
169
192
|
applicationId: 'app_456',
|
|
170
193
|
env: 'GKM_MASTER_KEY=secret-master-key',
|
|
171
194
|
});
|
|
@@ -173,6 +196,12 @@ describe('deployDokploy', () => {
|
|
|
173
196
|
|
|
174
197
|
it('should handle API error with message', async () => {
|
|
175
198
|
server.use(
|
|
199
|
+
http.post(
|
|
200
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
201
|
+
() => {
|
|
202
|
+
return HttpResponse.json({ success: true });
|
|
203
|
+
},
|
|
204
|
+
),
|
|
176
205
|
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
177
206
|
return HttpResponse.json(
|
|
178
207
|
{ message: 'Application not found' },
|
|
@@ -197,6 +226,12 @@ describe('deployDokploy', () => {
|
|
|
197
226
|
|
|
198
227
|
it('should handle API error with issues', async () => {
|
|
199
228
|
server.use(
|
|
229
|
+
http.post(
|
|
230
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
231
|
+
() => {
|
|
232
|
+
return HttpResponse.json({ success: true });
|
|
233
|
+
},
|
|
234
|
+
),
|
|
200
235
|
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
201
236
|
return HttpResponse.json(
|
|
202
237
|
{
|
|
@@ -224,6 +259,12 @@ describe('deployDokploy', () => {
|
|
|
224
259
|
|
|
225
260
|
it('should handle API error without JSON body', async () => {
|
|
226
261
|
server.use(
|
|
262
|
+
http.post(
|
|
263
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
264
|
+
() => {
|
|
265
|
+
return HttpResponse.json({ success: true });
|
|
266
|
+
},
|
|
267
|
+
),
|
|
227
268
|
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
228
269
|
return new HttpResponse('Internal Server Error', { status: 500 });
|
|
229
270
|
}),
|
|
@@ -242,4 +283,153 @@ describe('deployDokploy', () => {
|
|
|
242
283
|
}),
|
|
243
284
|
).rejects.toThrow('Dokploy API error: 500');
|
|
244
285
|
});
|
|
286
|
+
|
|
287
|
+
it('should use registryId from config', async () => {
|
|
288
|
+
const saveDockerCalls: unknown[] = [];
|
|
289
|
+
|
|
290
|
+
server.use(
|
|
291
|
+
http.post(
|
|
292
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
293
|
+
async ({ request }) => {
|
|
294
|
+
const body = await request.json();
|
|
295
|
+
saveDockerCalls.push(body);
|
|
296
|
+
return HttpResponse.json({ success: true });
|
|
297
|
+
},
|
|
298
|
+
),
|
|
299
|
+
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
300
|
+
return HttpResponse.json({ success: true });
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
await deployDokploy({
|
|
305
|
+
stage: 'production',
|
|
306
|
+
tag: 'v1.0.0',
|
|
307
|
+
imageRef: 'ghcr.io/myorg/app:v1.0.0',
|
|
308
|
+
config: {
|
|
309
|
+
endpoint: 'https://dokploy.example.com',
|
|
310
|
+
projectId: 'proj_123',
|
|
311
|
+
applicationId: 'app_456',
|
|
312
|
+
registryId: 'reg_789',
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(saveDockerCalls).toHaveLength(1);
|
|
317
|
+
expect(saveDockerCalls[0]).toMatchObject({
|
|
318
|
+
applicationId: 'app_456',
|
|
319
|
+
dockerImage: 'ghcr.io/myorg/app:v1.0.0',
|
|
320
|
+
registryId: 'reg_789',
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should use stored registryId when not in config', async () => {
|
|
325
|
+
const { getDokployRegistryId } = await import('../../auth');
|
|
326
|
+
vi.mocked(getDokployRegistryId).mockResolvedValueOnce('stored_reg_123');
|
|
327
|
+
|
|
328
|
+
const saveDockerCalls: unknown[] = [];
|
|
329
|
+
|
|
330
|
+
server.use(
|
|
331
|
+
http.post(
|
|
332
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
333
|
+
async ({ request }) => {
|
|
334
|
+
const body = await request.json();
|
|
335
|
+
saveDockerCalls.push(body);
|
|
336
|
+
return HttpResponse.json({ success: true });
|
|
337
|
+
},
|
|
338
|
+
),
|
|
339
|
+
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
340
|
+
return HttpResponse.json({ success: true });
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
await deployDokploy({
|
|
345
|
+
stage: 'production',
|
|
346
|
+
tag: 'v1.0.0',
|
|
347
|
+
imageRef: 'ghcr.io/myorg/app:v1.0.0',
|
|
348
|
+
config: {
|
|
349
|
+
endpoint: 'https://dokploy.example.com',
|
|
350
|
+
projectId: 'proj_123',
|
|
351
|
+
applicationId: 'app_456',
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
expect(saveDockerCalls).toHaveLength(1);
|
|
356
|
+
expect(saveDockerCalls[0]).toMatchObject({
|
|
357
|
+
applicationId: 'app_456',
|
|
358
|
+
dockerImage: 'ghcr.io/myorg/app:v1.0.0',
|
|
359
|
+
registryId: 'stored_reg_123',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should use registryCredentials from config', async () => {
|
|
364
|
+
const saveDockerCalls: unknown[] = [];
|
|
365
|
+
|
|
366
|
+
server.use(
|
|
367
|
+
http.post(
|
|
368
|
+
'https://dokploy.example.com/api/application.saveDockerProvider',
|
|
369
|
+
async ({ request }) => {
|
|
370
|
+
const body = await request.json();
|
|
371
|
+
saveDockerCalls.push(body);
|
|
372
|
+
return HttpResponse.json({ success: true });
|
|
373
|
+
},
|
|
374
|
+
),
|
|
375
|
+
http.post('https://dokploy.example.com/api/application.deploy', () => {
|
|
376
|
+
return HttpResponse.json({ success: true });
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await deployDokploy({
|
|
381
|
+
stage: 'production',
|
|
382
|
+
tag: 'v1.0.0',
|
|
383
|
+
imageRef: 'ghcr.io/myorg/app:v1.0.0',
|
|
384
|
+
config: {
|
|
385
|
+
endpoint: 'https://dokploy.example.com',
|
|
386
|
+
projectId: 'proj_123',
|
|
387
|
+
applicationId: 'app_456',
|
|
388
|
+
registryCredentials: {
|
|
389
|
+
registryUrl: 'ghcr.io',
|
|
390
|
+
username: 'myuser',
|
|
391
|
+
password: 'mytoken',
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(saveDockerCalls).toHaveLength(1);
|
|
397
|
+
expect(saveDockerCalls[0]).toMatchObject({
|
|
398
|
+
applicationId: 'app_456',
|
|
399
|
+
dockerImage: 'ghcr.io/myorg/app:v1.0.0',
|
|
400
|
+
username: 'myuser',
|
|
401
|
+
password: 'mytoken',
|
|
402
|
+
registryUrl: 'ghcr.io',
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
describe('generateTag', () => {
|
|
408
|
+
it('should generate tag with stage and timestamp', () => {
|
|
409
|
+
const tag = generateTag('production');
|
|
410
|
+
|
|
411
|
+
expect(tag).toMatch(/^production-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should use stage prefix', () => {
|
|
415
|
+
const tag = generateTag('staging');
|
|
416
|
+
|
|
417
|
+
expect(tag.startsWith('staging-')).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should generate unique tags', () => {
|
|
421
|
+
const tag1 = generateTag('dev');
|
|
422
|
+
const tag2 = generateTag('dev');
|
|
423
|
+
|
|
424
|
+
// Tags generated at the same second should be equal
|
|
425
|
+
// But the format should be consistent
|
|
426
|
+
expect(tag1).toMatch(/^dev-/);
|
|
427
|
+
expect(tag2).toMatch(/^dev-/);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle different stage names', () => {
|
|
431
|
+
expect(generateTag('prod')).toMatch(/^prod-/);
|
|
432
|
+
expect(generateTag('development')).toMatch(/^development-/);
|
|
433
|
+
expect(generateTag('test-env')).toMatch(/^test-env-/);
|
|
434
|
+
});
|
|
245
435
|
});
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { DokployApi } from '../dokploy-api';
|
|
5
|
+
import { generateTag, provisionServices } from '../index';
|
|
6
|
+
|
|
7
|
+
const BASE_URL = 'https://dokploy.example.com';
|
|
8
|
+
|
|
9
|
+
// MSW server for mocking Dokploy API calls
|
|
10
|
+
const server = setupServer();
|
|
11
|
+
|
|
12
|
+
describe('generateTag', () => {
|
|
13
|
+
it('should generate tag with stage prefix', () => {
|
|
14
|
+
const tag = generateTag('production');
|
|
15
|
+
expect(tag).toMatch(/^production-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should generate tag with staging prefix', () => {
|
|
19
|
+
const tag = generateTag('staging');
|
|
20
|
+
expect(tag).toMatch(/^staging-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should generate unique tags', () => {
|
|
24
|
+
const tag1 = generateTag('test');
|
|
25
|
+
// Small delay to ensure different timestamp
|
|
26
|
+
const tag2 = generateTag('test');
|
|
27
|
+
// Tags should start with same prefix but could be same in fast execution
|
|
28
|
+
expect(tag1).toMatch(/^test-/);
|
|
29
|
+
expect(tag2).toMatch(/^test-/);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should replace colons and periods in timestamp', () => {
|
|
33
|
+
const tag = generateTag('dev');
|
|
34
|
+
expect(tag).not.toContain(':');
|
|
35
|
+
expect(tag).not.toContain('.');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('provisionServices', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
server.listen({ onUnhandledRequest: 'bypass' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
server.resetHandlers();
|
|
46
|
+
server.close();
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should return undefined when no services configured', async () => {
|
|
51
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
52
|
+
|
|
53
|
+
const result = await provisionServices(
|
|
54
|
+
api,
|
|
55
|
+
'proj_1',
|
|
56
|
+
'env_1',
|
|
57
|
+
'myapp',
|
|
58
|
+
undefined,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return undefined when no environmentId', async () => {
|
|
65
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
66
|
+
|
|
67
|
+
const result = await provisionServices(api, 'proj_1', undefined, 'myapp', {
|
|
68
|
+
postgres: true,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should skip postgres when DATABASE_URL already exists', async () => {
|
|
75
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
76
|
+
|
|
77
|
+
const result = await provisionServices(
|
|
78
|
+
api,
|
|
79
|
+
'proj_1',
|
|
80
|
+
'env_1',
|
|
81
|
+
'myapp',
|
|
82
|
+
{ postgres: true },
|
|
83
|
+
{ DATABASE_URL: 'postgresql://existing:5432/db' },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Should return undefined since nothing new was provisioned
|
|
87
|
+
expect(result).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should skip redis when REDIS_URL already exists', async () => {
|
|
91
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
92
|
+
|
|
93
|
+
const result = await provisionServices(
|
|
94
|
+
api,
|
|
95
|
+
'proj_1',
|
|
96
|
+
'env_1',
|
|
97
|
+
'myapp',
|
|
98
|
+
{ redis: true },
|
|
99
|
+
{ REDIS_URL: 'redis://existing:6379' },
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should provision postgres and return DATABASE_URL', async () => {
|
|
106
|
+
server.use(
|
|
107
|
+
http.post(`${BASE_URL}/api/postgres.create`, async ({ request }) => {
|
|
108
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
109
|
+
return HttpResponse.json({
|
|
110
|
+
postgresId: 'pg_123',
|
|
111
|
+
name: 'myapp-db',
|
|
112
|
+
appName: 'myapp-db',
|
|
113
|
+
databaseName: 'app',
|
|
114
|
+
databaseUser: 'postgres',
|
|
115
|
+
databasePassword: body.databasePassword,
|
|
116
|
+
applicationStatus: 'idle',
|
|
117
|
+
});
|
|
118
|
+
}),
|
|
119
|
+
http.post(`${BASE_URL}/api/postgres.deploy`, () => {
|
|
120
|
+
return HttpResponse.json({ success: true });
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
125
|
+
|
|
126
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
127
|
+
postgres: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result).toBeDefined();
|
|
131
|
+
expect(result?.DATABASE_URL).toMatch(
|
|
132
|
+
/^postgresql:\/\/postgres:[a-f0-9]{32}@myapp-db:5432\/app$/,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should provision postgres and return individual connection parameters', async () => {
|
|
137
|
+
server.use(
|
|
138
|
+
http.post(`${BASE_URL}/api/postgres.create`, async ({ request }) => {
|
|
139
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
140
|
+
return HttpResponse.json({
|
|
141
|
+
postgresId: 'pg_123',
|
|
142
|
+
name: 'myapp-db',
|
|
143
|
+
appName: 'myapp-db',
|
|
144
|
+
databaseName: 'mydb',
|
|
145
|
+
databaseUser: 'dbuser',
|
|
146
|
+
databasePassword: body.databasePassword,
|
|
147
|
+
applicationStatus: 'idle',
|
|
148
|
+
});
|
|
149
|
+
}),
|
|
150
|
+
http.post(`${BASE_URL}/api/postgres.deploy`, () => {
|
|
151
|
+
return HttpResponse.json({ success: true });
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
156
|
+
|
|
157
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
158
|
+
postgres: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(result).toBeDefined();
|
|
162
|
+
expect(result?.DATABASE_HOST).toBe('myapp-db');
|
|
163
|
+
expect(result?.DATABASE_PORT).toBe('5432');
|
|
164
|
+
expect(result?.DATABASE_NAME).toBe('mydb');
|
|
165
|
+
expect(result?.DATABASE_USER).toBe('dbuser');
|
|
166
|
+
expect(result?.DATABASE_PASSWORD).toMatch(/^[a-f0-9]{32}$/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should provision redis and return REDIS_URL', async () => {
|
|
170
|
+
server.use(
|
|
171
|
+
http.post(`${BASE_URL}/api/redis.create`, async ({ request }) => {
|
|
172
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
173
|
+
return HttpResponse.json({
|
|
174
|
+
redisId: 'redis_123',
|
|
175
|
+
name: 'myapp-cache',
|
|
176
|
+
appName: 'myapp-cache',
|
|
177
|
+
databasePassword: body.databasePassword,
|
|
178
|
+
applicationStatus: 'idle',
|
|
179
|
+
});
|
|
180
|
+
}),
|
|
181
|
+
http.post(`${BASE_URL}/api/redis.deploy`, () => {
|
|
182
|
+
return HttpResponse.json({ success: true });
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
187
|
+
|
|
188
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
189
|
+
redis: true,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(result).toBeDefined();
|
|
193
|
+
expect(result?.REDIS_URL).toMatch(
|
|
194
|
+
/^redis:\/\/:[a-f0-9]{32}@myapp-cache:6379$/,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should provision redis and return individual connection parameters', async () => {
|
|
199
|
+
server.use(
|
|
200
|
+
http.post(`${BASE_URL}/api/redis.create`, async ({ request }) => {
|
|
201
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
202
|
+
return HttpResponse.json({
|
|
203
|
+
redisId: 'redis_123',
|
|
204
|
+
name: 'myapp-cache',
|
|
205
|
+
appName: 'myapp-cache',
|
|
206
|
+
databasePassword: body.databasePassword,
|
|
207
|
+
applicationStatus: 'idle',
|
|
208
|
+
});
|
|
209
|
+
}),
|
|
210
|
+
http.post(`${BASE_URL}/api/redis.deploy`, () => {
|
|
211
|
+
return HttpResponse.json({ success: true });
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
216
|
+
|
|
217
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
218
|
+
redis: true,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result).toBeDefined();
|
|
222
|
+
expect(result?.REDIS_HOST).toBe('myapp-cache');
|
|
223
|
+
expect(result?.REDIS_PORT).toBe('6379');
|
|
224
|
+
expect(result?.REDIS_PASSWORD).toMatch(/^[a-f0-9]{32}$/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should provision both postgres and redis', async () => {
|
|
228
|
+
server.use(
|
|
229
|
+
http.post(`${BASE_URL}/api/postgres.create`, async ({ request }) => {
|
|
230
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
231
|
+
return HttpResponse.json({
|
|
232
|
+
postgresId: 'pg_123',
|
|
233
|
+
name: 'myapp-db',
|
|
234
|
+
appName: 'myapp-db',
|
|
235
|
+
databaseName: 'app',
|
|
236
|
+
databaseUser: 'postgres',
|
|
237
|
+
databasePassword: body.databasePassword,
|
|
238
|
+
});
|
|
239
|
+
}),
|
|
240
|
+
http.post(`${BASE_URL}/api/postgres.deploy`, () => {
|
|
241
|
+
return HttpResponse.json({ success: true });
|
|
242
|
+
}),
|
|
243
|
+
http.post(`${BASE_URL}/api/redis.create`, async ({ request }) => {
|
|
244
|
+
const body = (await request.json()) as { databasePassword?: string };
|
|
245
|
+
return HttpResponse.json({
|
|
246
|
+
redisId: 'redis_123',
|
|
247
|
+
name: 'myapp-cache',
|
|
248
|
+
appName: 'myapp-cache',
|
|
249
|
+
databasePassword: body.databasePassword,
|
|
250
|
+
});
|
|
251
|
+
}),
|
|
252
|
+
http.post(`${BASE_URL}/api/redis.deploy`, () => {
|
|
253
|
+
return HttpResponse.json({ success: true });
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
258
|
+
|
|
259
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
260
|
+
postgres: true,
|
|
261
|
+
redis: true,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result).toBeDefined();
|
|
265
|
+
expect(result?.DATABASE_URL).toBeDefined();
|
|
266
|
+
expect(result?.REDIS_URL).toBeDefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle postgres already exists error gracefully', async () => {
|
|
270
|
+
server.use(
|
|
271
|
+
http.post(`${BASE_URL}/api/postgres.create`, () => {
|
|
272
|
+
return HttpResponse.json(
|
|
273
|
+
{ message: 'Resource already exists' },
|
|
274
|
+
{ status: 400 },
|
|
275
|
+
);
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
280
|
+
|
|
281
|
+
// Should not throw, just return undefined for that service
|
|
282
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
283
|
+
postgres: true,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result).toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle redis already exists error gracefully', async () => {
|
|
290
|
+
server.use(
|
|
291
|
+
http.post(`${BASE_URL}/api/redis.create`, () => {
|
|
292
|
+
return HttpResponse.json(
|
|
293
|
+
{ message: 'duplicate key error' },
|
|
294
|
+
{ status: 400 },
|
|
295
|
+
);
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const api = new DokployApi({ baseUrl: BASE_URL, token: 'test-token' });
|
|
300
|
+
|
|
301
|
+
const result = await provisionServices(api, 'proj_1', 'env_1', 'myapp', {
|
|
302
|
+
redis: true,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect(result).toBeUndefined();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe('DockerComposeServices parsing', () => {
|
|
310
|
+
it('should parse array format', () => {
|
|
311
|
+
const composeServices = ['postgres', 'redis'];
|
|
312
|
+
const dockerServices = {
|
|
313
|
+
postgres: composeServices.includes('postgres'),
|
|
314
|
+
redis: composeServices.includes('redis'),
|
|
315
|
+
rabbitmq: composeServices.includes('rabbitmq'),
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(dockerServices).toEqual({
|
|
319
|
+
postgres: true,
|
|
320
|
+
redis: true,
|
|
321
|
+
rabbitmq: false,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should parse object format', () => {
|
|
326
|
+
const composeServices = { postgres: true, redis: false };
|
|
327
|
+
const dockerServices = {
|
|
328
|
+
postgres: Boolean(composeServices.postgres),
|
|
329
|
+
redis: Boolean(composeServices.redis),
|
|
330
|
+
rabbitmq: false,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
expect(dockerServices).toEqual({
|
|
334
|
+
postgres: true,
|
|
335
|
+
redis: false,
|
|
336
|
+
rabbitmq: false,
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should handle undefined', () => {
|
|
341
|
+
const composeServices = undefined;
|
|
342
|
+
const dockerServices = composeServices ? {} : undefined;
|
|
343
|
+
|
|
344
|
+
expect(dockerServices).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('image reference construction', () => {
|
|
349
|
+
it('should construct image ref with registry', () => {
|
|
350
|
+
const registry = 'ghcr.io/myorg';
|
|
351
|
+
const imageName = 'myapp';
|
|
352
|
+
const imageTag = 'v1.0.0';
|
|
353
|
+
const imageRef = registry
|
|
354
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
355
|
+
: `${imageName}:${imageTag}`;
|
|
356
|
+
|
|
357
|
+
expect(imageRef).toBe('ghcr.io/myorg/myapp:v1.0.0');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should construct image ref without registry', () => {
|
|
361
|
+
const registry = undefined;
|
|
362
|
+
const imageName = 'myapp';
|
|
363
|
+
const imageTag = 'v1.0.0';
|
|
364
|
+
const imageRef = registry
|
|
365
|
+
? `${registry}/${imageName}:${imageTag}`
|
|
366
|
+
: `${imageName}:${imageTag}`;
|
|
367
|
+
|
|
368
|
+
expect(imageRef).toBe('myapp:v1.0.0');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should handle different registry formats', () => {
|
|
372
|
+
// Docker Hub
|
|
373
|
+
expect(`docker.io/myapp:latest`).toBe('docker.io/myapp:latest');
|
|
374
|
+
// GCR
|
|
375
|
+
expect(`gcr.io/project/myapp:v1`).toBe('gcr.io/project/myapp:v1');
|
|
376
|
+
// ECR
|
|
377
|
+
expect(`123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest`).toBe(
|
|
378
|
+
'123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest',
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('generateTag edge cases', () => {
|
|
384
|
+
it('should handle stage with special characters', () => {
|
|
385
|
+
const tag = generateTag('prod-us-east');
|
|
386
|
+
expect(tag).toMatch(/^prod-us-east-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('should generate consistent format', () => {
|
|
390
|
+
const tags = [
|
|
391
|
+
generateTag('dev'),
|
|
392
|
+
generateTag('staging'),
|
|
393
|
+
generateTag('production'),
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
for (const tag of tags) {
|
|
397
|
+
// Should match ISO 8601 format with dashes instead of colons/periods
|
|
398
|
+
expect(tag).toMatch(/^[a-z-]+-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}$/);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
});
|