@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.
@@ -1,35 +1,301 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import {
3
- databaseServiceEnv,
3
+ app,
4
+ database,
4
5
  defineConfig,
5
6
  defineFile,
6
- defineHttpProfile,
7
- nextApp,
8
- nextBuild,
9
- nodeToolchain,
10
- nodeApp,
11
- postgresDatabase,
12
- postgresFixture,
13
- schemaSql,
14
- seedCommand,
15
- seedModule,
16
- stepsBuild,
17
- templateDatabase,
18
- tscBuild,
19
- verifyCommand,
20
- verifyModule,
7
+ profiles,
8
+ toolchain,
21
9
  } from "./index.mjs";
10
+ import { clearRuntimeContext, registerRuntimeContext } from "./runtime.mjs";
22
11
 
23
- describe("config helpers", () => {
12
+ describe("config api", () => {
24
13
  it("defines repo config plainly", () => {
25
14
  expect(defineConfig({ execution: { workers: 4 } })).toEqual({
26
15
  execution: { workers: 4 },
27
16
  });
28
17
  });
29
18
 
30
- it("defines HTTP profiles plainly", () => {
31
- const profile = defineHttpProfile({ headers: () => ({ Authorization: "Bearer token" }) });
32
- expect(typeof profile.headers).toBe("function");
19
+ it("builds raw HTTP profiles with deterministic forwarded headers", () => {
20
+ const profile = profiles.raw({
21
+ headers: {
22
+ contentTypeJson: true,
23
+ forwardedFor: "deterministic",
24
+ values: { "X-Testkit-Mode": "raw" },
25
+ },
26
+ });
27
+
28
+ const headers = profile.headers?.(null, { env: { BASE: "http://api.test" } });
29
+ const rawHeaders = profile.rawHeaders?.(null, { env: { BASE: "http://api.test" } });
30
+
31
+ expect(headers).toMatchObject({
32
+ "Content-Type": "application/json",
33
+ "X-Testkit-Mode": "raw",
34
+ });
35
+ expect(headers["X-Forwarded-For"]).toMatch(/^10\.\d+\.\d+\.\d+$/);
36
+ expect(rawHeaders).toEqual(headers);
37
+ });
38
+
39
+ it("builds session profiles that perform bootstrap/login and derive auth/session headers", () => {
40
+ const requests = [];
41
+ registerRuntimeContext({
42
+ env: {
43
+ BASE: "http://api.test",
44
+ routeParams: { "x-route": "route-a" },
45
+ },
46
+ http: {
47
+ post(url, body, params) {
48
+ requests.push({ url, body, params });
49
+ if (url.endsWith("/signup")) {
50
+ return {
51
+ status: 201,
52
+ body: JSON.stringify({ data: { organizations: [{ id: "org-signup" }] } }),
53
+ headers: {
54
+ "set-cookie": ["fixture_session=signup-token; Path=/", "fixture_refresh=signup-refresh; Path=/"],
55
+ },
56
+ };
57
+ }
58
+ return {
59
+ status: 200,
60
+ body: JSON.stringify({ data: { organizations: [{ id: "org-123" }] } }),
61
+ headers: {
62
+ "set-cookie": ["fixture_session=jwt-123; Path=/", "fixture_refresh=refresh-123; Path=/"],
63
+ },
64
+ };
65
+ },
66
+ },
67
+ });
68
+
69
+ const profile = profiles.session({
70
+ actor: {
71
+ bootstrap: {
72
+ path: "/signup",
73
+ expect: [201, 409],
74
+ body: ({ actor }) => ({ email: `${actor}@example.com` }),
75
+ },
76
+ login: {
77
+ path: "/login",
78
+ expect: 200,
79
+ body: ({ actor }) => ({ email: `${actor}@example.com` }),
80
+ },
81
+ session: {
82
+ cookies: {
83
+ jwt: "fixture_session",
84
+ refreshToken: "fixture_refresh",
85
+ },
86
+ fields: {
87
+ organizationId: "data.organizations[0].id",
88
+ },
89
+ auth: {
90
+ source: { key: "jwt" },
91
+ },
92
+ },
93
+ },
94
+ headers: {
95
+ contentTypeJson: true,
96
+ forwardedFor: "deterministic",
97
+ fromSession: [{ header: "X-Organization-Id", field: "organizationId" }],
98
+ values: ({ actor }) => ({ "X-Testkit-Actor": actor || "primary" }),
99
+ },
100
+ });
101
+
102
+ const session = profile.auth.setup({
103
+ env: { BASE: "http://api.test", routeParams: { "x-route": "route-a" } },
104
+ });
105
+ const authHeaders = profile.auth.headers(session, { env: { BASE: "http://api.test" } });
106
+ const requestHeaders = profile.headers(session, { env: { BASE: "http://api.test" } });
107
+
108
+ expect(requests).toHaveLength(2);
109
+ expect(requests[0].params.headers["x-route"]).toBe("route-a");
110
+ expect(session).toEqual({
111
+ jwt: "jwt-123",
112
+ refreshToken: "refresh-123",
113
+ organizationId: "org-123",
114
+ });
115
+ expect(authHeaders).toEqual({
116
+ Authorization: "Bearer jwt-123",
117
+ });
118
+ expect(requestHeaders).toMatchObject({
119
+ "Content-Type": "application/json",
120
+ "X-Organization-Id": "org-123",
121
+ "X-Testkit-Actor": "primary",
122
+ });
123
+ expect(requestHeaders["X-Forwarded-For"]).toMatch(/^10\.\d+\.\d+\.\d+$/);
124
+ clearRuntimeContext();
125
+ });
126
+
127
+ it("builds local-json profile presets that derive session, multi-actor, and raw variants", () => {
128
+ const requests = [];
129
+ registerRuntimeContext({
130
+ env: {
131
+ BASE: "http://api.test",
132
+ },
133
+ http: {
134
+ post(url, body) {
135
+ const payload = JSON.parse(body);
136
+ requests.push({ url, payload });
137
+ const actor = payload.email.startsWith("primary")
138
+ ? "primary"
139
+ : payload.email.startsWith("user-a")
140
+ ? "userA"
141
+ : "userB";
142
+ return {
143
+ status: url.endsWith("/signup") ? 201 : 200,
144
+ body: JSON.stringify({
145
+ data: {
146
+ organizations: [{ id: `org-${actor}` }],
147
+ },
148
+ }),
149
+ headers: {
150
+ "set-cookie": [
151
+ `fixture_session=jwt-${actor}; Path=/`,
152
+ `fixture_refresh=refresh-${actor}; Path=/`,
153
+ ],
154
+ },
155
+ };
156
+ },
157
+ },
158
+ });
159
+
160
+ const auth = profiles.localJson({
161
+ password: "TestkitPass2026",
162
+ identities: {
163
+ primary: {
164
+ email: "primary@example.com",
165
+ name: "Primary User",
166
+ organizationName: "Primary Org",
167
+ },
168
+ userA: {
169
+ email: "user-a@example.com",
170
+ name: "User A",
171
+ organizationName: "Org A",
172
+ },
173
+ userB: {
174
+ email: "user-b@example.com",
175
+ name: "User B",
176
+ organizationName: "Org B",
177
+ },
178
+ },
179
+ session: {
180
+ authCookie: "fixture_session",
181
+ refreshCookie: "fixture_refresh",
182
+ organizationIdPath: "data.organizations[0].id",
183
+ },
184
+ headers: {
185
+ contentTypeJson: true,
186
+ forwardedFor: "deterministic",
187
+ organization: "X-Organization-Id",
188
+ },
189
+ });
190
+
191
+ const defaultProfile = auth.session();
192
+ const defaultSetup = defaultProfile.auth.setup({ env: { BASE: "http://api.test" } });
193
+ expect(defaultSetup).toEqual({
194
+ jwt: "jwt-primary",
195
+ refreshToken: "refresh-primary",
196
+ organizationId: "org-primary",
197
+ });
198
+ expect(defaultProfile.auth.headers(defaultSetup)).toEqual({
199
+ Authorization: "Bearer jwt-primary",
200
+ });
201
+ expect(defaultProfile.headers(defaultSetup, { env: { BASE: "http://api.test" } })).toMatchObject({
202
+ "Content-Type": "application/json",
203
+ "X-Organization-Id": "org-primary",
204
+ });
205
+
206
+ const dualProfile = auth.multiActor({
207
+ primaryActor: "userA",
208
+ actors: ["userA", "userB"],
209
+ });
210
+ const dualSetup = dualProfile.auth.setup({ env: { BASE: "http://api.test" } });
211
+ expect(dualSetup.userA.organizationId).toBe("org-userA");
212
+ expect(dualSetup.userB.organizationId).toBe("org-userB");
213
+ expect(dualProfile.auth.headers(dualSetup)).toEqual({
214
+ Authorization: "Bearer jwt-userA",
215
+ });
216
+
217
+ const rawProfile = auth.raw();
218
+ expect(rawProfile.rawHeaders(null, { env: { BASE: "http://api.test" } })).toMatchObject({
219
+ "Content-Type": "application/json",
220
+ });
221
+
222
+ expect(requests.map((entry) => entry.url)).toEqual([
223
+ "http://api.test/api/v1/auth/signup",
224
+ "http://api.test/api/v1/auth/login",
225
+ "http://api.test/api/v1/auth/signup",
226
+ "http://api.test/api/v1/auth/login",
227
+ "http://api.test/api/v1/auth/signup",
228
+ "http://api.test/api/v1/auth/login",
229
+ ]);
230
+ clearRuntimeContext();
231
+ });
232
+
233
+ it("builds multi-actor profiles and derives headers from the primary actor by default", () => {
234
+ const responses = {
235
+ alpha: {
236
+ status: 200,
237
+ body: JSON.stringify({ data: { organizations: [{ id: "org-alpha" }] } }),
238
+ headers: { "set-cookie": "fixture_session=token-alpha; Path=/" },
239
+ },
240
+ beta: {
241
+ status: 200,
242
+ body: JSON.stringify({ data: { organizations: [{ id: "org-beta" }] } }),
243
+ headers: { "set-cookie": "fixture_session=token-beta; Path=/" },
244
+ },
245
+ };
246
+ registerRuntimeContext({
247
+ env: {
248
+ BASE: "http://api.test",
249
+ },
250
+ http: {
251
+ post(_url, body) {
252
+ const payload = JSON.parse(body);
253
+ return payload.email.startsWith("alpha") ? responses.alpha : responses.beta;
254
+ },
255
+ },
256
+ });
257
+
258
+ const profile = profiles.multiActor({
259
+ primaryActor: "userA",
260
+ actors: {
261
+ userA: {
262
+ login: {
263
+ path: "/login",
264
+ body: () => ({ email: "alpha@example.com" }),
265
+ },
266
+ session: {
267
+ cookies: { jwt: "fixture_session" },
268
+ fields: { organizationId: "data.organizations[0].id" },
269
+ auth: { source: { key: "jwt" } },
270
+ },
271
+ },
272
+ userB: {
273
+ login: {
274
+ path: "/login",
275
+ body: () => ({ email: "beta@example.com" }),
276
+ },
277
+ session: {
278
+ cookies: { jwt: "fixture_session" },
279
+ fields: { organizationId: "data.organizations[0].id" },
280
+ auth: { source: { key: "jwt" } },
281
+ },
282
+ },
283
+ },
284
+ headers: {
285
+ fromSession: [{ header: "X-Organization-Id", field: "organizationId" }],
286
+ },
287
+ });
288
+
289
+ const setupData = profile.auth.setup({ env: { BASE: "http://api.test" } });
290
+ expect(setupData.userA.organizationId).toBe("org-alpha");
291
+ expect(setupData.userB.organizationId).toBe("org-beta");
292
+ expect(profile.auth.headers(setupData)).toEqual({
293
+ Authorization: "Bearer token-alpha",
294
+ });
295
+ expect(profile.headers(setupData, { env: { BASE: "http://api.test" } })).toMatchObject({
296
+ "X-Organization-Id": "org-alpha",
297
+ });
298
+ clearRuntimeContext();
33
299
  });
34
300
 
35
301
  it("defines file-local metadata plainly", () => {
@@ -40,13 +306,13 @@ describe("config helpers", () => {
40
306
  });
41
307
 
42
308
  it("builds a Next app preset for dev mode", () => {
43
- const config = nextApp({ port: 3000 });
309
+ const config = app.next({ port: 3000 });
44
310
 
45
311
  expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
46
312
  });
47
313
 
48
314
  it("builds a Next app preset for start mode with managed runtime env defaults", () => {
49
- const config = nextApp({ cwd: "frontend", port: 3000, mode: "start" });
315
+ const config = app.next({ cwd: "frontend", port: 3000, mode: "start" });
50
316
 
51
317
  expect(config.local.start).toBe("./node_modules/.bin/next start --port {port}");
52
318
  expect(config.local.env).toMatchObject({
@@ -63,7 +329,7 @@ describe("config helpers", () => {
63
329
  });
64
330
 
65
331
  it("allows Next start apps to disable managed builds explicitly", () => {
66
- const config = nextApp({ cwd: "frontend", port: 3000, mode: "start", build: null });
332
+ const config = app.next({ cwd: "frontend", port: 3000, mode: "start", build: null });
67
333
 
68
334
  expect(config.runtime.build).toBeNull();
69
335
  expect(config.local.env).toMatchObject({
@@ -73,7 +339,7 @@ describe("config helpers", () => {
73
339
  });
74
340
 
75
341
  it("builds a Node app preset with tsc build defaults", () => {
76
- const config = nodeApp({ port: 3000, entry: "src/server.ts" });
342
+ const config = app.node({ port: 3000, entry: "src/server.ts" });
77
343
 
78
344
  expect(config.local.start).toBe("node {prepareDir}/dist/server.js");
79
345
  expect(config.runtime.build).toEqual({
@@ -87,88 +353,22 @@ describe("config helpers", () => {
87
353
  });
88
354
 
89
355
  it("builds node toolchain profiles with a node kind", () => {
90
- expect(nodeToolchain({ node: "20.19.5", install: "download" })).toEqual({
356
+ expect(toolchain.node({ node: "20.19.5", install: "download" })).toEqual({
91
357
  kind: "node",
92
358
  node: "20.19.5",
93
359
  install: "download",
94
360
  });
95
361
  });
96
362
 
97
- it("builds explicit build presets", () => {
98
- expect(tscBuild({ entry: "src/server.ts", outDir: "build" })).toEqual({
99
- kind: "tsc",
100
- cwd: undefined,
101
- entry: "src/server.ts",
102
- tsconfig: "tsconfig.json",
103
- outDir: "build",
104
- inputs: undefined,
105
- });
106
- expect(nextBuild({ cwd: "frontend" })).toEqual({
107
- kind: "next",
108
- cwd: "frontend",
109
- distDir: "dist",
110
- tsconfig: "tsconfig.json",
111
- inputs: undefined,
112
- });
363
+ it("builds declarative postgres database templates from plain objects", () => {
113
364
  expect(
114
- stepsBuild({
115
- inputs: ["scripts/prepare.mjs"],
116
- steps: [seedCommand("node scripts/prepare.mjs")],
117
- })
118
- ).toEqual({
119
- kind: "steps",
120
- inputs: ["scripts/prepare.mjs"],
121
- steps: [
122
- {
123
- kind: "command",
124
- cmd: "node scripts/prepare.mjs",
125
- cwd: undefined,
126
- inputs: undefined,
365
+ database.postgres({
366
+ template: {
367
+ inputs: ["db/schema.sql", "scripts/seed.ts"],
368
+ schema: "db/schema.sql",
369
+ seed: { kind: "command", run: "npm run db:seed" },
370
+ verify: { kind: "module", target: "scripts/verify.ts#verifySeed" },
127
371
  },
128
- ],
129
- });
130
- });
131
-
132
- it("emits semantic database template steps using the underlying step shapes", () => {
133
- expect(schemaSql("db/schema.sql")).toEqual({
134
- kind: "sql-file",
135
- path: "db/schema.sql",
136
- cwd: undefined,
137
- inputs: undefined,
138
- });
139
- expect(seedCommand("npm run db:seed")).toEqual({
140
- kind: "command",
141
- cmd: "npm run db:seed",
142
- cwd: undefined,
143
- inputs: undefined,
144
- });
145
- expect(seedModule("scripts/seed.ts#seed")).toEqual({
146
- kind: "module",
147
- specifier: "scripts/seed.ts#seed",
148
- cwd: undefined,
149
- inputs: undefined,
150
- });
151
- expect(verifyCommand("npm run db:verify")).toEqual({
152
- kind: "command",
153
- cmd: "npm run db:verify",
154
- cwd: undefined,
155
- inputs: undefined,
156
- });
157
- expect(verifyModule("scripts/verify.ts#verify")).toEqual({
158
- kind: "module",
159
- specifier: "scripts/verify.ts#verify",
160
- cwd: undefined,
161
- inputs: undefined,
162
- });
163
- });
164
-
165
- it("builds declarative template databases from schema, seed, and verify intents", () => {
166
- expect(
167
- templateDatabase({
168
- inputs: ["db/schema.sql", "scripts/seed.ts"],
169
- schema: "db/schema.sql",
170
- seed: seedCommand("npm run db:seed"),
171
- verify: verifyModule("scripts/verify.ts#verifySeed"),
172
372
  })
173
373
  ).toEqual({
174
374
  provider: "local",
@@ -178,24 +378,18 @@ describe("config helpers", () => {
178
378
  {
179
379
  kind: "sql-file",
180
380
  path: "db/schema.sql",
181
- cwd: undefined,
182
- inputs: undefined,
183
381
  },
184
382
  ],
185
383
  seed: [
186
384
  {
187
385
  kind: "command",
188
- cmd: "npm run db:seed",
189
- cwd: undefined,
190
- inputs: undefined,
386
+ run: "npm run db:seed",
191
387
  },
192
388
  ],
193
389
  verify: [
194
390
  {
195
391
  kind: "module",
196
- specifier: "scripts/verify.ts#verifySeed",
197
- cwd: undefined,
198
- inputs: undefined,
392
+ target: "scripts/verify.ts#verifySeed",
199
393
  },
200
394
  ],
201
395
  },
@@ -204,9 +398,11 @@ describe("config helpers", () => {
204
398
 
205
399
  it("prepends schema before explicit migrate steps and normalizes singletons to arrays", () => {
206
400
  expect(
207
- templateDatabase({
208
- schema: schemaSql("db/schema.sql", { cwd: "db" }),
209
- migrate: seedCommand("echo migrate"),
401
+ database.postgres({
402
+ template: {
403
+ schema: { kind: "sql-file", path: "db/schema.sql", cwd: "db" },
404
+ migrate: { kind: "command", run: "echo migrate" },
405
+ },
210
406
  })
211
407
  ).toEqual({
212
408
  provider: "local",
@@ -217,13 +413,10 @@ describe("config helpers", () => {
217
413
  kind: "sql-file",
218
414
  path: "db/schema.sql",
219
415
  cwd: "db",
220
- inputs: undefined,
221
416
  },
222
417
  {
223
418
  kind: "command",
224
- cmd: "echo migrate",
225
- cwd: undefined,
226
- inputs: undefined,
419
+ run: "echo migrate",
227
420
  },
228
421
  ],
229
422
  seed: [],
@@ -232,46 +425,9 @@ describe("config helpers", () => {
232
425
  });
233
426
  });
234
427
 
235
- it("builds declarative postgres database helpers", () => {
236
- expect(postgresDatabase({ reset: false })).toEqual({
237
- provider: "local",
238
- reset: false,
239
- });
240
- expect(
241
- templateDatabase({
242
- reset: true,
243
- schema: "db/schema.sql",
244
- seed: seedCommand("npm run db:seed"),
245
- })
246
- ).toEqual({
247
- provider: "local",
248
- reset: true,
249
- template: {
250
- inputs: undefined,
251
- migrate: [
252
- {
253
- kind: "sql-file",
254
- path: "db/schema.sql",
255
- cwd: undefined,
256
- inputs: undefined,
257
- },
258
- ],
259
- seed: [
260
- {
261
- kind: "command",
262
- cmd: "npm run db:seed",
263
- cwd: undefined,
264
- inputs: undefined,
265
- },
266
- ],
267
- verify: [],
268
- },
269
- });
270
- });
271
-
272
- it("builds support database presets and env bindings", () => {
428
+ it("builds support database presets and expands database env bindings declaratively", () => {
273
429
  expect(
274
- postgresFixture({
430
+ database.fixture({
275
431
  reset: true,
276
432
  })
277
433
  ).toEqual({
@@ -285,7 +441,19 @@ describe("config helpers", () => {
285
441
  reset: true,
286
442
  },
287
443
  });
288
- expect(databaseServiceEnv("ONIX", "catalog")).toEqual({
444
+
445
+ const config = app.node({
446
+ port: 3000,
447
+ env: {
448
+ values: { API_KEY: "test" },
449
+ databases: {
450
+ onix: { service: "catalog", prefix: "ONIX" },
451
+ },
452
+ },
453
+ });
454
+
455
+ expect(config.local.env).toEqual({
456
+ API_KEY: "test",
289
457
  ONIX_DATABASE_HOST: "{dbHost:catalog}",
290
458
  ONIX_DATABASE_PORT: "{dbPort:catalog}",
291
459
  ONIX_DATABASE_NAME: "{dbName:catalog}",
@@ -295,12 +463,25 @@ describe("config helpers", () => {
295
463
  });
296
464
  });
297
465
 
466
+ it("rejects top-level database template lifecycle fields", () => {
467
+ expect(() =>
468
+ database.postgres({
469
+ schema: "db/schema.sql",
470
+ })
471
+ ).toThrow(/no longer accepts top-level "schema"/);
472
+ expect(() =>
473
+ database.fixture({
474
+ seed: { kind: "command", run: "npm run db:seed" },
475
+ })
476
+ ).toThrow(/no longer accepts top-level "seed"/);
477
+ });
478
+
298
479
  it("does not leak preset-only helper fields into node app configs", () => {
299
- const config = nodeApp({
480
+ const config = app.node({
300
481
  port: 3000,
301
482
  entry: "src/server.ts",
302
483
  buildInputs: ["src", "package.json"],
303
- env: { API_KEY: "test" },
484
+ env: { values: { API_KEY: "test" } },
304
485
  readyPath: "/live",
305
486
  });
306
487
 
@@ -310,4 +491,15 @@ describe("config helpers", () => {
310
491
  expect(config.local.env).toEqual({ API_KEY: "test" });
311
492
  expect(config.local.readyUrl).toBe("http://127.0.0.1:{port}/live");
312
493
  });
494
+
495
+ it("rejects flat preset env maps in favor of env.values and env.databases", () => {
496
+ expect(() =>
497
+ app.node({
498
+ port: 3000,
499
+ env: {
500
+ API_KEY: "test",
501
+ },
502
+ })
503
+ ).toThrow(/Preset env only supports "values" and "databases"/);
504
+ });
313
505
  });