@geekmidas/cli 0.53.0 → 1.0.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/CHANGELOG.md +17 -0
- package/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2242 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2219 -545
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +55 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +41 -17
- package/src/deploy/__tests__/sniffer.spec.ts +168 -10
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +130 -5
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/docker/templates.ts +3 -3
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { HttpResponse, http } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import {
|
|
4
|
+
afterAll,
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
it,
|
|
11
|
+
} from 'vitest';
|
|
12
|
+
import { HostingerProvider } from '../dns/HostingerProvider';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HostingerProvider Tests
|
|
16
|
+
*
|
|
17
|
+
* Uses MSW to mock the Hostinger DNS API.
|
|
18
|
+
* API Base: https://developers.hostinger.com
|
|
19
|
+
*/
|
|
20
|
+
describe('HostingerProvider', () => {
|
|
21
|
+
const HOSTINGER_API_BASE = 'https://developers.hostinger.com';
|
|
22
|
+
const TEST_DOMAIN = 'example.com';
|
|
23
|
+
const TEST_TOKEN = 'test-hostinger-token';
|
|
24
|
+
|
|
25
|
+
// Track current mock records for realistic API simulation
|
|
26
|
+
let mockRecords: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
type: string;
|
|
29
|
+
ttl: number;
|
|
30
|
+
records: Array<{ content: string }>;
|
|
31
|
+
}> = [];
|
|
32
|
+
|
|
33
|
+
// MSW server setup
|
|
34
|
+
const server = setupServer(
|
|
35
|
+
// GET /api/dns/v1/zones/{domain} - Get DNS records
|
|
36
|
+
http.get(
|
|
37
|
+
`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`,
|
|
38
|
+
({ request }) => {
|
|
39
|
+
// Check authorization
|
|
40
|
+
const authHeader = request.headers.get('Authorization');
|
|
41
|
+
if (authHeader !== `Bearer ${TEST_TOKEN}`) {
|
|
42
|
+
return HttpResponse.json(
|
|
43
|
+
{ message: 'Unauthorized' },
|
|
44
|
+
{ status: 401 },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return HttpResponse.json({ data: mockRecords });
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
|
|
52
|
+
// PUT /api/dns/v1/zones/{domain} - Upsert DNS records
|
|
53
|
+
http.put(
|
|
54
|
+
`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`,
|
|
55
|
+
async ({ request }) => {
|
|
56
|
+
// Check authorization
|
|
57
|
+
const authHeader = request.headers.get('Authorization');
|
|
58
|
+
if (authHeader !== `Bearer ${TEST_TOKEN}`) {
|
|
59
|
+
return HttpResponse.json(
|
|
60
|
+
{ message: 'Unauthorized' },
|
|
61
|
+
{ status: 401 },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = (await request.json()) as {
|
|
66
|
+
overwrite?: boolean;
|
|
67
|
+
zone: Array<{
|
|
68
|
+
name: string;
|
|
69
|
+
type: string;
|
|
70
|
+
ttl: number;
|
|
71
|
+
records: Array<{ content: string }>;
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Simulate upsert behavior
|
|
76
|
+
for (const record of body.zone) {
|
|
77
|
+
const existingIndex = mockRecords.findIndex(
|
|
78
|
+
(r) => r.name === record.name && r.type === record.type,
|
|
79
|
+
);
|
|
80
|
+
if (existingIndex >= 0) {
|
|
81
|
+
mockRecords[existingIndex] = record;
|
|
82
|
+
} else {
|
|
83
|
+
mockRecords.push(record);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new HttpResponse(null, { status: 204 });
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
beforeAll(() => {
|
|
93
|
+
// Set the token via environment variable
|
|
94
|
+
process.env.HOSTINGER_API_TOKEN = TEST_TOKEN;
|
|
95
|
+
server.listen({ onUnhandledRequest: 'error' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
// Reset mock records before each test
|
|
100
|
+
mockRecords = [];
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
server.resetHandlers();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterAll(() => {
|
|
108
|
+
delete process.env.HOSTINGER_API_TOKEN;
|
|
109
|
+
server.close();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('name', () => {
|
|
113
|
+
it('should have name "hostinger"', () => {
|
|
114
|
+
const provider = new HostingerProvider();
|
|
115
|
+
expect(provider.name).toBe('hostinger');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getRecords', () => {
|
|
120
|
+
it('should throw error when token is invalid', async () => {
|
|
121
|
+
// Use an invalid token
|
|
122
|
+
const savedToken = process.env.HOSTINGER_API_TOKEN;
|
|
123
|
+
process.env.HOSTINGER_API_TOKEN = 'invalid-token';
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const provider = new HostingerProvider();
|
|
127
|
+
await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow(
|
|
128
|
+
'Hostinger API error',
|
|
129
|
+
);
|
|
130
|
+
} finally {
|
|
131
|
+
// Restore the token
|
|
132
|
+
process.env.HOSTINGER_API_TOKEN = savedToken;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should return empty array when no records exist', async () => {
|
|
137
|
+
const provider = new HostingerProvider();
|
|
138
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
139
|
+
|
|
140
|
+
expect(records).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return records from API', async () => {
|
|
144
|
+
// Set up mock records
|
|
145
|
+
mockRecords = [
|
|
146
|
+
{
|
|
147
|
+
name: 'api',
|
|
148
|
+
type: 'A',
|
|
149
|
+
ttl: 300,
|
|
150
|
+
records: [{ content: '1.2.3.4' }],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'www',
|
|
154
|
+
type: 'CNAME',
|
|
155
|
+
ttl: 300,
|
|
156
|
+
records: [{ content: 'example.com' }],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
const provider = new HostingerProvider();
|
|
161
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
162
|
+
|
|
163
|
+
expect(records).toHaveLength(2);
|
|
164
|
+
expect(records[0]).toEqual({
|
|
165
|
+
name: 'api',
|
|
166
|
+
type: 'A',
|
|
167
|
+
ttl: 300,
|
|
168
|
+
values: ['1.2.3.4'],
|
|
169
|
+
});
|
|
170
|
+
expect(records[1]).toEqual({
|
|
171
|
+
name: 'www',
|
|
172
|
+
type: 'CNAME',
|
|
173
|
+
ttl: 300,
|
|
174
|
+
values: ['example.com'],
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should handle records with multiple values', async () => {
|
|
179
|
+
mockRecords = [
|
|
180
|
+
{
|
|
181
|
+
name: 'mail',
|
|
182
|
+
type: 'MX',
|
|
183
|
+
ttl: 3600,
|
|
184
|
+
records: [
|
|
185
|
+
{ content: '10 mail1.example.com' },
|
|
186
|
+
{ content: '20 mail2.example.com' },
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const provider = new HostingerProvider();
|
|
192
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
193
|
+
|
|
194
|
+
expect(records).toHaveLength(1);
|
|
195
|
+
expect(records[0]?.values).toEqual([
|
|
196
|
+
'10 mail1.example.com',
|
|
197
|
+
'20 mail2.example.com',
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should cache API client after first call', async () => {
|
|
202
|
+
const provider = new HostingerProvider();
|
|
203
|
+
|
|
204
|
+
// Make two calls
|
|
205
|
+
await provider.getRecords(TEST_DOMAIN);
|
|
206
|
+
await provider.getRecords(TEST_DOMAIN);
|
|
207
|
+
|
|
208
|
+
// Both should succeed (API client is reused internally)
|
|
209
|
+
// If caching wasn't working, we'd see issues with token retrieval
|
|
210
|
+
expect(true).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('upsertRecords', () => {
|
|
215
|
+
it('should create new records', async () => {
|
|
216
|
+
const provider = new HostingerProvider();
|
|
217
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
218
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
219
|
+
]);
|
|
220
|
+
|
|
221
|
+
expect(results).toHaveLength(1);
|
|
222
|
+
expect(results[0]).toEqual({
|
|
223
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
224
|
+
created: true,
|
|
225
|
+
unchanged: false,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Verify record was added to mock store
|
|
229
|
+
expect(mockRecords).toHaveLength(1);
|
|
230
|
+
expect(mockRecords[0]).toEqual({
|
|
231
|
+
name: 'api',
|
|
232
|
+
type: 'A',
|
|
233
|
+
ttl: 300,
|
|
234
|
+
records: [{ content: '1.2.3.4' }],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should mark unchanged when record exists with same value', async () => {
|
|
239
|
+
// Pre-populate with existing record
|
|
240
|
+
mockRecords = [
|
|
241
|
+
{
|
|
242
|
+
name: 'api',
|
|
243
|
+
type: 'A',
|
|
244
|
+
ttl: 300,
|
|
245
|
+
records: [{ content: '1.2.3.4' }],
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const provider = new HostingerProvider();
|
|
250
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
251
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
expect(results).toHaveLength(1);
|
|
255
|
+
expect(results[0]).toEqual({
|
|
256
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
257
|
+
created: false,
|
|
258
|
+
unchanged: true,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should update record when value changes', async () => {
|
|
263
|
+
// Pre-populate with existing record
|
|
264
|
+
mockRecords = [
|
|
265
|
+
{
|
|
266
|
+
name: 'api',
|
|
267
|
+
type: 'A',
|
|
268
|
+
ttl: 300,
|
|
269
|
+
records: [{ content: '1.2.3.4' }],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const provider = new HostingerProvider();
|
|
274
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
275
|
+
{ name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
expect(results).toHaveLength(1);
|
|
279
|
+
expect(results[0]).toEqual({
|
|
280
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
|
|
281
|
+
created: false,
|
|
282
|
+
unchanged: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Verify record was updated in mock store
|
|
286
|
+
expect(mockRecords[0]?.records[0]?.content).toBe('5.6.7.8');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle multiple records with mixed states', async () => {
|
|
290
|
+
// Pre-populate with one existing record
|
|
291
|
+
mockRecords = [
|
|
292
|
+
{
|
|
293
|
+
name: 'api',
|
|
294
|
+
type: 'A',
|
|
295
|
+
ttl: 300,
|
|
296
|
+
records: [{ content: '1.2.3.4' }],
|
|
297
|
+
},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const provider = new HostingerProvider();
|
|
301
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
302
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' }, // Unchanged
|
|
303
|
+
{ name: 'www', type: 'A', ttl: 300, value: '1.2.3.4' }, // New
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
expect(results).toHaveLength(2);
|
|
307
|
+
expect(results[0]?.unchanged).toBe(true);
|
|
308
|
+
expect(results[1]?.created).toBe(true);
|
|
309
|
+
|
|
310
|
+
// Verify both records exist in mock store
|
|
311
|
+
expect(mockRecords).toHaveLength(2);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('API error handling', () => {
|
|
316
|
+
it('should handle API errors gracefully', async () => {
|
|
317
|
+
// Override the handler to return an error
|
|
318
|
+
server.use(
|
|
319
|
+
http.get(`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`, () => {
|
|
320
|
+
return HttpResponse.json(
|
|
321
|
+
{ message: 'Domain not found' },
|
|
322
|
+
{ status: 404 },
|
|
323
|
+
);
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const provider = new HostingerProvider();
|
|
328
|
+
|
|
329
|
+
await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow(
|
|
330
|
+
'Hostinger API error',
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle network errors', async () => {
|
|
335
|
+
// Override the handler to simulate network error
|
|
336
|
+
server.use(
|
|
337
|
+
http.get(`${HOSTINGER_API_BASE}/api/dns/v1/zones/:domain`, () => {
|
|
338
|
+
return HttpResponse.error();
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const provider = new HostingerProvider();
|
|
343
|
+
|
|
344
|
+
await expect(provider.getRecords(TEST_DOMAIN)).rejects.toThrow();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { LocalStateProvider } from '../LocalStateProvider';
|
|
6
|
+
import type { DokployStageState } from '../state';
|
|
7
|
+
|
|
8
|
+
describe('LocalStateProvider', () => {
|
|
9
|
+
let testDir: string;
|
|
10
|
+
let provider: LocalStateProvider;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
testDir = join(tmpdir(), `gkm-local-state-test-${Date.now()}`);
|
|
14
|
+
await mkdir(testDir, { recursive: true });
|
|
15
|
+
provider = new LocalStateProvider(testDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await rm(testDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('read', () => {
|
|
23
|
+
it('should return null when state file does not exist', async () => {
|
|
24
|
+
const state = await provider.read('nonexistent');
|
|
25
|
+
expect(state).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should read existing state file', async () => {
|
|
29
|
+
const stateData: DokployStageState = {
|
|
30
|
+
provider: 'dokploy',
|
|
31
|
+
stage: 'production',
|
|
32
|
+
environmentId: 'env_123',
|
|
33
|
+
applications: { api: 'app_123' },
|
|
34
|
+
services: { postgresId: 'pg_123' },
|
|
35
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await mkdir(join(testDir, '.gkm'), { recursive: true });
|
|
39
|
+
await writeFile(
|
|
40
|
+
join(testDir, '.gkm', 'deploy-production.json'),
|
|
41
|
+
JSON.stringify(stateData),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const state = await provider.read('production');
|
|
45
|
+
expect(state).toEqual(stateData);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return null for invalid JSON', async () => {
|
|
49
|
+
await mkdir(join(testDir, '.gkm'), { recursive: true });
|
|
50
|
+
await writeFile(
|
|
51
|
+
join(testDir, '.gkm', 'deploy-invalid.json'),
|
|
52
|
+
'not valid json',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const state = await provider.read('invalid');
|
|
56
|
+
expect(state).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('write', () => {
|
|
61
|
+
it('should create .gkm directory if not exists', async () => {
|
|
62
|
+
const state: DokployStageState = {
|
|
63
|
+
provider: 'dokploy',
|
|
64
|
+
stage: 'staging',
|
|
65
|
+
environmentId: 'env_456',
|
|
66
|
+
applications: {},
|
|
67
|
+
services: {},
|
|
68
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await provider.write('staging', state);
|
|
72
|
+
|
|
73
|
+
const content = await readFile(
|
|
74
|
+
join(testDir, '.gkm', 'deploy-staging.json'),
|
|
75
|
+
'utf-8',
|
|
76
|
+
);
|
|
77
|
+
expect(JSON.parse(content).stage).toBe('staging');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should update lastDeployedAt timestamp', async () => {
|
|
81
|
+
const state: DokployStageState = {
|
|
82
|
+
provider: 'dokploy',
|
|
83
|
+
stage: 'staging',
|
|
84
|
+
environmentId: 'env_456',
|
|
85
|
+
applications: {},
|
|
86
|
+
services: {},
|
|
87
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const originalTimestamp = state.lastDeployedAt;
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
92
|
+
await provider.write('staging', state);
|
|
93
|
+
|
|
94
|
+
expect(state.lastDeployedAt).not.toBe(originalTimestamp);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should preserve state data', async () => {
|
|
98
|
+
const state: DokployStageState = {
|
|
99
|
+
provider: 'dokploy',
|
|
100
|
+
stage: 'production',
|
|
101
|
+
environmentId: 'env_123',
|
|
102
|
+
applications: { api: 'app_123', web: 'app_456' },
|
|
103
|
+
services: { postgresId: 'pg_123', redisId: 'redis_123' },
|
|
104
|
+
appCredentials: { api: { dbUser: 'api', dbPassword: 'secret' } },
|
|
105
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await provider.write('production', state);
|
|
109
|
+
|
|
110
|
+
const content = await readFile(
|
|
111
|
+
join(testDir, '.gkm', 'deploy-production.json'),
|
|
112
|
+
'utf-8',
|
|
113
|
+
);
|
|
114
|
+
const parsed = JSON.parse(content);
|
|
115
|
+
|
|
116
|
+
expect(parsed.applications).toEqual({ api: 'app_123', web: 'app_456' });
|
|
117
|
+
expect(parsed.services).toEqual({
|
|
118
|
+
postgresId: 'pg_123',
|
|
119
|
+
redisId: 'redis_123',
|
|
120
|
+
});
|
|
121
|
+
expect(parsed.appCredentials).toEqual({
|
|
122
|
+
api: { dbUser: 'api', dbPassword: 'secret' },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|