@elench/testkit 0.1.37 → 0.1.38
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/lib/config/index.mjs +8 -2
- package/lib/runner/state-io.mjs +23 -0
- package/lib/runner/template.mjs +68 -8
- package/lib/runner/template.test.mjs +33 -13
- package/lib/setup/index.d.ts +2 -1
- package/package.json +1 -1
package/lib/config/index.mjs
CHANGED
|
@@ -101,7 +101,10 @@ function normalizeServiceConfig({
|
|
|
101
101
|
}) {
|
|
102
102
|
const local = normalizeLocalConfig(name, explicitService, discoveredService, productDir);
|
|
103
103
|
const envFiles = inferEnvFiles(productDir, explicitService, local);
|
|
104
|
-
const serviceEnv =
|
|
104
|
+
const serviceEnv = {
|
|
105
|
+
...loadServiceEnv(productDir, envFiles),
|
|
106
|
+
...(explicitService.env || {}),
|
|
107
|
+
};
|
|
105
108
|
const database = normalizeDatabaseConfig(explicitService, name);
|
|
106
109
|
const migrate = normalizeLifecycle(explicitService.migrate);
|
|
107
110
|
const seed = normalizeLifecycle(explicitService.seed);
|
|
@@ -151,7 +154,10 @@ function normalizeServiceConfig({
|
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
function normalizeLocalConfig(name, explicitService, discoveredService, productDir) {
|
|
154
|
-
if (explicitService
|
|
157
|
+
if (Object.prototype.hasOwnProperty.call(explicitService, "local")) {
|
|
158
|
+
if (explicitService.local === false) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
155
161
|
return {
|
|
156
162
|
...explicitService.local,
|
|
157
163
|
cwd: explicitService.local.cwd || ".",
|
package/lib/runner/state-io.mjs
CHANGED
|
@@ -5,6 +5,29 @@ export function readDatabaseUrl(stateDir) {
|
|
|
5
5
|
return readStateValue(path.join(stateDir, "database_url"));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export function readDatabaseInfo(stateDir) {
|
|
9
|
+
return parseDatabaseUrl(readDatabaseUrl(stateDir));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseDatabaseUrl(databaseUrl) {
|
|
13
|
+
if (!databaseUrl) return null;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(databaseUrl);
|
|
17
|
+
const port = Number(parsed.port || "5432");
|
|
18
|
+
return {
|
|
19
|
+
url: databaseUrl,
|
|
20
|
+
host: parsed.hostname,
|
|
21
|
+
port: Number.isInteger(port) && port > 0 ? port : 5432,
|
|
22
|
+
database: parsed.pathname.replace(/^\//, ""),
|
|
23
|
+
user: decodeURIComponent(parsed.username || ""),
|
|
24
|
+
password: decodeURIComponent(parsed.password || ""),
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
8
31
|
export function readStateValue(filePath) {
|
|
9
32
|
if (!fs.existsSync(filePath)) return null;
|
|
10
33
|
return fs.readFileSync(filePath, "utf8").trim();
|
package/lib/runner/template.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { readDatabaseInfo } from "./state-io.mjs";
|
|
2
3
|
|
|
3
4
|
const PORT_STRIDE = 100;
|
|
4
5
|
|
|
@@ -6,6 +7,14 @@ export function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, worker
|
|
|
6
7
|
const portMap = buildPortMap(runtimeConfigs, workerId);
|
|
7
8
|
const baseUrlByService = new Map();
|
|
8
9
|
const readyUrlByService = new Map();
|
|
10
|
+
const stateDirByService = new Map();
|
|
11
|
+
|
|
12
|
+
for (const config of runtimeConfigs) {
|
|
13
|
+
stateDirByService.set(
|
|
14
|
+
config.name,
|
|
15
|
+
resolveServiceStateDir(workerStateDir, targetConfig.name, config)
|
|
16
|
+
);
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
for (const config of runtimeConfigs) {
|
|
11
20
|
if (!config.testkit.local) continue;
|
|
@@ -16,6 +25,7 @@ export function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, worker
|
|
|
16
25
|
portMap,
|
|
17
26
|
baseUrlByService,
|
|
18
27
|
readyUrlByService,
|
|
28
|
+
stateDirByService,
|
|
19
29
|
})
|
|
20
30
|
);
|
|
21
31
|
readyUrlByService.set(
|
|
@@ -25,6 +35,7 @@ export function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, worker
|
|
|
25
35
|
portMap,
|
|
26
36
|
baseUrlByService,
|
|
27
37
|
readyUrlByService,
|
|
38
|
+
stateDirByService,
|
|
28
39
|
})
|
|
29
40
|
);
|
|
30
41
|
}
|
|
@@ -51,6 +62,7 @@ export function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, worker
|
|
|
51
62
|
portMap,
|
|
52
63
|
baseUrlByService,
|
|
53
64
|
readyUrlByService,
|
|
65
|
+
stateDirByService,
|
|
54
66
|
urlMappings
|
|
55
67
|
)
|
|
56
68
|
);
|
|
@@ -90,6 +102,7 @@ export function resolveWorkerConfig(
|
|
|
90
102
|
portMap,
|
|
91
103
|
baseUrlByService,
|
|
92
104
|
readyUrlByService,
|
|
105
|
+
stateDirByService,
|
|
93
106
|
urlMappings
|
|
94
107
|
) {
|
|
95
108
|
const stateDir = resolveServiceStateDir(workerStateDir, targetConfig.name, config);
|
|
@@ -101,6 +114,7 @@ export function resolveWorkerConfig(
|
|
|
101
114
|
portMap,
|
|
102
115
|
baseUrlByService,
|
|
103
116
|
readyUrlByService,
|
|
117
|
+
stateDirByService,
|
|
104
118
|
urlMappings,
|
|
105
119
|
};
|
|
106
120
|
|
|
@@ -143,12 +157,7 @@ export function resolveWorkerConfig(
|
|
|
143
157
|
port: portMap.get(config.name) || config.testkit.local.port,
|
|
144
158
|
baseUrl: baseUrlByService.get(config.name),
|
|
145
159
|
readyUrl: readyUrlByService.get(config.name),
|
|
146
|
-
env:
|
|
147
|
-
Object.entries(config.testkit.local.env || {}).map(([key, value]) => [
|
|
148
|
-
key,
|
|
149
|
-
finalizeString(String(value), context),
|
|
150
|
-
])
|
|
151
|
-
),
|
|
160
|
+
env: { ...(config.testkit.local.env || {}) },
|
|
152
161
|
}
|
|
153
162
|
: undefined;
|
|
154
163
|
|
|
@@ -163,6 +172,7 @@ export function resolveWorkerConfig(
|
|
|
163
172
|
database,
|
|
164
173
|
migrate,
|
|
165
174
|
seed,
|
|
175
|
+
templateContext: context,
|
|
166
176
|
local,
|
|
167
177
|
},
|
|
168
178
|
};
|
|
@@ -182,10 +192,11 @@ export function getWorkerServiceStateDir(workerStateDir, targetName, serviceName
|
|
|
182
192
|
|
|
183
193
|
export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
|
|
184
194
|
const inheritedEnv = { ...processEnv };
|
|
195
|
+
const templateContext = config.testkit?.templateContext;
|
|
185
196
|
const env = {
|
|
186
197
|
...inheritedEnv,
|
|
187
|
-
...(config.testkit.serviceEnv || {}),
|
|
188
|
-
...extraEnv,
|
|
198
|
+
...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
|
|
199
|
+
...resolveEnvTemplates(extraEnv, templateContext),
|
|
189
200
|
TESTKIT_ACTIVE: "1",
|
|
190
201
|
...(config.workerId ? { TESTKIT_WORKER_ID: String(config.workerId) } : {}),
|
|
191
202
|
};
|
|
@@ -266,12 +277,61 @@ export function resolveTemplateString(value, context) {
|
|
|
266
277
|
}
|
|
267
278
|
return readyUrl;
|
|
268
279
|
}
|
|
280
|
+
case "dbUrl":
|
|
281
|
+
case "dbHost":
|
|
282
|
+
case "dbPort":
|
|
283
|
+
case "dbName":
|
|
284
|
+
case "dbUser":
|
|
285
|
+
case "dbPassword": {
|
|
286
|
+
const serviceName = arg || context.serviceName;
|
|
287
|
+
return resolveDatabaseTemplateValue(token, serviceName, context);
|
|
288
|
+
}
|
|
269
289
|
default:
|
|
270
290
|
throw new Error(`Unsupported template token "{${token}${arg ? `:${arg}` : ""}}"`);
|
|
271
291
|
}
|
|
272
292
|
});
|
|
273
293
|
}
|
|
274
294
|
|
|
295
|
+
function resolveEnvTemplates(values, templateContext) {
|
|
296
|
+
return Object.fromEntries(
|
|
297
|
+
Object.entries(values || {}).map(([key, value]) => [
|
|
298
|
+
key,
|
|
299
|
+
typeof value === "string" && templateContext ? finalizeString(value, templateContext) : value,
|
|
300
|
+
])
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function resolveDatabaseTemplateValue(token, serviceName, context) {
|
|
305
|
+
const stateDir = context.stateDirByService?.get(serviceName);
|
|
306
|
+
if (!stateDir) {
|
|
307
|
+
throw new Error(`Unknown database placeholder for service "${serviceName}"`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const info = readDatabaseInfo(stateDir);
|
|
311
|
+
if (!info) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Database placeholder "{${token}:${serviceName}}" is unavailable before "${serviceName}" database preparation`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
switch (token) {
|
|
318
|
+
case "dbUrl":
|
|
319
|
+
return info.url;
|
|
320
|
+
case "dbHost":
|
|
321
|
+
return info.host;
|
|
322
|
+
case "dbPort":
|
|
323
|
+
return String(info.port);
|
|
324
|
+
case "dbName":
|
|
325
|
+
return info.database;
|
|
326
|
+
case "dbUser":
|
|
327
|
+
return info.user;
|
|
328
|
+
case "dbPassword":
|
|
329
|
+
return info.password;
|
|
330
|
+
default:
|
|
331
|
+
throw new Error(`Unsupported database placeholder "{${token}:${serviceName}}"`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
275
335
|
export function rewriteUrlPort(rawUrl, port) {
|
|
276
336
|
try {
|
|
277
337
|
const original = new URL(rawUrl);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
1
4
|
import { describe, expect, it } from "vitest";
|
|
2
5
|
import {
|
|
3
6
|
buildExecutionEnv,
|
|
@@ -51,6 +54,14 @@ describe("runner-template", () => {
|
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
it("resolves template strings and URL rewrites", () => {
|
|
57
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-template-"));
|
|
58
|
+
const apiStateDir = path.join(tmpDir, "api");
|
|
59
|
+
fs.mkdirSync(apiStateDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(apiStateDir, "database_url"),
|
|
62
|
+
"postgres://testkit:testkit@127.0.0.1:55432/onix_db"
|
|
63
|
+
);
|
|
64
|
+
|
|
54
65
|
const context = {
|
|
55
66
|
workerId: 2,
|
|
56
67
|
targetName: "frontend",
|
|
@@ -62,11 +73,15 @@ describe("runner-template", () => {
|
|
|
62
73
|
]),
|
|
63
74
|
baseUrlByService: new Map([["api", "http://127.0.0.1:3100"]]),
|
|
64
75
|
readyUrlByService: new Map([["api", "http://127.0.0.1:3100/health"]]),
|
|
76
|
+
stateDirByService: new Map([["api", apiStateDir]]),
|
|
65
77
|
urlMappings: [["http://api:3000", "http://127.0.0.1:3100"]],
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
expect(resolveTemplateString("{worker}:{target}:{service}", context)).toBe("2:frontend:frontend");
|
|
69
81
|
expect(resolveTemplateString("{baseUrl:api}", context)).toBe("http://127.0.0.1:3100");
|
|
82
|
+
expect(resolveTemplateString("{dbHost:api}:{dbPort:api}/{dbName:api}", context)).toBe(
|
|
83
|
+
"127.0.0.1:55432/onix_db"
|
|
84
|
+
);
|
|
70
85
|
expect(finalizeString("API={baseUrl:api} OLD=http://api:3000", context)).toBe(
|
|
71
86
|
"API=http://127.0.0.1:3100 OLD=http://127.0.0.1:3100"
|
|
72
87
|
);
|
|
@@ -76,6 +91,7 @@ describe("runner-template", () => {
|
|
|
76
91
|
});
|
|
77
92
|
|
|
78
93
|
it("builds worker runtime configs and execution env", () => {
|
|
94
|
+
const workerStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-worker-"));
|
|
79
95
|
const api = makeRuntimeConfig("api", {
|
|
80
96
|
cwd: ".",
|
|
81
97
|
start: "npm run api",
|
|
@@ -98,26 +114,29 @@ describe("runner-template", () => {
|
|
|
98
114
|
readyUrl: "http://127.0.0.1:{port}",
|
|
99
115
|
env: {
|
|
100
116
|
NEXT_PUBLIC_API_URL: "{baseUrl:api}",
|
|
117
|
+
ONIX_DB_HOST: "{dbHost:api}",
|
|
101
118
|
},
|
|
102
119
|
});
|
|
103
120
|
|
|
104
|
-
const resolved = resolveWorkerRuntimeConfigs(frontend, [api, frontend], 2,
|
|
121
|
+
const resolved = resolveWorkerRuntimeConfigs(frontend, [api, frontend], 2, workerStateDir);
|
|
105
122
|
expect(resolved[0].testkit.local.port).toBe(3100);
|
|
106
|
-
expect(resolved[1].testkit.local.env.NEXT_PUBLIC_API_URL).toBe("
|
|
107
|
-
expect(resolveServiceStateDir(
|
|
108
|
-
|
|
123
|
+
expect(resolved[1].testkit.local.env.NEXT_PUBLIC_API_URL).toBe("{baseUrl:api}");
|
|
124
|
+
expect(resolveServiceStateDir(workerStateDir, "frontend", api)).toBe(
|
|
125
|
+
`${workerStateDir}/deps/api`
|
|
126
|
+
);
|
|
127
|
+
expect(getWorkerServiceStateDir(workerStateDir, "frontend", "frontend")).toBe(workerStateDir);
|
|
128
|
+
|
|
129
|
+
fs.mkdirSync(path.join(workerStateDir, "deps", "api"), { recursive: true });
|
|
130
|
+
fs.writeFileSync(
|
|
131
|
+
path.join(workerStateDir, "deps", "api", "database_url"),
|
|
132
|
+
"postgres://testkit:testkit@127.0.0.1:55432/onix_db"
|
|
133
|
+
);
|
|
109
134
|
|
|
110
135
|
expect(
|
|
111
136
|
buildExecutionEnv(
|
|
137
|
+
resolved[1],
|
|
112
138
|
{
|
|
113
|
-
|
|
114
|
-
testkit: {
|
|
115
|
-
serviceEnv: {
|
|
116
|
-
API_KEY: "secret",
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
139
|
+
...resolved[1].testkit.local.env,
|
|
121
140
|
DATABASE_URL: "gone",
|
|
122
141
|
},
|
|
123
142
|
{
|
|
@@ -126,7 +145,8 @@ describe("runner-template", () => {
|
|
|
126
145
|
)
|
|
127
146
|
).toEqual({
|
|
128
147
|
PATH: "/usr/bin",
|
|
129
|
-
|
|
148
|
+
NEXT_PUBLIC_API_URL: "http://127.0.0.1:3100",
|
|
149
|
+
ONIX_DB_HOST: "127.0.0.1",
|
|
130
150
|
TESTKIT_ACTIVE: "1",
|
|
131
151
|
TESTKIT_WORKER_ID: "2",
|
|
132
152
|
});
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -40,9 +40,10 @@ export interface ServiceConfig {
|
|
|
40
40
|
discovery?: {
|
|
41
41
|
roots?: string[];
|
|
42
42
|
};
|
|
43
|
+
env?: Record<string, string>;
|
|
43
44
|
envFile?: string;
|
|
44
45
|
envFiles?: string[];
|
|
45
|
-
local?: {
|
|
46
|
+
local?: false | {
|
|
46
47
|
baseUrl: string;
|
|
47
48
|
cwd?: string;
|
|
48
49
|
env?: Record<string, string>;
|