@geekmidas/cli 0.9.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +525 -0
- package/dist/bundler-DRXCw_YR.mjs +70 -0
- package/dist/bundler-DRXCw_YR.mjs.map +1 -0
- package/dist/bundler-WsEvH_b2.cjs +71 -0
- package/dist/bundler-WsEvH_b2.cjs.map +1 -0
- package/dist/{config-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
- package/dist/config-AmInkU7k.cjs.map +1 -0
- package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
- package/dist/config-DYULeEv8.mjs.map +1 -0
- package/dist/config.cjs +1 -1
- package/dist/config.d.cts +2 -1
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +2 -1
- package/dist/config.d.mts.map +1 -0
- 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 +2125 -184
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2143 -197
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
- package/dist/openapi-BfFlOBCG.mjs.map +1 -0
- package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
- 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 -0
- package/dist/openapi-react-query.d.mts.map +1 -0
- 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 -0
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.d.mts.map +1 -0
- package/dist/openapi.mjs +2 -2
- package/dist/storage-BUYQJgz7.cjs +4 -0
- package/dist/storage-BXoJvmv2.cjs +149 -0
- package/dist/storage-BXoJvmv2.cjs.map +1 -0
- package/dist/storage-C9PU_30f.mjs +101 -0
- package/dist/storage-C9PU_30f.mjs.map +1 -0
- package/dist/storage-DLJAYxzJ.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 +77 -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__/index-new.spec.ts +474 -474
- package/src/build/__tests__/manifests.spec.ts +333 -333
- package/src/build/bundler.ts +141 -0
- package/src/build/endpoint-analyzer.ts +236 -0
- package/src/build/handler-templates.ts +1253 -0
- package/src/build/index.ts +250 -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 -37
- 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 -593
- 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 +589 -589
- 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 +628 -206
- 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 +403 -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 +134 -0
- package/src/secrets/types.ts +53 -0
- package/src/types.ts +295 -176
- package/tsconfig.json +9 -0
- package/tsdown.config.ts +11 -8
- package/dist/config-Bq72aj8e.mjs.map +0 -1
- package/dist/config-CFls09Ey.cjs.map +0 -1
- package/dist/openapi--vOy9mo4.mjs.map +0 -1
- package/dist/openapi-CHhTPief.cjs.map +0 -1
- package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
- package/dist/openapi-react-query-o5iMi8tz.cjs.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,71 +110,72 @@ 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
|
-
|
|
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}` }));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
export function createTestFunction(timeout: number = 30) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}));
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
export function createTestCron(
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
schedule: ScheduleExpression = 'rate(1 hour)',
|
|
139
|
+
timeout: number = 30,
|
|
139
140
|
) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
});
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
151
|
* Creates a mock build context
|
|
151
152
|
*/
|
|
152
153
|
export function createMockBuildContext() {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
return {
|
|
155
|
+
envParserPath: './env',
|
|
156
|
+
envParserImportPattern: 'envParser',
|
|
157
|
+
loggerPath: './logger',
|
|
158
|
+
loggerImportPattern: 'logger',
|
|
159
|
+
};
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
/**
|
|
162
163
|
* Waits for a condition to be true
|
|
163
164
|
*/
|
|
164
165
|
export async function waitFor(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
condition: () => boolean,
|
|
167
|
+
timeout = 5000,
|
|
168
|
+
interval = 100,
|
|
168
169
|
): Promise<void> {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
import { dirname } from 'node:path';
|
|
179
180
|
import { FunctionBuilder } from '@geekmidas/constructs/functions';
|
|
180
|
-
import type { HttpMethod } from '
|
|
181
|
+
import type { HttpMethod } from '@geekmidas/constructs/types';
|
|
@@ -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
|
+
});
|