@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.
@@ -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 = loadServiceEnv(productDir, envFiles);
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.local) {
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 || ".",
@@ -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();
@@ -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: Object.fromEntries(
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, "/tmp/w2");
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("http://127.0.0.1:3100");
107
- expect(resolveServiceStateDir("/tmp/w2", "frontend", api)).toBe("/tmp/w2/deps/api");
108
- expect(getWorkerServiceStateDir("/tmp/w2", "frontend", "frontend")).toBe("/tmp/w2");
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
- workerId: 2,
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
- API_KEY: "secret",
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
  });
@@ -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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "types": "./lib/index.d.ts",