@geekmidas/cli 0.48.0 → 0.50.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.
Files changed (80) hide show
  1. package/dist/deploy/sniffer-envkit-patch.cjs +27 -0
  2. package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -0
  3. package/dist/deploy/sniffer-envkit-patch.d.cts +46 -0
  4. package/dist/deploy/sniffer-envkit-patch.d.cts.map +1 -0
  5. package/dist/deploy/sniffer-envkit-patch.d.mts +46 -0
  6. package/dist/deploy/sniffer-envkit-patch.d.mts.map +1 -0
  7. package/dist/deploy/sniffer-envkit-patch.mjs +20 -0
  8. package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -0
  9. package/dist/deploy/sniffer-hooks.cjs +25 -0
  10. package/dist/deploy/sniffer-hooks.cjs.map +1 -0
  11. package/dist/deploy/sniffer-hooks.d.cts +27 -0
  12. package/dist/deploy/sniffer-hooks.d.cts.map +1 -0
  13. package/dist/deploy/sniffer-hooks.d.mts +27 -0
  14. package/dist/deploy/sniffer-hooks.d.mts.map +1 -0
  15. package/dist/deploy/sniffer-hooks.mjs +24 -0
  16. package/dist/deploy/sniffer-hooks.mjs.map +1 -0
  17. package/dist/deploy/sniffer-loader.cjs +16 -0
  18. package/dist/deploy/sniffer-loader.cjs.map +1 -0
  19. package/dist/deploy/sniffer-loader.d.cts +1 -0
  20. package/dist/deploy/sniffer-loader.d.mts +1 -0
  21. package/dist/deploy/sniffer-loader.mjs +15 -0
  22. package/dist/deploy/sniffer-loader.mjs.map +1 -0
  23. package/dist/deploy/sniffer-worker.cjs +42 -0
  24. package/dist/deploy/sniffer-worker.cjs.map +1 -0
  25. package/dist/deploy/sniffer-worker.d.cts +9 -0
  26. package/dist/deploy/sniffer-worker.d.cts.map +1 -0
  27. package/dist/deploy/sniffer-worker.d.mts +9 -0
  28. package/dist/deploy/sniffer-worker.d.mts.map +1 -0
  29. package/dist/deploy/sniffer-worker.mjs +41 -0
  30. package/dist/deploy/sniffer-worker.mjs.map +1 -0
  31. package/dist/{dokploy-api-DvzIDxTj.mjs → dokploy-api-94KzmTVf.mjs} +4 -4
  32. package/dist/dokploy-api-94KzmTVf.mjs.map +1 -0
  33. package/dist/dokploy-api-CItuaWTq.mjs +3 -0
  34. package/dist/dokploy-api-DBNE8MDt.cjs +3 -0
  35. package/dist/{dokploy-api-BDLu0qWi.cjs → dokploy-api-YD8WCQfW.cjs} +4 -4
  36. package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
  37. package/dist/index.cjs +2415 -1893
  38. package/dist/index.cjs.map +1 -1
  39. package/dist/index.mjs +2411 -1889
  40. package/dist/index.mjs.map +1 -1
  41. package/package.json +8 -6
  42. package/src/build/__tests__/handler-templates.spec.ts +947 -0
  43. package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +24 -0
  44. package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +24 -0
  45. package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +12 -0
  46. package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +14 -0
  47. package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +16 -0
  48. package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +10 -0
  49. package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +18 -0
  50. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +18 -0
  51. package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +16 -0
  52. package/src/deploy/__tests__/dns-verification.spec.ts +229 -0
  53. package/src/deploy/__tests__/dokploy-api.spec.ts +2 -3
  54. package/src/deploy/__tests__/domain.spec.ts +7 -3
  55. package/src/deploy/__tests__/env-resolver.spec.ts +469 -0
  56. package/src/deploy/__tests__/index.spec.ts +12 -12
  57. package/src/deploy/__tests__/secrets.spec.ts +4 -1
  58. package/src/deploy/__tests__/sniffer.spec.ts +326 -1
  59. package/src/deploy/__tests__/state.spec.ts +844 -0
  60. package/src/deploy/dns/hostinger-api.ts +4 -1
  61. package/src/deploy/dns/index.ts +113 -1
  62. package/src/deploy/docker.ts +1 -2
  63. package/src/deploy/dokploy-api.ts +18 -9
  64. package/src/deploy/domain.ts +5 -4
  65. package/src/deploy/env-resolver.ts +278 -0
  66. package/src/deploy/index.ts +525 -119
  67. package/src/deploy/secrets.ts +7 -2
  68. package/src/deploy/sniffer-envkit-patch.ts +59 -0
  69. package/src/deploy/sniffer-hooks.ts +57 -0
  70. package/src/deploy/sniffer-loader.ts +28 -0
  71. package/src/deploy/sniffer-worker.ts +74 -0
  72. package/src/deploy/sniffer.ts +170 -14
  73. package/src/deploy/state.ts +162 -1
  74. package/src/init/versions.ts +3 -3
  75. package/tsconfig.tsbuildinfo +1 -1
  76. package/tsdown.config.ts +5 -0
  77. package/dist/dokploy-api-BDLu0qWi.cjs.map +0 -1
  78. package/dist/dokploy-api-BN3V57z1.mjs +0 -3
  79. package/dist/dokploy-api-BdCKjFDA.cjs +0 -3
  80. package/dist/dokploy-api-DvzIDxTj.mjs.map +0 -1
@@ -0,0 +1,844 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import {
6
+ createEmptyState,
7
+ getAllAppCredentials,
8
+ getAllDnsVerifications,
9
+ getAllGeneratedSecrets,
10
+ getAppCredentials,
11
+ getAppGeneratedSecrets,
12
+ getApplicationId,
13
+ getDnsVerification,
14
+ getGeneratedSecret,
15
+ getPostgresId,
16
+ getRedisId,
17
+ isDnsVerified,
18
+ readStageState,
19
+ setAppCredentials,
20
+ setApplicationId,
21
+ setDnsVerification,
22
+ setGeneratedSecret,
23
+ setPostgresId,
24
+ setRedisId,
25
+ writeStageState,
26
+ } from '../state';
27
+ import type { DokployStageState } from '../state';
28
+
29
+ describe('state management', () => {
30
+ let testDir: string;
31
+
32
+ beforeEach(async () => {
33
+ testDir = join(tmpdir(), `gkm-state-test-${Date.now()}`);
34
+ await mkdir(testDir, { recursive: true });
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await rm(testDir, { recursive: true, force: true });
39
+ });
40
+
41
+ describe('createEmptyState', () => {
42
+ it('should create a valid empty state', () => {
43
+ const state = createEmptyState('production', 'env_123');
44
+
45
+ expect(state.provider).toBe('dokploy');
46
+ expect(state.stage).toBe('production');
47
+ expect(state.environmentId).toBe('env_123');
48
+ expect(state.applications).toEqual({});
49
+ expect(state.services).toEqual({});
50
+ expect(state.lastDeployedAt).toBeDefined();
51
+ });
52
+
53
+ it('should generate valid ISO timestamp', () => {
54
+ const state = createEmptyState('staging', 'env_456');
55
+
56
+ expect(() => new Date(state.lastDeployedAt)).not.toThrow();
57
+ });
58
+ });
59
+
60
+ describe('readStageState', () => {
61
+ it('should return null when state file does not exist', async () => {
62
+ const state = await readStageState(testDir, 'nonexistent');
63
+
64
+ expect(state).toBeNull();
65
+ });
66
+
67
+ it('should read existing state file', async () => {
68
+ const stateData: DokployStageState = {
69
+ provider: 'dokploy',
70
+ stage: 'production',
71
+ environmentId: 'env_123',
72
+ applications: { api: 'app_123' },
73
+ services: { postgresId: 'pg_123' },
74
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
75
+ };
76
+
77
+ await mkdir(join(testDir, '.gkm'), { recursive: true });
78
+ await writeFile(
79
+ join(testDir, '.gkm', 'deploy-production.json'),
80
+ JSON.stringify(stateData),
81
+ );
82
+
83
+ const state = await readStageState(testDir, 'production');
84
+
85
+ expect(state).toEqual(stateData);
86
+ });
87
+
88
+ it('should return null for invalid JSON', async () => {
89
+ await mkdir(join(testDir, '.gkm'), { recursive: true });
90
+ await writeFile(
91
+ join(testDir, '.gkm', 'deploy-invalid.json'),
92
+ 'not valid json',
93
+ );
94
+
95
+ const state = await readStageState(testDir, 'invalid');
96
+
97
+ expect(state).toBeNull();
98
+ });
99
+ });
100
+
101
+ describe('writeStageState', () => {
102
+ it('should create .gkm directory if not exists', async () => {
103
+ const state = createEmptyState('staging', 'env_456');
104
+
105
+ await writeStageState(testDir, 'staging', state);
106
+
107
+ const content = await readFile(
108
+ join(testDir, '.gkm', 'deploy-staging.json'),
109
+ 'utf-8',
110
+ );
111
+ expect(JSON.parse(content).stage).toBe('staging');
112
+ });
113
+
114
+ it('should update lastDeployedAt timestamp', async () => {
115
+ const state = createEmptyState('staging', 'env_456');
116
+ const originalTimestamp = state.lastDeployedAt;
117
+
118
+ // Wait a bit to ensure different timestamp
119
+ await new Promise((resolve) => setTimeout(resolve, 10));
120
+
121
+ await writeStageState(testDir, 'staging', state);
122
+
123
+ expect(state.lastDeployedAt).not.toBe(originalTimestamp);
124
+ });
125
+
126
+ it('should preserve existing state data', async () => {
127
+ const state: DokployStageState = {
128
+ provider: 'dokploy',
129
+ stage: 'production',
130
+ environmentId: 'env_123',
131
+ applications: { api: 'app_123', web: 'app_456' },
132
+ services: { postgresId: 'pg_123', redisId: 'redis_123' },
133
+ appCredentials: { api: { dbUser: 'api', dbPassword: 'secret' } },
134
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
135
+ };
136
+
137
+ await writeStageState(testDir, 'production', state);
138
+
139
+ const content = await readFile(
140
+ join(testDir, '.gkm', 'deploy-production.json'),
141
+ 'utf-8',
142
+ );
143
+ const parsed = JSON.parse(content);
144
+
145
+ expect(parsed.applications).toEqual({ api: 'app_123', web: 'app_456' });
146
+ expect(parsed.services).toEqual({
147
+ postgresId: 'pg_123',
148
+ redisId: 'redis_123',
149
+ });
150
+ expect(parsed.appCredentials).toEqual({
151
+ api: { dbUser: 'api', dbPassword: 'secret' },
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('getApplicationId', () => {
157
+ it('should return application ID when exists', () => {
158
+ const state: DokployStageState = {
159
+ provider: 'dokploy',
160
+ stage: 'production',
161
+ environmentId: 'env_123',
162
+ applications: { api: 'app_123' },
163
+ services: {},
164
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
165
+ };
166
+
167
+ expect(getApplicationId(state, 'api')).toBe('app_123');
168
+ });
169
+
170
+ it('should return undefined when application does not exist', () => {
171
+ const state: DokployStageState = {
172
+ provider: 'dokploy',
173
+ stage: 'production',
174
+ environmentId: 'env_123',
175
+ applications: {},
176
+ services: {},
177
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
178
+ };
179
+
180
+ expect(getApplicationId(state, 'nonexistent')).toBeUndefined();
181
+ });
182
+
183
+ it('should return undefined when state is null', () => {
184
+ expect(getApplicationId(null, 'api')).toBeUndefined();
185
+ });
186
+ });
187
+
188
+ describe('setApplicationId', () => {
189
+ it('should set application ID', () => {
190
+ const state = createEmptyState('production', 'env_123');
191
+
192
+ setApplicationId(state, 'api', 'app_123');
193
+
194
+ expect(state.applications.api).toBe('app_123');
195
+ });
196
+
197
+ it('should update existing application ID', () => {
198
+ const state = createEmptyState('production', 'env_123');
199
+ state.applications.api = 'app_old';
200
+
201
+ setApplicationId(state, 'api', 'app_new');
202
+
203
+ expect(state.applications.api).toBe('app_new');
204
+ });
205
+ });
206
+
207
+ describe('getPostgresId', () => {
208
+ it('should return postgres ID when exists', () => {
209
+ const state: DokployStageState = {
210
+ provider: 'dokploy',
211
+ stage: 'production',
212
+ environmentId: 'env_123',
213
+ applications: {},
214
+ services: { postgresId: 'pg_123' },
215
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
216
+ };
217
+
218
+ expect(getPostgresId(state)).toBe('pg_123');
219
+ });
220
+
221
+ it('should return undefined when postgres not configured', () => {
222
+ const state = createEmptyState('production', 'env_123');
223
+
224
+ expect(getPostgresId(state)).toBeUndefined();
225
+ });
226
+
227
+ it('should return undefined when state is null', () => {
228
+ expect(getPostgresId(null)).toBeUndefined();
229
+ });
230
+ });
231
+
232
+ describe('setPostgresId', () => {
233
+ it('should set postgres ID', () => {
234
+ const state = createEmptyState('production', 'env_123');
235
+
236
+ setPostgresId(state, 'pg_123');
237
+
238
+ expect(state.services.postgresId).toBe('pg_123');
239
+ });
240
+ });
241
+
242
+ describe('getRedisId', () => {
243
+ it('should return redis ID when exists', () => {
244
+ const state: DokployStageState = {
245
+ provider: 'dokploy',
246
+ stage: 'production',
247
+ environmentId: 'env_123',
248
+ applications: {},
249
+ services: { redisId: 'redis_123' },
250
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
251
+ };
252
+
253
+ expect(getRedisId(state)).toBe('redis_123');
254
+ });
255
+
256
+ it('should return undefined when redis not configured', () => {
257
+ const state = createEmptyState('production', 'env_123');
258
+
259
+ expect(getRedisId(state)).toBeUndefined();
260
+ });
261
+
262
+ it('should return undefined when state is null', () => {
263
+ expect(getRedisId(null)).toBeUndefined();
264
+ });
265
+ });
266
+
267
+ describe('setRedisId', () => {
268
+ it('should set redis ID', () => {
269
+ const state = createEmptyState('production', 'env_123');
270
+
271
+ setRedisId(state, 'redis_123');
272
+
273
+ expect(state.services.redisId).toBe('redis_123');
274
+ });
275
+ });
276
+
277
+ describe('getAppCredentials', () => {
278
+ it('should return credentials when exists', () => {
279
+ const state: DokployStageState = {
280
+ provider: 'dokploy',
281
+ stage: 'production',
282
+ environmentId: 'env_123',
283
+ applications: {},
284
+ services: {},
285
+ appCredentials: {
286
+ api: { dbUser: 'api', dbPassword: 'secret123' },
287
+ },
288
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
289
+ };
290
+
291
+ expect(getAppCredentials(state, 'api')).toEqual({
292
+ dbUser: 'api',
293
+ dbPassword: 'secret123',
294
+ });
295
+ });
296
+
297
+ it('should return undefined when app not found', () => {
298
+ const state: DokployStageState = {
299
+ provider: 'dokploy',
300
+ stage: 'production',
301
+ environmentId: 'env_123',
302
+ applications: {},
303
+ services: {},
304
+ appCredentials: {},
305
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
306
+ };
307
+
308
+ expect(getAppCredentials(state, 'nonexistent')).toBeUndefined();
309
+ });
310
+
311
+ it('should return undefined when no appCredentials', () => {
312
+ const state = createEmptyState('production', 'env_123');
313
+
314
+ expect(getAppCredentials(state, 'api')).toBeUndefined();
315
+ });
316
+
317
+ it('should return undefined when state is null', () => {
318
+ expect(getAppCredentials(null, 'api')).toBeUndefined();
319
+ });
320
+ });
321
+
322
+ describe('setAppCredentials', () => {
323
+ it('should set credentials', () => {
324
+ const state = createEmptyState('production', 'env_123');
325
+
326
+ setAppCredentials(state, 'api', { dbUser: 'api', dbPassword: 'secret' });
327
+
328
+ expect(state.appCredentials?.api).toEqual({
329
+ dbUser: 'api',
330
+ dbPassword: 'secret',
331
+ });
332
+ });
333
+
334
+ it('should initialize appCredentials if not exists', () => {
335
+ const state = createEmptyState('production', 'env_123');
336
+ expect(state.appCredentials).toBeUndefined();
337
+
338
+ setAppCredentials(state, 'api', { dbUser: 'api', dbPassword: 'secret' });
339
+
340
+ expect(state.appCredentials).toBeDefined();
341
+ });
342
+
343
+ it('should update existing credentials', () => {
344
+ const state = createEmptyState('production', 'env_123');
345
+ state.appCredentials = {
346
+ api: { dbUser: 'api', dbPassword: 'old_password' },
347
+ };
348
+
349
+ setAppCredentials(state, 'api', {
350
+ dbUser: 'api',
351
+ dbPassword: 'new_password',
352
+ });
353
+
354
+ expect(state.appCredentials.api.dbPassword).toBe('new_password');
355
+ });
356
+ });
357
+
358
+ describe('getAllAppCredentials', () => {
359
+ it('should return all credentials', () => {
360
+ const state: DokployStageState = {
361
+ provider: 'dokploy',
362
+ stage: 'production',
363
+ environmentId: 'env_123',
364
+ applications: {},
365
+ services: {},
366
+ appCredentials: {
367
+ api: { dbUser: 'api', dbPassword: 'secret1' },
368
+ auth: { dbUser: 'auth', dbPassword: 'secret2' },
369
+ },
370
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
371
+ };
372
+
373
+ expect(getAllAppCredentials(state)).toEqual({
374
+ api: { dbUser: 'api', dbPassword: 'secret1' },
375
+ auth: { dbUser: 'auth', dbPassword: 'secret2' },
376
+ });
377
+ });
378
+
379
+ it('should return empty object when no credentials', () => {
380
+ const state = createEmptyState('production', 'env_123');
381
+
382
+ expect(getAllAppCredentials(state)).toEqual({});
383
+ });
384
+
385
+ it('should return empty object when state is null', () => {
386
+ expect(getAllAppCredentials(null)).toEqual({});
387
+ });
388
+ });
389
+
390
+ // =========================================================================
391
+ // Generated Secrets Tests
392
+ // =========================================================================
393
+
394
+ describe('getGeneratedSecret', () => {
395
+ it('should return secret when exists', () => {
396
+ const state: DokployStageState = {
397
+ provider: 'dokploy',
398
+ stage: 'production',
399
+ environmentId: 'env_123',
400
+ applications: {},
401
+ services: {},
402
+ generatedSecrets: {
403
+ auth: { BETTER_AUTH_SECRET: 'secret123' },
404
+ },
405
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
406
+ };
407
+
408
+ expect(getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET')).toBe(
409
+ 'secret123',
410
+ );
411
+ });
412
+
413
+ it('should return undefined when app not found', () => {
414
+ const state: DokployStageState = {
415
+ provider: 'dokploy',
416
+ stage: 'production',
417
+ environmentId: 'env_123',
418
+ applications: {},
419
+ services: {},
420
+ generatedSecrets: {
421
+ auth: { BETTER_AUTH_SECRET: 'secret123' },
422
+ },
423
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
424
+ };
425
+
426
+ expect(getGeneratedSecret(state, 'api', 'BETTER_AUTH_SECRET')).toBeUndefined();
427
+ });
428
+
429
+ it('should return undefined when secret name not found', () => {
430
+ const state: DokployStageState = {
431
+ provider: 'dokploy',
432
+ stage: 'production',
433
+ environmentId: 'env_123',
434
+ applications: {},
435
+ services: {},
436
+ generatedSecrets: {
437
+ auth: { BETTER_AUTH_SECRET: 'secret123' },
438
+ },
439
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
440
+ };
441
+
442
+ expect(getGeneratedSecret(state, 'auth', 'OTHER_SECRET')).toBeUndefined();
443
+ });
444
+
445
+ it('should return undefined when no generatedSecrets', () => {
446
+ const state = createEmptyState('production', 'env_123');
447
+
448
+ expect(getGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET')).toBeUndefined();
449
+ });
450
+
451
+ it('should return undefined when state is null', () => {
452
+ expect(getGeneratedSecret(null, 'auth', 'BETTER_AUTH_SECRET')).toBeUndefined();
453
+ });
454
+ });
455
+
456
+ describe('setGeneratedSecret', () => {
457
+ it('should set secret', () => {
458
+ const state = createEmptyState('production', 'env_123');
459
+
460
+ setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
461
+
462
+ expect(state.generatedSecrets?.auth?.BETTER_AUTH_SECRET).toBe('secret123');
463
+ });
464
+
465
+ it('should initialize generatedSecrets if not exists', () => {
466
+ const state = createEmptyState('production', 'env_123');
467
+ expect(state.generatedSecrets).toBeUndefined();
468
+
469
+ setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
470
+
471
+ expect(state.generatedSecrets).toBeDefined();
472
+ });
473
+
474
+ it('should initialize app secrets if not exists', () => {
475
+ const state = createEmptyState('production', 'env_123');
476
+ state.generatedSecrets = {};
477
+
478
+ setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret123');
479
+
480
+ expect(state.generatedSecrets.auth).toBeDefined();
481
+ });
482
+
483
+ it('should update existing secret', () => {
484
+ const state = createEmptyState('production', 'env_123');
485
+ state.generatedSecrets = {
486
+ auth: { BETTER_AUTH_SECRET: 'old_secret' },
487
+ };
488
+
489
+ setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'new_secret');
490
+
491
+ expect(state.generatedSecrets.auth.BETTER_AUTH_SECRET).toBe('new_secret');
492
+ });
493
+
494
+ it('should add multiple secrets for same app', () => {
495
+ const state = createEmptyState('production', 'env_123');
496
+
497
+ setGeneratedSecret(state, 'auth', 'BETTER_AUTH_SECRET', 'secret1');
498
+ setGeneratedSecret(state, 'auth', 'OTHER_SECRET', 'secret2');
499
+
500
+ expect(state.generatedSecrets?.auth).toEqual({
501
+ BETTER_AUTH_SECRET: 'secret1',
502
+ OTHER_SECRET: 'secret2',
503
+ });
504
+ });
505
+ });
506
+
507
+ describe('getAppGeneratedSecrets', () => {
508
+ it('should return all secrets for an app', () => {
509
+ const state: DokployStageState = {
510
+ provider: 'dokploy',
511
+ stage: 'production',
512
+ environmentId: 'env_123',
513
+ applications: {},
514
+ services: {},
515
+ generatedSecrets: {
516
+ auth: {
517
+ BETTER_AUTH_SECRET: 'secret1',
518
+ OTHER_SECRET: 'secret2',
519
+ },
520
+ },
521
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
522
+ };
523
+
524
+ expect(getAppGeneratedSecrets(state, 'auth')).toEqual({
525
+ BETTER_AUTH_SECRET: 'secret1',
526
+ OTHER_SECRET: 'secret2',
527
+ });
528
+ });
529
+
530
+ it('should return empty object when app not found', () => {
531
+ const state: DokployStageState = {
532
+ provider: 'dokploy',
533
+ stage: 'production',
534
+ environmentId: 'env_123',
535
+ applications: {},
536
+ services: {},
537
+ generatedSecrets: {
538
+ auth: { BETTER_AUTH_SECRET: 'secret1' },
539
+ },
540
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
541
+ };
542
+
543
+ expect(getAppGeneratedSecrets(state, 'api')).toEqual({});
544
+ });
545
+
546
+ it('should return empty object when state is null', () => {
547
+ expect(getAppGeneratedSecrets(null, 'auth')).toEqual({});
548
+ });
549
+ });
550
+
551
+ describe('getAllGeneratedSecrets', () => {
552
+ it('should return all generated secrets', () => {
553
+ const state: DokployStageState = {
554
+ provider: 'dokploy',
555
+ stage: 'production',
556
+ environmentId: 'env_123',
557
+ applications: {},
558
+ services: {},
559
+ generatedSecrets: {
560
+ auth: { BETTER_AUTH_SECRET: 'secret1' },
561
+ 'admin-auth': { BETTER_AUTH_SECRET: 'secret2' },
562
+ },
563
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
564
+ };
565
+
566
+ expect(getAllGeneratedSecrets(state)).toEqual({
567
+ auth: { BETTER_AUTH_SECRET: 'secret1' },
568
+ 'admin-auth': { BETTER_AUTH_SECRET: 'secret2' },
569
+ });
570
+ });
571
+
572
+ it('should return empty object when no secrets', () => {
573
+ const state = createEmptyState('production', 'env_123');
574
+
575
+ expect(getAllGeneratedSecrets(state)).toEqual({});
576
+ });
577
+
578
+ it('should return empty object when state is null', () => {
579
+ expect(getAllGeneratedSecrets(null)).toEqual({});
580
+ });
581
+ });
582
+
583
+ // =========================================================================
584
+ // DNS Verification Tests
585
+ // =========================================================================
586
+
587
+ describe('getDnsVerification', () => {
588
+ it('should return verification record when exists', () => {
589
+ const state: DokployStageState = {
590
+ provider: 'dokploy',
591
+ stage: 'production',
592
+ environmentId: 'env_123',
593
+ applications: {},
594
+ services: {},
595
+ dnsVerified: {
596
+ 'api.example.com': {
597
+ serverIp: '1.2.3.4',
598
+ verifiedAt: '2024-01-01T00:00:00.000Z',
599
+ },
600
+ },
601
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
602
+ };
603
+
604
+ expect(getDnsVerification(state, 'api.example.com')).toEqual({
605
+ serverIp: '1.2.3.4',
606
+ verifiedAt: '2024-01-01T00:00:00.000Z',
607
+ });
608
+ });
609
+
610
+ it('should return undefined when hostname not found', () => {
611
+ const state: DokployStageState = {
612
+ provider: 'dokploy',
613
+ stage: 'production',
614
+ environmentId: 'env_123',
615
+ applications: {},
616
+ services: {},
617
+ dnsVerified: {
618
+ 'api.example.com': {
619
+ serverIp: '1.2.3.4',
620
+ verifiedAt: '2024-01-01T00:00:00.000Z',
621
+ },
622
+ },
623
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
624
+ };
625
+
626
+ expect(getDnsVerification(state, 'web.example.com')).toBeUndefined();
627
+ });
628
+
629
+ it('should return undefined when no dnsVerified', () => {
630
+ const state = createEmptyState('production', 'env_123');
631
+
632
+ expect(getDnsVerification(state, 'api.example.com')).toBeUndefined();
633
+ });
634
+
635
+ it('should return undefined when state is null', () => {
636
+ expect(getDnsVerification(null, 'api.example.com')).toBeUndefined();
637
+ });
638
+ });
639
+
640
+ describe('setDnsVerification', () => {
641
+ it('should set verification record', () => {
642
+ const state = createEmptyState('production', 'env_123');
643
+
644
+ setDnsVerification(state, 'api.example.com', '1.2.3.4');
645
+
646
+ expect(state.dnsVerified?.['api.example.com']?.serverIp).toBe('1.2.3.4');
647
+ expect(state.dnsVerified?.['api.example.com']?.verifiedAt).toBeDefined();
648
+ });
649
+
650
+ it('should initialize dnsVerified if not exists', () => {
651
+ const state = createEmptyState('production', 'env_123');
652
+ expect(state.dnsVerified).toBeUndefined();
653
+
654
+ setDnsVerification(state, 'api.example.com', '1.2.3.4');
655
+
656
+ expect(state.dnsVerified).toBeDefined();
657
+ });
658
+
659
+ it('should update existing verification', () => {
660
+ const state = createEmptyState('production', 'env_123');
661
+ state.dnsVerified = {
662
+ 'api.example.com': {
663
+ serverIp: '1.1.1.1',
664
+ verifiedAt: '2024-01-01T00:00:00.000Z',
665
+ },
666
+ };
667
+
668
+ setDnsVerification(state, 'api.example.com', '2.2.2.2');
669
+
670
+ expect(state.dnsVerified['api.example.com'].serverIp).toBe('2.2.2.2');
671
+ });
672
+
673
+ it('should generate valid ISO timestamp', () => {
674
+ const state = createEmptyState('production', 'env_123');
675
+
676
+ setDnsVerification(state, 'api.example.com', '1.2.3.4');
677
+
678
+ const verifiedAt = state.dnsVerified?.['api.example.com']?.verifiedAt;
679
+ expect(() => new Date(verifiedAt!)).not.toThrow();
680
+ });
681
+ });
682
+
683
+ describe('isDnsVerified', () => {
684
+ it('should return true when hostname verified with same IP', () => {
685
+ const state: DokployStageState = {
686
+ provider: 'dokploy',
687
+ stage: 'production',
688
+ environmentId: 'env_123',
689
+ applications: {},
690
+ services: {},
691
+ dnsVerified: {
692
+ 'api.example.com': {
693
+ serverIp: '1.2.3.4',
694
+ verifiedAt: '2024-01-01T00:00:00.000Z',
695
+ },
696
+ },
697
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
698
+ };
699
+
700
+ expect(isDnsVerified(state, 'api.example.com', '1.2.3.4')).toBe(true);
701
+ });
702
+
703
+ it('should return false when hostname verified with different IP', () => {
704
+ const state: DokployStageState = {
705
+ provider: 'dokploy',
706
+ stage: 'production',
707
+ environmentId: 'env_123',
708
+ applications: {},
709
+ services: {},
710
+ dnsVerified: {
711
+ 'api.example.com': {
712
+ serverIp: '1.2.3.4',
713
+ verifiedAt: '2024-01-01T00:00:00.000Z',
714
+ },
715
+ },
716
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
717
+ };
718
+
719
+ expect(isDnsVerified(state, 'api.example.com', '5.6.7.8')).toBe(false);
720
+ });
721
+
722
+ it('should return false when hostname not verified', () => {
723
+ const state: DokployStageState = {
724
+ provider: 'dokploy',
725
+ stage: 'production',
726
+ environmentId: 'env_123',
727
+ applications: {},
728
+ services: {},
729
+ dnsVerified: {},
730
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
731
+ };
732
+
733
+ expect(isDnsVerified(state, 'api.example.com', '1.2.3.4')).toBe(false);
734
+ });
735
+
736
+ it('should return false when state is null', () => {
737
+ expect(isDnsVerified(null, 'api.example.com', '1.2.3.4')).toBe(false);
738
+ });
739
+ });
740
+
741
+ describe('getAllDnsVerifications', () => {
742
+ it('should return all verification records', () => {
743
+ const state: DokployStageState = {
744
+ provider: 'dokploy',
745
+ stage: 'production',
746
+ environmentId: 'env_123',
747
+ applications: {},
748
+ services: {},
749
+ dnsVerified: {
750
+ 'api.example.com': {
751
+ serverIp: '1.2.3.4',
752
+ verifiedAt: '2024-01-01T00:00:00.000Z',
753
+ },
754
+ 'web.example.com': {
755
+ serverIp: '1.2.3.4',
756
+ verifiedAt: '2024-01-02T00:00:00.000Z',
757
+ },
758
+ },
759
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
760
+ };
761
+
762
+ expect(getAllDnsVerifications(state)).toEqual({
763
+ 'api.example.com': {
764
+ serverIp: '1.2.3.4',
765
+ verifiedAt: '2024-01-01T00:00:00.000Z',
766
+ },
767
+ 'web.example.com': {
768
+ serverIp: '1.2.3.4',
769
+ verifiedAt: '2024-01-02T00:00:00.000Z',
770
+ },
771
+ });
772
+ });
773
+
774
+ it('should return empty object when no verifications', () => {
775
+ const state = createEmptyState('production', 'env_123');
776
+
777
+ expect(getAllDnsVerifications(state)).toEqual({});
778
+ });
779
+
780
+ it('should return empty object when state is null', () => {
781
+ expect(getAllDnsVerifications(null)).toEqual({});
782
+ });
783
+ });
784
+
785
+ describe('writeStageState with new fields', () => {
786
+ it('should preserve generatedSecrets', async () => {
787
+ const state: DokployStageState = {
788
+ provider: 'dokploy',
789
+ stage: 'production',
790
+ environmentId: 'env_123',
791
+ applications: {},
792
+ services: {},
793
+ generatedSecrets: {
794
+ auth: { BETTER_AUTH_SECRET: 'secret123' },
795
+ },
796
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
797
+ };
798
+
799
+ await writeStageState(testDir, 'production', state);
800
+
801
+ const content = await readFile(
802
+ join(testDir, '.gkm', 'deploy-production.json'),
803
+ 'utf-8',
804
+ );
805
+ const parsed = JSON.parse(content);
806
+
807
+ expect(parsed.generatedSecrets).toEqual({
808
+ auth: { BETTER_AUTH_SECRET: 'secret123' },
809
+ });
810
+ });
811
+
812
+ it('should preserve dnsVerified', async () => {
813
+ const state: DokployStageState = {
814
+ provider: 'dokploy',
815
+ stage: 'production',
816
+ environmentId: 'env_123',
817
+ applications: {},
818
+ services: {},
819
+ dnsVerified: {
820
+ 'api.example.com': {
821
+ serverIp: '1.2.3.4',
822
+ verifiedAt: '2024-01-01T00:00:00.000Z',
823
+ },
824
+ },
825
+ lastDeployedAt: '2024-01-01T00:00:00.000Z',
826
+ };
827
+
828
+ await writeStageState(testDir, 'production', state);
829
+
830
+ const content = await readFile(
831
+ join(testDir, '.gkm', 'deploy-production.json'),
832
+ 'utf-8',
833
+ );
834
+ const parsed = JSON.parse(content);
835
+
836
+ expect(parsed.dnsVerified).toEqual({
837
+ 'api.example.com': {
838
+ serverIp: '1.2.3.4',
839
+ verifiedAt: '2024-01-01T00:00:00.000Z',
840
+ },
841
+ });
842
+ });
843
+ });
844
+ });