@elench/testkit 0.1.76 → 0.1.78

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.
@@ -3,79 +3,34 @@ import {
3
3
  clearRuntimeContext,
4
4
  getRepoConfig,
5
5
  getRuntimeContext,
6
- getRuntimeEnv,
7
6
  registerRepoConfig,
8
7
  registerRuntimeContext,
9
8
  runtimeHttp,
10
9
  runtimeJson,
11
10
  } from "./runtime.mjs";
11
+ import {
12
+ customProfile,
13
+ localJsonProfile,
14
+ multiActorProfile,
15
+ rawProfile,
16
+ sessionProfile,
17
+ } from "./profiles.mjs";
12
18
 
13
19
  export function defineConfig(config) {
14
20
  return config || {};
15
21
  }
16
22
 
17
- export function defineHttpProfile(profile) {
18
- return profile || {};
19
- }
20
-
21
23
  export function defineFile(metadata) {
22
24
  return metadata || {};
23
25
  }
24
26
 
25
- export function postgresDatabase(options = {}) {
27
+ function postgresDatabase(options = {}) {
26
28
  return {
27
29
  provider: "local",
28
30
  ...options,
29
31
  };
30
32
  }
31
33
 
32
- export function commandStep(cmd, options = {}) {
33
- return {
34
- kind: "command",
35
- cmd,
36
- cwd: options.cwd,
37
- inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
38
- };
39
- }
40
-
41
- export function sqlFileStep(filePath, options = {}) {
42
- return {
43
- kind: "sql-file",
44
- path: filePath,
45
- cwd: options.cwd,
46
- inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
47
- };
48
- }
49
-
50
- export function moduleStep(specifier, options = {}) {
51
- return {
52
- kind: "module",
53
- specifier,
54
- cwd: options.cwd,
55
- inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
56
- };
57
- }
58
-
59
- export function schemaSql(filePath, options = {}) {
60
- return sqlFileStep(filePath, options);
61
- }
62
-
63
- export function seedCommand(cmd, options = {}) {
64
- return commandStep(cmd, options);
65
- }
66
-
67
- export function seedModule(specifier, options = {}) {
68
- return moduleStep(specifier, options);
69
- }
70
-
71
- export function verifyCommand(cmd, options = {}) {
72
- return commandStep(cmd, options);
73
- }
74
-
75
- export function verifyModule(specifier, options = {}) {
76
- return moduleStep(specifier, options);
77
- }
78
-
79
34
  function buildDatabaseTemplateConfig(options = {}) {
80
35
  const migrate = normalizeTemplateStepList(options.migrate);
81
36
  const seed = normalizeTemplateStepList(options.seed);
@@ -90,60 +45,40 @@ function buildDatabaseTemplateConfig(options = {}) {
90
45
  };
91
46
  }
92
47
 
93
- export function templateDatabase(options = {}) {
94
- const {
95
- inputs,
96
- schema,
97
- migrate,
98
- seed,
99
- verify,
100
- ...databaseOptions
101
- } = options;
102
- return postgresDatabase({
103
- ...databaseOptions,
104
- template: buildDatabaseTemplateConfig(options.template || { inputs, schema, migrate, seed, verify }),
105
- });
106
- }
107
-
108
- export function postgresFixture(options = {}) {
109
- const { discovery, envFiles, ...databaseOptions } = options;
48
+ function postgresFixture(options = {}) {
49
+ const { discovery, envFiles, template, ...databaseOptions } = options;
50
+ for (const legacyKey of ["inputs", "schema", "migrate", "seed", "verify"]) {
51
+ if (Object.prototype.hasOwnProperty.call(options, legacyKey)) {
52
+ throw new Error(
53
+ `database.fixture(...) no longer accepts top-level "${legacyKey}". Move lifecycle config under database.fixture({ template: { ... } }).`
54
+ );
55
+ }
56
+ }
110
57
  return {
111
58
  discovery: discovery || {
112
59
  roots: [".testkit-fixture"],
113
60
  },
114
61
  envFiles,
115
62
  local: false,
116
- database: postgresDatabase(databaseOptions),
117
- };
118
- }
119
-
120
- export function databaseServiceEnv(prefix, serviceName) {
121
- const normalizedPrefix = String(prefix || "").trim().replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
122
- if (!normalizedPrefix) {
123
- throw new Error("databaseServiceEnv prefix must be a non-empty string");
124
- }
125
- if (!serviceName || !String(serviceName).trim()) {
126
- throw new Error("databaseServiceEnv serviceName must be a non-empty string");
127
- }
128
-
129
- return {
130
- [`${normalizedPrefix}_DATABASE_HOST`]: `{dbHost:${serviceName}}`,
131
- [`${normalizedPrefix}_DATABASE_PORT`]: `{dbPort:${serviceName}}`,
132
- [`${normalizedPrefix}_DATABASE_NAME`]: `{dbName:${serviceName}}`,
133
- [`${normalizedPrefix}_DATABASE_USER`]: `{dbUser:${serviceName}}`,
134
- [`${normalizedPrefix}_DATABASE_PASSWORD`]: `{dbPassword:${serviceName}}`,
135
- [`${normalizedPrefix}_DATABASE_SSL`]: "0",
63
+ database: postgresDatabase(
64
+ template
65
+ ? {
66
+ ...databaseOptions,
67
+ template: buildDatabaseTemplateConfig(template),
68
+ }
69
+ : databaseOptions
70
+ ),
136
71
  };
137
72
  }
138
73
 
139
- export function nodeToolchain(options = {}) {
74
+ function nodeToolchain(options = {}) {
140
75
  return {
141
76
  kind: "node",
142
77
  ...options,
143
78
  };
144
79
  }
145
80
 
146
- export function tscBuild(options = {}) {
81
+ function tscBuild(options = {}) {
147
82
  return {
148
83
  kind: "tsc",
149
84
  cwd: options.cwd,
@@ -154,7 +89,7 @@ export function tscBuild(options = {}) {
154
89
  };
155
90
  }
156
91
 
157
- export function scriptBuild(script, options = {}) {
92
+ function scriptBuild(script, options = {}) {
158
93
  return {
159
94
  kind: "script",
160
95
  script,
@@ -163,7 +98,7 @@ export function scriptBuild(script, options = {}) {
163
98
  };
164
99
  }
165
100
 
166
- export function stepsBuild(options = {}) {
101
+ function stepsBuild(options = {}) {
167
102
  return {
168
103
  kind: "steps",
169
104
  inputs: Array.isArray(options.inputs) ? [...options.inputs] : undefined,
@@ -171,7 +106,7 @@ export function stepsBuild(options = {}) {
171
106
  };
172
107
  }
173
108
 
174
- export function nextBuild(options = {}) {
109
+ function nextBuild(options = {}) {
175
110
  return {
176
111
  kind: "next",
177
112
  cwd: options.cwd,
@@ -181,14 +116,14 @@ export function nextBuild(options = {}) {
181
116
  };
182
117
  }
183
118
 
184
- export function nodeApp(options = {}) {
119
+ function nodeApp(options = {}) {
185
120
  const {
186
121
  baseUrl: explicitBaseUrl,
187
122
  build: explicitBuild,
188
123
  buildInputs,
189
124
  cwd = ".",
190
125
  entry = "src/index.ts",
191
- env = {},
126
+ env,
192
127
  envFiles,
193
128
  outDir = "dist",
194
129
  port,
@@ -202,7 +137,8 @@ export function nodeApp(options = {}) {
202
137
  ...serviceConfig
203
138
  } = options;
204
139
 
205
- const normalizedPort = requiredNumber(port, "nodeApp port");
140
+ const normalizedPort = requiredNumber(port, "app.node port");
141
+ const normalizedEnv = normalizePresetEnv(env);
206
142
  const baseUrl = explicitBaseUrl || "http://127.0.0.1:{port}";
207
143
  const build = explicitBuild === undefined ? tscBuild({
208
144
  cwd,
@@ -228,12 +164,12 @@ export function nodeApp(options = {}) {
228
164
  baseUrl,
229
165
  readyUrl: explicitReadyUrl || `${baseUrl}${readyPath}`,
230
166
  readyTimeoutMs,
231
- env,
167
+ env: normalizedEnv,
232
168
  },
233
169
  };
234
170
  }
235
171
 
236
- export function nextApp(options = {}) {
172
+ function nextApp(options = {}) {
237
173
  const {
238
174
  baseUrl: explicitBaseUrl,
239
175
  browser,
@@ -241,7 +177,7 @@ export function nextApp(options = {}) {
241
177
  buildInputs,
242
178
  cwd = ".",
243
179
  dependsOn,
244
- env = {},
180
+ env,
245
181
  envFiles,
246
182
  mode = "dev",
247
183
  port,
@@ -253,7 +189,8 @@ export function nextApp(options = {}) {
253
189
  ...serviceConfig
254
190
  } = options;
255
191
 
256
- const normalizedPort = requiredNumber(port, "nextApp port");
192
+ const normalizedPort = requiredNumber(port, "app.next port");
193
+ const normalizedEnv = normalizePresetEnv(env);
257
194
  const baseUrl = explicitBaseUrl || "http://127.0.0.1:{port}";
258
195
  const build =
259
196
  explicitBuild === undefined
@@ -286,82 +223,54 @@ export function nextApp(options = {}) {
286
223
  readyTimeoutMs,
287
224
  env: mode === "start"
288
225
  ? {
289
- NEXT_DIST_DIR: env.NEXT_DIST_DIR || ".next-testkit/{runtimeId}/dist",
290
- NEXT_TSCONFIG_PATH: env.NEXT_TSCONFIG_PATH || ".next-testkit/{runtimeId}/tsconfig.json",
291
- ...env,
226
+ NEXT_DIST_DIR: normalizedEnv.NEXT_DIST_DIR || ".next-testkit/{runtimeId}/dist",
227
+ NEXT_TSCONFIG_PATH:
228
+ normalizedEnv.NEXT_TSCONFIG_PATH || ".next-testkit/{runtimeId}/tsconfig.json",
229
+ ...normalizedEnv,
292
230
  }
293
- : env,
231
+ : normalizedEnv,
294
232
  },
295
233
  };
296
234
  }
297
235
 
298
- export function clerkSessionProfile(options = {}) {
299
- const apiBase = options.apiBase || "https://api.clerk.com/v1";
300
- const secretKeyEnv = options.secretKeyEnv || "CLERK_SECRET_KEY";
301
- const needsAuth = options.needsAuth !== false;
302
-
303
- return defineHttpProfile({
304
- auth: {
305
- setup() {
306
- return resolveClerkSession({
307
- apiBase,
308
- secretKey: envValue(secretKeyEnv),
309
- needsAuth,
310
- });
311
- },
312
- headers(setupData) {
313
- return getClerkAuthHeaders(setupData, { apiBase });
314
- },
315
- },
316
- });
317
- }
236
+ export const app = {
237
+ node: nodeApp,
238
+ next: nextApp,
239
+ };
318
240
 
319
- export function jsonSessionProfile(options = {}) {
320
- const loginPath = options.loginPath || "/api/auth/login";
321
- const cookieName = options.cookieName || "session";
322
- const body =
323
- typeof options.body === "function"
324
- ? options.body
325
- : () => ({
326
- username: envValue(options.usernameEnv || "TESTKIT_USERNAME"),
327
- password: envValue(options.passwordEnv || "TESTKIT_PASSWORD"),
328
- });
329
-
330
- return defineHttpProfile({
331
- auth: {
332
- setup({ env }) {
333
- const requestBody = body({ env });
334
- const res = runtimeHttp.post(`${env.BASE}${loginPath}`, JSON.stringify(requestBody), {
335
- headers: {
336
- "Content-Type": "application/json",
337
- ...(env.routeParams || {}),
338
- ...(options.headers || {}),
339
- },
340
- });
341
-
342
- if (res.status !== (options.successStatus || 200)) {
343
- throw new Error(`Login failed (${res.status}): ${res.body}`);
344
- }
345
-
346
- const cookie = res.cookies?.[cookieName]?.[0]?.value ?? null;
347
- if (!cookie) {
348
- throw new Error(`No ${cookieName} cookie returned from login`);
349
- }
350
-
351
- return {
352
- cookie,
353
- body: safeJson(res),
354
- };
355
- },
356
- headers(session) {
357
- if (!session?.cookie) return {};
358
- return {
359
- Cookie: `${cookieName}=${session.cookie}`,
360
- };
361
- },
362
- },
363
- });
364
- }
241
+ export const database = {
242
+ postgres(options = {}) {
243
+ const { template, ...databaseOptions } = options;
244
+ for (const legacyKey of ["inputs", "schema", "migrate", "seed", "verify"]) {
245
+ if (Object.prototype.hasOwnProperty.call(options, legacyKey)) {
246
+ throw new Error(
247
+ `database.postgres(...) no longer accepts top-level "${legacyKey}". Move lifecycle config under database.postgres({ template: { ... } }).`
248
+ );
249
+ }
250
+ }
251
+ return postgresDatabase(
252
+ template
253
+ ? {
254
+ ...databaseOptions,
255
+ template: buildDatabaseTemplateConfig(template),
256
+ }
257
+ : databaseOptions
258
+ );
259
+ },
260
+ fixture: postgresFixture,
261
+ };
262
+
263
+ export const toolchain = {
264
+ node: nodeToolchain,
265
+ };
266
+
267
+ export const profiles = {
268
+ custom: customProfile,
269
+ localJson: localJsonProfile,
270
+ multiActor: multiActorProfile,
271
+ raw: rawProfile,
272
+ session: sessionProfile,
273
+ };
365
274
 
366
275
  export {
367
276
  clearRepoConfig,
@@ -381,6 +290,58 @@ function requiredNumber(value, label) {
381
290
  return value;
382
291
  }
383
292
 
293
+ function normalizePresetEnv(env) {
294
+ if (!env) return {};
295
+ if (typeof env !== "object" || Array.isArray(env)) {
296
+ throw new Error("Preset env must be an object");
297
+ }
298
+ const allowedKeys = new Set(["values", "databases"]);
299
+ const unexpectedKeys = Object.keys(env).filter((key) => !allowedKeys.has(key));
300
+ if (unexpectedKeys.length > 0) {
301
+ throw new Error(
302
+ `Preset env only supports "values" and "databases". Received unexpected key(s): ${unexpectedKeys.join(", ")}`
303
+ );
304
+ }
305
+
306
+ const values = env.values && typeof env.values === "object" && !Array.isArray(env.values) ? env.values : {};
307
+ const databases =
308
+ env.databases && typeof env.databases === "object" && !Array.isArray(env.databases) ? env.databases : {};
309
+
310
+ return {
311
+ ...expandDatabaseBindings(databases),
312
+ ...values,
313
+ };
314
+ }
315
+
316
+ function expandDatabaseBindings(bindings) {
317
+ const env = {};
318
+ for (const [name, binding] of Object.entries(bindings || {})) {
319
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) {
320
+ throw new Error(`env.databases.${name} must be an object`);
321
+ }
322
+ const prefix = normalizeDatabaseEnvToken(binding.prefix, `env.databases.${name}.prefix`);
323
+ const serviceName = normalizeDatabaseEnvToken(binding.service, `env.databases.${name}.service`, false);
324
+ env[`${prefix}_DATABASE_HOST`] = `{dbHost:${serviceName}}`;
325
+ env[`${prefix}_DATABASE_PORT`] = `{dbPort:${serviceName}}`;
326
+ env[`${prefix}_DATABASE_NAME`] = `{dbName:${serviceName}}`;
327
+ env[`${prefix}_DATABASE_USER`] = `{dbUser:${serviceName}}`;
328
+ env[`${prefix}_DATABASE_PASSWORD`] = `{dbPassword:${serviceName}}`;
329
+ env[`${prefix}_DATABASE_SSL`] = "0";
330
+ }
331
+ return env;
332
+ }
333
+
334
+ function normalizeDatabaseEnvToken(value, label, sanitize = true) {
335
+ const raw = String(value || "").trim();
336
+ const normalized = sanitize
337
+ ? raw.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "")
338
+ : raw;
339
+ if (!normalized) {
340
+ throw new Error(`${label} must be a non-empty string`);
341
+ }
342
+ return normalized;
343
+ }
344
+
384
345
  function resolveNodeAppStart(build, entry) {
385
346
  if (build?.kind === "tsc") {
386
347
  const compiled = compiledEntryFromSource(entry || build.entry || "src/index.ts", build.outDir || "dist");
@@ -397,7 +358,10 @@ function normalizeTemplateStepList(value) {
397
358
  function normalizeSchemaStep(value) {
398
359
  if (value == null) return null;
399
360
  if (typeof value === "string") {
400
- return sqlFileStep(value);
361
+ return {
362
+ kind: "sql-file",
363
+ path: value,
364
+ };
401
365
  }
402
366
  return value;
403
367
  }
@@ -408,101 +372,3 @@ function compiledEntryFromSource(entry, outDir) {
408
372
  const relative = compiled.startsWith("src/") ? compiled.slice(4) : compiled;
409
373
  return `${outDir}/${relative}`.replace(/\/+/g, "/");
410
374
  }
411
-
412
- function envValue(name) {
413
- const env = getRuntimeEnv();
414
- const value = env?.rawEnv?.[name] || env?.[name];
415
- if (!value) {
416
- throw new Error(`Missing required env var "${name}" for testkit config`);
417
- }
418
- return value;
419
- }
420
-
421
- function safeJson(response) {
422
- try {
423
- return runtimeJson(response);
424
- } catch {
425
- return null;
426
- }
427
- }
428
-
429
- function resolveClerkSession({ apiBase, secretKey, needsAuth }) {
430
- if (!needsAuth) {
431
- return { jwt: null };
432
- }
433
-
434
- const headers = { Authorization: `Bearer ${secretKey}` };
435
- const usersRes = runtimeHttp.get(`${apiBase}/users?limit=1&order_by=-created_at`, {
436
- headers,
437
- });
438
- if (usersRes.status !== 200) {
439
- throw new Error(`Clerk list users failed (${usersRes.status}): ${usersRes.body}`);
440
- }
441
-
442
- const users = runtimeJson(usersRes);
443
- if (!users.length) {
444
- throw new Error("No Clerk users found for testkit Clerk auth profile");
445
- }
446
-
447
- const userId = users[0].id;
448
- const sessionsRes = runtimeHttp.get(
449
- `${apiBase}/sessions?user_id=${userId}&status=active&limit=1`,
450
- { headers }
451
- );
452
- if (sessionsRes.status !== 200) {
453
- throw new Error(`Clerk list sessions failed (${sessionsRes.status}): ${sessionsRes.body}`);
454
- }
455
-
456
- const sessions = runtimeJson(sessionsRes);
457
- if (!sessions?.length) {
458
- throw new Error("No active Clerk session found for testkit Clerk auth profile");
459
- }
460
-
461
- const sessionId = sessions[0].id;
462
- const tokenRes = runtimeHttp.post(`${apiBase}/sessions/${sessionId}/tokens`, null, {
463
- headers: {
464
- ...headers,
465
- "Content-Type": "application/json",
466
- },
467
- });
468
- if (tokenRes.status !== 200) {
469
- throw new Error(`Clerk create token failed (${tokenRes.status}): ${tokenRes.body}`);
470
- }
471
-
472
- return {
473
- jwt: runtimeJson(tokenRes).jwt,
474
- sessionId,
475
- secretKey,
476
- };
477
- }
478
-
479
- function getClerkAuthHeaders(setupData, { apiBase }) {
480
- if (!setupData?.jwt) return {};
481
-
482
- if (!setupData.sessionId || !setupData.secretKey) {
483
- return {
484
- Authorization: `Bearer ${setupData.jwt}`,
485
- "Content-Type": "application/json",
486
- };
487
- }
488
-
489
- const tokenRes = runtimeHttp.post(`${apiBase}/sessions/${setupData.sessionId}/tokens`, null, {
490
- headers: {
491
- Authorization: `Bearer ${setupData.secretKey}`,
492
- "Content-Type": "application/json",
493
- },
494
- });
495
- if (tokenRes.status !== 200) {
496
- return {
497
- Authorization: `Bearer ${setupData.jwt}`,
498
- "Content-Type": "application/json",
499
- };
500
- }
501
-
502
- const nextJwt = runtimeJson(tokenRes).jwt;
503
- setupData.jwt = nextJwt;
504
- return {
505
- Authorization: `Bearer ${nextJwt}`,
506
- "Content-Type": "application/json",
507
- };
508
- }