@geekmidas/cli 0.10.0 → 0.13.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 +525 -0
- package/dist/bundler-B1qy9b-j.cjs +112 -0
- package/dist/bundler-B1qy9b-j.cjs.map +1 -0
- package/dist/bundler-DskIqW2t.mjs +111 -0
- package/dist/bundler-DskIqW2t.mjs.map +1 -0
- package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +1 -1
- package/dist/encryption-C8H-38Yy.mjs +42 -0
- package/dist/encryption-C8H-38Yy.mjs.map +1 -0
- package/dist/encryption-Dyf_r1h-.cjs +44 -0
- package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
- package/dist/index.cjs +2123 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2141 -192
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
- package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
- package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
- package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
- package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
- package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.d.cts.map +1 -1
- package/dist/openapi-react-query.d.mts.map +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +2 -2
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.cts.map +1 -1
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -1
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BOOpAF8N.cjs +5 -0
- package/dist/storage-Bj1E26lU.cjs +187 -0
- package/dist/storage-Bj1E26lU.cjs.map +1 -0
- package/dist/storage-kSxTjkNb.mjs +133 -0
- package/dist/storage-kSxTjkNb.mjs.map +1 -0
- package/dist/storage-tgZSUnKl.mjs +3 -0
- package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
- package/dist/types-BR0M2v_c.d.mts.map +1 -0
- package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
- package/dist/types-BhkZc-vm.d.cts.map +1 -0
- package/examples/cron-example.ts +27 -27
- package/examples/env.ts +27 -27
- package/examples/function-example.ts +31 -31
- package/examples/gkm.config.json +20 -20
- package/examples/gkm.config.ts +8 -8
- package/examples/gkm.minimal.config.json +5 -5
- package/examples/gkm.production.config.json +25 -25
- package/examples/logger.ts +2 -2
- package/package.json +6 -6
- package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
- package/src/__tests__/config.spec.ts +55 -55
- package/src/__tests__/loadEnvFiles.spec.ts +93 -93
- package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
- package/src/__tests__/openapi-react-query.spec.ts +497 -497
- package/src/__tests__/openapi.spec.ts +428 -428
- package/src/__tests__/test-helpers.ts +76 -76
- package/src/auth/__tests__/credentials.spec.ts +204 -0
- package/src/auth/__tests__/index.spec.ts +168 -0
- package/src/auth/credentials.ts +187 -0
- package/src/auth/index.ts +226 -0
- package/src/build/__tests__/bundler.spec.ts +444 -0
- package/src/build/__tests__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +210 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +260 -179
- package/src/build/manifests.ts +52 -52
- package/src/build/providerResolver.ts +145 -145
- package/src/build/types.ts +64 -43
- package/src/config.ts +39 -39
- package/src/deploy/__tests__/docker.spec.ts +111 -0
- package/src/deploy/__tests__/dokploy.spec.ts +245 -0
- package/src/deploy/__tests__/init.spec.ts +662 -0
- package/src/deploy/docker.ts +128 -0
- package/src/deploy/dokploy.ts +204 -0
- package/src/deploy/index.ts +136 -0
- package/src/deploy/init.ts +484 -0
- package/src/deploy/types.ts +48 -0
- package/src/dev/__tests__/index.spec.ts +266 -266
- package/src/dev/index.ts +647 -601
- package/src/docker/__tests__/compose.spec.ts +531 -0
- package/src/docker/__tests__/templates.spec.ts +280 -0
- package/src/docker/compose.ts +273 -0
- package/src/docker/index.ts +230 -0
- package/src/docker/templates.ts +446 -0
- package/src/generators/CronGenerator.ts +72 -72
- package/src/generators/EndpointGenerator.ts +699 -398
- package/src/generators/FunctionGenerator.ts +84 -84
- package/src/generators/Generator.ts +72 -72
- package/src/generators/OpenApiTsGenerator.ts +577 -577
- package/src/generators/SubscriberGenerator.ts +124 -124
- package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
- package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
- package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
- package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
- package/src/generators/index.ts +4 -4
- package/src/index.ts +623 -201
- package/src/init/__tests__/generators.spec.ts +334 -334
- package/src/init/__tests__/init.spec.ts +332 -332
- package/src/init/__tests__/utils.spec.ts +89 -89
- package/src/init/generators/config.ts +175 -175
- package/src/init/generators/docker.ts +41 -41
- package/src/init/generators/env.ts +72 -72
- package/src/init/generators/index.ts +1 -1
- package/src/init/generators/models.ts +64 -64
- package/src/init/generators/monorepo.ts +161 -161
- package/src/init/generators/package.ts +71 -71
- package/src/init/generators/source.ts +6 -6
- package/src/init/index.ts +203 -208
- package/src/init/templates/api.ts +115 -115
- package/src/init/templates/index.ts +75 -75
- package/src/init/templates/minimal.ts +98 -98
- package/src/init/templates/serverless.ts +89 -89
- package/src/init/templates/worker.ts +98 -98
- package/src/init/utils.ts +54 -56
- package/src/openapi-react-query.ts +194 -194
- package/src/openapi.ts +63 -63
- package/src/secrets/__tests__/encryption.spec.ts +226 -0
- package/src/secrets/__tests__/generator.spec.ts +319 -0
- package/src/secrets/__tests__/index.spec.ts +91 -0
- package/src/secrets/__tests__/storage.spec.ts +611 -0
- package/src/secrets/encryption.ts +91 -0
- package/src/secrets/generator.ts +164 -0
- package/src/secrets/index.ts +383 -0
- package/src/secrets/storage.ts +192 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsdown.config.ts +11 -8
- package/dist/config-BrkUalUh.mjs.map +0 -1
- package/dist/config-C9aXOHBe.cjs.map +0 -1
- package/dist/openapi-BeHLKcwP.cjs.map +0 -1
- package/dist/openapi-CZLI4QTr.mjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
- package/dist/types-DXgiA1sF.d.mts.map +0 -1
- package/dist/types-b-vwGpqc.d.cts.map +0 -1
|
@@ -2,8 +2,8 @@ import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
CronBuilder,
|
|
6
|
+
type ScheduleExpression,
|
|
7
7
|
} from '@geekmidas/constructs/crons';
|
|
8
8
|
import { e } from '@geekmidas/constructs/endpoints';
|
|
9
9
|
import { z } from 'zod';
|
|
@@ -12,50 +12,50 @@ import { z } from 'zod';
|
|
|
12
12
|
* Creates a temporary directory for testing
|
|
13
13
|
*/
|
|
14
14
|
export async function createTempDir(prefix = 'cli-test-'): Promise<string> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
const tempPath = join(
|
|
16
|
+
tmpdir(),
|
|
17
|
+
`${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
18
|
+
);
|
|
19
|
+
await mkdir(tempPath, { recursive: true });
|
|
20
|
+
return tempPath;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Cleans up a directory
|
|
25
25
|
*/
|
|
26
26
|
export async function cleanupDir(path: string): Promise<void> {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
try {
|
|
28
|
+
await rm(path, { recursive: true, force: true });
|
|
29
|
+
} catch (_error) {
|
|
30
|
+
// Ignore errors during cleanup
|
|
31
|
+
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Creates a test file with content
|
|
36
36
|
*/
|
|
37
37
|
export async function createTestFile(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
dir: string,
|
|
39
|
+
filename: string,
|
|
40
|
+
content: string,
|
|
41
41
|
): Promise<string> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const filePath = join(dir, filename);
|
|
43
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
44
|
+
await writeFile(filePath, content);
|
|
45
|
+
return filePath;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Creates a mock endpoint file with real endpoint construct
|
|
50
50
|
*/
|
|
51
51
|
export async function createMockEndpointFile(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
dir: string,
|
|
53
|
+
filename: string,
|
|
54
|
+
exportName: string,
|
|
55
|
+
path: string = '/test',
|
|
56
|
+
method: string = 'GET',
|
|
57
57
|
): Promise<string> {
|
|
58
|
-
|
|
58
|
+
const content = `
|
|
59
59
|
import { e } from '@geekmidas/constructs/endpoints';
|
|
60
60
|
import { z } from 'zod';
|
|
61
61
|
|
|
@@ -64,19 +64,19 @@ export const ${exportName} = e
|
|
|
64
64
|
.output(z.object({ message: z.string() }))
|
|
65
65
|
.handle(async () => ({ message: 'Hello from ${exportName}' }));
|
|
66
66
|
`;
|
|
67
|
-
|
|
67
|
+
return createTestFile(dir, filename, content);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Creates a mock function file with real function construct
|
|
72
72
|
*/
|
|
73
73
|
export async function createMockFunctionFile(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
dir: string,
|
|
75
|
+
filename: string,
|
|
76
|
+
exportName: string,
|
|
77
|
+
timeout = 30,
|
|
78
78
|
): Promise<string> {
|
|
79
|
-
|
|
79
|
+
const content = `
|
|
80
80
|
import { f } from '@geekmidas/constructs/functions';
|
|
81
81
|
import { z } from 'zod';
|
|
82
82
|
|
|
@@ -86,19 +86,19 @@ export const ${exportName} = f
|
|
|
86
86
|
.timeout(${timeout})
|
|
87
87
|
.handle(async ({ input }) => ({ greeting: \`Hello, \${input.name}!\` }));
|
|
88
88
|
`;
|
|
89
|
-
|
|
89
|
+
return createTestFile(dir, filename, content);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Creates a mock cron file with real cron construct
|
|
94
94
|
*/
|
|
95
95
|
export async function createMockCronFile(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
dir: string,
|
|
97
|
+
filename: string,
|
|
98
|
+
exportName: string,
|
|
99
|
+
schedule = 'rate(1 hour)',
|
|
100
100
|
): Promise<string> {
|
|
101
|
-
|
|
101
|
+
const content = `
|
|
102
102
|
import { CronBuilder } from '@geekmidas/constructs/crons';
|
|
103
103
|
import { z } from 'zod';
|
|
104
104
|
|
|
@@ -110,70 +110,70 @@ export const ${exportName} = new CronBuilder()
|
|
|
110
110
|
return { processed: 10 };
|
|
111
111
|
});
|
|
112
112
|
`;
|
|
113
|
-
|
|
113
|
+
return createTestFile(dir, filename, content);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Helper functions to create real constructs for testing
|
|
118
118
|
*/
|
|
119
119
|
export function createTestEndpoint(path: string, method: HttpMethod = 'GET') {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
const m = method.toLowerCase() as Lowercase<HttpMethod>;
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
const builder = (e as any)[m](path);
|
|
123
|
+
builder.output(z.object({ message: z.string() }));
|
|
124
|
+
return builder.handle(async () => ({ message: `Hello from ${path}` }));
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function createTestFunction(timeout: number = 30) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
const builder = new FunctionBuilder();
|
|
129
|
+
builder.input(z.object({ name: z.string() }));
|
|
130
|
+
builder.output(z.object({ greeting: z.string() }));
|
|
131
|
+
builder.timeout(timeout);
|
|
132
|
+
return builder.handle(async ({ input }: any) => ({
|
|
133
|
+
greeting: `Hello, ${input.name}!`,
|
|
134
|
+
}));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function createTestCron(
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
schedule: ScheduleExpression = 'rate(1 hour)',
|
|
139
|
+
timeout: number = 30,
|
|
140
140
|
) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const builder = new CronBuilder();
|
|
142
|
+
builder.schedule(schedule);
|
|
143
|
+
builder.output(z.object({ processed: z.number() }));
|
|
144
|
+
builder.timeout(timeout);
|
|
145
|
+
return builder.handle(async () => {
|
|
146
|
+
return { processed: 10 };
|
|
147
|
+
});
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
151
|
* Creates a mock build context
|
|
152
152
|
*/
|
|
153
153
|
export function createMockBuildContext() {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
return {
|
|
155
|
+
envParserPath: './env',
|
|
156
|
+
envParserImportPattern: 'envParser',
|
|
157
|
+
loggerPath: './logger',
|
|
158
|
+
loggerImportPattern: 'logger',
|
|
159
|
+
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* Waits for a condition to be true
|
|
164
164
|
*/
|
|
165
165
|
export async function waitFor(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
condition: () => boolean,
|
|
167
|
+
timeout = 5000,
|
|
168
|
+
interval = 100,
|
|
169
169
|
): Promise<void> {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
170
|
+
const start = Date.now();
|
|
171
|
+
while (!condition() && Date.now() - start < timeout) {
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
173
|
+
}
|
|
174
|
+
if (!condition()) {
|
|
175
|
+
throw new Error('Timeout waiting for condition');
|
|
176
|
+
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
import { dirname } from 'node:path';
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getCredentialsDir,
|
|
9
|
+
getCredentialsPath,
|
|
10
|
+
getDokployCredentials,
|
|
11
|
+
getDokployToken,
|
|
12
|
+
readCredentials,
|
|
13
|
+
removeDokployCredentials,
|
|
14
|
+
storeDokployCredentials,
|
|
15
|
+
writeCredentials,
|
|
16
|
+
} from '../credentials';
|
|
17
|
+
|
|
18
|
+
describe('credentials storage', () => {
|
|
19
|
+
let tempDir: string;
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tempDir = join(tmpdir(), `gkm-auth-test-${Date.now()}`);
|
|
23
|
+
await mkdir(tempDir, { recursive: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (existsSync(tempDir)) {
|
|
28
|
+
await rm(tempDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('path utilities', () => {
|
|
33
|
+
it('should return credentials dir in specified root directory', () => {
|
|
34
|
+
const dir = getCredentialsDir({ root: tempDir });
|
|
35
|
+
expect(dir).toBe(join(tempDir, '.gkm'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return credentials path', () => {
|
|
39
|
+
const path = getCredentialsPath({ root: tempDir });
|
|
40
|
+
expect(path).toBe(join(tempDir, '.gkm', 'credentials.json'));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('readCredentials / writeCredentials', () => {
|
|
45
|
+
it('should return empty object when no credentials file exists', async () => {
|
|
46
|
+
const creds = await readCredentials({ root: tempDir });
|
|
47
|
+
expect(creds).toEqual({});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should write and read credentials', async () => {
|
|
51
|
+
const credentials = {
|
|
52
|
+
dokploy: {
|
|
53
|
+
token: 'test-token',
|
|
54
|
+
endpoint: 'https://dokploy.example.com',
|
|
55
|
+
storedAt: new Date().toISOString(),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
await writeCredentials(credentials, { root: tempDir });
|
|
60
|
+
const read = await readCredentials({ root: tempDir });
|
|
61
|
+
|
|
62
|
+
expect(read).toEqual(credentials);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should create credentials directory if it does not exist', async () => {
|
|
66
|
+
const credentials = {
|
|
67
|
+
dokploy: {
|
|
68
|
+
token: 'test',
|
|
69
|
+
endpoint: 'https://test.com',
|
|
70
|
+
storedAt: new Date().toISOString(),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
await writeCredentials(credentials, { root: tempDir });
|
|
75
|
+
|
|
76
|
+
expect(existsSync(join(tempDir, '.gkm'))).toBe(true);
|
|
77
|
+
expect(existsSync(join(tempDir, '.gkm', 'credentials.json'))).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should set secure file permissions', async () => {
|
|
81
|
+
const credentials = {
|
|
82
|
+
dokploy: {
|
|
83
|
+
token: 'test',
|
|
84
|
+
endpoint: 'https://test.com',
|
|
85
|
+
storedAt: new Date().toISOString(),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await writeCredentials(credentials, { root: tempDir });
|
|
90
|
+
|
|
91
|
+
// Verify the file was created (we can't easily check permissions in tests)
|
|
92
|
+
const content = await readFile(
|
|
93
|
+
getCredentialsPath({ root: tempDir }),
|
|
94
|
+
'utf-8',
|
|
95
|
+
);
|
|
96
|
+
expect(JSON.parse(content)).toEqual(credentials);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('storeDokployCredentials', () => {
|
|
101
|
+
it('should store dokploy credentials', async () => {
|
|
102
|
+
await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
|
|
103
|
+
root: tempDir,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const creds = await readCredentials({ root: tempDir });
|
|
107
|
+
expect(creds.dokploy).toBeDefined();
|
|
108
|
+
expect(creds.dokploy!.token).toBe('my-token');
|
|
109
|
+
expect(creds.dokploy!.endpoint).toBe('https://dokploy.example.com');
|
|
110
|
+
expect(creds.dokploy!.storedAt).toBeDefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should overwrite existing credentials', async () => {
|
|
114
|
+
await storeDokployCredentials('old-token', 'https://old.com', {
|
|
115
|
+
root: tempDir,
|
|
116
|
+
});
|
|
117
|
+
await storeDokployCredentials('new-token', 'https://new.com', {
|
|
118
|
+
root: tempDir,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
122
|
+
expect(creds!.token).toBe('new-token');
|
|
123
|
+
expect(creds!.endpoint).toBe('https://new.com');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('getDokployCredentials', () => {
|
|
128
|
+
it('should return null when no credentials stored', async () => {
|
|
129
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
130
|
+
expect(creds).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return stored credentials', async () => {
|
|
134
|
+
await storeDokployCredentials('test-token', 'https://test.com', {
|
|
135
|
+
root: tempDir,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
139
|
+
expect(creds).toEqual({
|
|
140
|
+
token: 'test-token',
|
|
141
|
+
endpoint: 'https://test.com',
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('removeDokployCredentials', () => {
|
|
147
|
+
it('should return false when no credentials to remove', async () => {
|
|
148
|
+
const removed = await removeDokployCredentials({ root: tempDir });
|
|
149
|
+
expect(removed).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should remove dokploy credentials', async () => {
|
|
153
|
+
await storeDokployCredentials('test-token', 'https://test.com', {
|
|
154
|
+
root: tempDir,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const removed = await removeDokployCredentials({ root: tempDir });
|
|
158
|
+
expect(removed).toBe(true);
|
|
159
|
+
|
|
160
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
161
|
+
expect(creds).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('getDokployToken', () => {
|
|
166
|
+
it('should return null when no token available', async () => {
|
|
167
|
+
const token = await getDokployToken({ root: tempDir });
|
|
168
|
+
expect(token).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return stored token', async () => {
|
|
172
|
+
await storeDokployCredentials('stored-token', 'https://test.com', {
|
|
173
|
+
root: tempDir,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const token = await getDokployToken({ root: tempDir });
|
|
177
|
+
expect(token).toBe('stored-token');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should prefer environment variable over stored token', async () => {
|
|
181
|
+
await storeDokployCredentials('stored-token', 'https://test.com', {
|
|
182
|
+
root: tempDir,
|
|
183
|
+
});
|
|
184
|
+
process.env.DOKPLOY_API_TOKEN = 'env-token';
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const token = await getDokployToken({ root: tempDir });
|
|
188
|
+
expect(token).toBe('env-token');
|
|
189
|
+
} finally {
|
|
190
|
+
delete process.env.DOKPLOY_API_TOKEN;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should fall back to stored token when env var not set', async () => {
|
|
195
|
+
delete process.env.DOKPLOY_API_TOKEN;
|
|
196
|
+
await storeDokployCredentials('stored-token', 'https://test.com', {
|
|
197
|
+
root: tempDir,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const token = await getDokployToken({ root: tempDir });
|
|
201
|
+
expect(token).toBe('stored-token');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { HttpResponse, http } from 'msw';
|
|
6
|
+
import { setupServer } from 'msw/node';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
getDokployCredentials,
|
|
11
|
+
removeDokployCredentials,
|
|
12
|
+
storeDokployCredentials,
|
|
13
|
+
} from '../credentials';
|
|
14
|
+
import { maskToken, validateDokployToken } from '../index';
|
|
15
|
+
|
|
16
|
+
// MSW server for mocking API calls
|
|
17
|
+
const server = setupServer();
|
|
18
|
+
|
|
19
|
+
describe('auth commands', () => {
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tempDir = join(tmpdir(), `gkm-auth-cmd-test-${Date.now()}`);
|
|
24
|
+
await mkdir(tempDir, { recursive: true });
|
|
25
|
+
server.listen({ onUnhandledRequest: 'bypass' });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
server.resetHandlers();
|
|
30
|
+
server.close();
|
|
31
|
+
if (existsSync(tempDir)) {
|
|
32
|
+
await rm(tempDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('validateDokployToken', () => {
|
|
37
|
+
it('should return true for valid token', async () => {
|
|
38
|
+
server.use(
|
|
39
|
+
http.get('https://dokploy.example.com/api/project.all', () => {
|
|
40
|
+
return HttpResponse.json([{ projectId: 'proj_1', name: 'Test' }]);
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const result = await validateDokployToken(
|
|
45
|
+
'https://dokploy.example.com',
|
|
46
|
+
'valid-token',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(result).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return false for invalid token (401)', async () => {
|
|
53
|
+
server.use(
|
|
54
|
+
http.get('https://dokploy.example.com/api/project.all', () => {
|
|
55
|
+
return new HttpResponse(null, { status: 401 });
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = await validateDokployToken(
|
|
60
|
+
'https://dokploy.example.com',
|
|
61
|
+
'invalid-token',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should return false for server error (500)', async () => {
|
|
68
|
+
server.use(
|
|
69
|
+
http.get('https://dokploy.example.com/api/project.all', () => {
|
|
70
|
+
return new HttpResponse(null, { status: 500 });
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const result = await validateDokployToken(
|
|
75
|
+
'https://dokploy.example.com',
|
|
76
|
+
'token',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return false on network error', async () => {
|
|
83
|
+
server.use(
|
|
84
|
+
http.get('https://dokploy.example.com/api/project.all', () => {
|
|
85
|
+
return HttpResponse.error();
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const result = await validateDokployToken(
|
|
90
|
+
'https://dokploy.example.com',
|
|
91
|
+
'token',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('maskToken', () => {
|
|
99
|
+
it('should mask long tokens showing first and last 4 chars', () => {
|
|
100
|
+
expect(maskToken('abcdefghijklmnop')).toBe('abcd...mnop');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should fully mask tokens 8 chars or less', () => {
|
|
104
|
+
expect(maskToken('short')).toBe('****');
|
|
105
|
+
expect(maskToken('12345678')).toBe('****');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle exactly 9 character token', () => {
|
|
109
|
+
expect(maskToken('123456789')).toBe('1234...6789');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle very long tokens', () => {
|
|
113
|
+
const longToken = 'a'.repeat(100);
|
|
114
|
+
expect(maskToken(longToken)).toBe('aaaa...aaaa');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle empty string', () => {
|
|
118
|
+
expect(maskToken('')).toBe('****');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('credentials flow', () => {
|
|
123
|
+
it('should store and retrieve credentials', async () => {
|
|
124
|
+
await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
|
|
125
|
+
root: tempDir,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
129
|
+
expect(creds).not.toBeNull();
|
|
130
|
+
expect(creds!.token).toBe('my-token');
|
|
131
|
+
expect(creds!.endpoint).toBe('https://dokploy.example.com');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should remove credentials', async () => {
|
|
135
|
+
await storeDokployCredentials('my-token', 'https://dokploy.example.com', {
|
|
136
|
+
root: tempDir,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const removed = await removeDokployCredentials({ root: tempDir });
|
|
140
|
+
expect(removed).toBe(true);
|
|
141
|
+
|
|
142
|
+
const creds = await getDokployCredentials({ root: tempDir });
|
|
143
|
+
expect(creds).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return false when removing non-existent credentials', async () => {
|
|
147
|
+
const removed = await removeDokployCredentials({ root: tempDir });
|
|
148
|
+
expect(removed).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('URL normalization', () => {
|
|
154
|
+
it('should remove trailing slash from endpoint', () => {
|
|
155
|
+
const endpoint = 'https://dokploy.example.com/'.replace(/\/$/, '');
|
|
156
|
+
expect(endpoint).toBe('https://dokploy.example.com');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should not modify endpoint without trailing slash', () => {
|
|
160
|
+
const endpoint = 'https://dokploy.example.com'.replace(/\/$/, '');
|
|
161
|
+
expect(endpoint).toBe('https://dokploy.example.com');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should validate URL format', () => {
|
|
165
|
+
expect(() => new URL('https://dokploy.example.com')).not.toThrow();
|
|
166
|
+
expect(() => new URL('invalid-url')).toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|