@jterrazz/test 6.0.0 → 6.2.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 CHANGED
@@ -35,13 +35,13 @@ afterAll(() => run.cleanup());
35
35
  import { run } from '../../setup/integration.specification.js';
36
36
 
37
37
  test('creates a user', async () => {
38
- // Given one existing user
38
+ // Given - one existing user
39
39
  const result = await run('creates user')
40
40
  .seed('initial-users.sql')
41
41
  .post('/users', 'new-user.json')
42
42
  .run();
43
43
 
44
- // Then user created
44
+ // Then - user created
45
45
  expect(result.status).toBe(201);
46
46
  await result.table('users').toMatch({
47
47
  columns: ['name'],
@@ -67,10 +67,10 @@ export const run = await spec(command(resolve(import.meta.dirname, '../../bin/my
67
67
  import { run } from '../../setup/cli.specification.js';
68
68
 
69
69
  test('builds the project', async () => {
70
- // Given sample app project
70
+ // Given - sample app project
71
71
  const result = await run('build').project('sample-app').exec('build').run();
72
72
 
73
- // Then ESM output with source maps
73
+ // Then - ESM output with source maps
74
74
  expect(result.exitCode).toBe(0);
75
75
  expect(result.stdout).toContain('Build completed');
76
76
  expect(result.file('dist/index.js').exists).toBe(true);
@@ -83,7 +83,7 @@ test('builds the project', async () => {
83
83
 
84
84
  Three modes, same builder API. Each handles infrastructure and cleanup automatically.
85
85
 
86
- ### `spec(app(...))` testcontainers + in-process app
86
+ ### `spec(app(...))` - testcontainers + in-process app
87
87
 
88
88
  Starts real containers via testcontainers. App runs in-process (Hono). Fastest feedback loop.
89
89
 
@@ -103,7 +103,7 @@ export const run = await spec(
103
103
  );
104
104
  ```
105
105
 
106
- ### `spec(stack(...))` docker compose up + real HTTP
106
+ ### `spec(stack(...))` - docker compose up + real HTTP
107
107
 
108
108
  Starts the full `docker/compose.test.yaml` stack. App URL and databases auto-detected.
109
109
 
@@ -113,7 +113,7 @@ import { spec, stack } from '@jterrazz/test';
113
113
  export const run = await spec(stack('../../'));
114
114
  ```
115
115
 
116
- ### `spec(command(...))` local command execution
116
+ ### `spec(command(...))` - local command execution
117
117
 
118
118
  Runs CLI commands against fixture projects in temp directories. Optionally starts infrastructure.
119
119
 
@@ -331,13 +331,13 @@ Every test uses `// Given` and `// Then` comments. Always both, never one withou
331
331
 
332
332
  ```typescript
333
333
  test('creates a user and returns 201', async () => {
334
- // Given two existing users
334
+ // Given - two existing users
335
335
  const result = await run('creates user')
336
336
  .seed('initial-users.sql')
337
337
  .post('/users', 'new-user.json')
338
338
  .run();
339
339
 
340
- // Then user created with all three in table
340
+ // Then - user created with all three in table
341
341
  expect(result.status).toBe(201);
342
342
  await result.table('users').toMatch({
343
343
  columns: ['name'],
package/dist/chunk.cjs ADDED
@@ -0,0 +1,28 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ Object.defineProperty(exports, "__toESM", {
24
+ enumerable: true,
25
+ get: function() {
26
+ return __toESM;
27
+ }
28
+ });
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ require("./chunk.cjs");
3
+ const require_sqlite_adapter = require("./sqlite.adapter.cjs");
2
4
  const require_mock_of = require("./mock-of.cjs");
3
- const require_redis_adapter = require("./redis.adapter.cjs");
4
5
  let node_child_process = require("node:child_process");
5
6
  let node_fs = require("node:fs");
6
7
  let node_os = require("node:os");
@@ -114,22 +115,25 @@ var FetchAdapter = class {
114
115
  constructor(url) {
115
116
  this.baseUrl = url.replace(/\/$/, "");
116
117
  }
117
- async request(method, path, body) {
118
+ async request(method, path, body, headers) {
118
119
  const init = {
119
120
  method,
120
- headers: { "Content-Type": "application/json" }
121
+ headers: {
122
+ "Content-Type": "application/json",
123
+ ...headers
124
+ }
121
125
  };
122
126
  if (body !== void 0) init.body = JSON.stringify(body);
123
127
  const response = await fetch(`${this.baseUrl}${path}`, init);
124
128
  const responseBody = await response.json().catch(() => null);
125
- const headers = {};
129
+ const responseHeaders = {};
126
130
  response.headers.forEach((value, key) => {
127
- headers[key] = value;
131
+ responseHeaders[key] = value;
128
132
  });
129
133
  return {
130
134
  status: response.status,
131
135
  body: responseBody,
132
- headers
136
+ headers: responseHeaders
133
137
  };
134
138
  }
135
139
  };
@@ -144,22 +148,25 @@ var HonoAdapter = class {
144
148
  constructor(app) {
145
149
  this.app = app;
146
150
  }
147
- async request(method, path, body) {
151
+ async request(method, path, body, headers) {
148
152
  const init = {
149
153
  method,
150
- headers: { "Content-Type": "application/json" }
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ ...headers
157
+ }
151
158
  };
152
159
  if (body !== void 0) init.body = JSON.stringify(body);
153
160
  const response = await this.app.request(path, init);
154
161
  const responseBody = await response.json().catch(() => null);
155
- const headers = {};
162
+ const responseHeaders = {};
156
163
  response.headers.forEach((value, key) => {
157
- headers[key] = value;
164
+ responseHeaders[key] = value;
158
165
  });
159
166
  return {
160
167
  status: response.status,
161
168
  body: responseBody,
162
- headers
169
+ headers: responseHeaders
163
170
  };
164
171
  }
165
172
  };
@@ -589,6 +596,7 @@ var SpecificationBuilder = class {
589
596
  mocks = [];
590
597
  projectName = null;
591
598
  request = null;
599
+ requestHeaders = {};
592
600
  seeds = [];
593
601
  spawnConfig = null;
594
602
  testDir;
@@ -643,6 +651,19 @@ var SpecificationBuilder = class {
643
651
  return this;
644
652
  }
645
653
  /**
654
+ * Set HTTP headers for the request. Multiple calls merge.
655
+ *
656
+ * @example
657
+ * spec("french").headers({ 'Accept-Language': 'fr' }).get("/articles").run();
658
+ */
659
+ headers(headers) {
660
+ this.requestHeaders = {
661
+ ...this.requestHeaders,
662
+ ...headers
663
+ };
664
+ return this;
665
+ }
666
+ /**
646
667
  * Send a GET request to the server adapter.
647
668
  *
648
669
  * @example
@@ -763,7 +784,8 @@ var SpecificationBuilder = class {
763
784
  if (!this.config.server) throw new Error("HTTP actions require a server adapter (use integration() or e2e())");
764
785
  let body;
765
786
  if (this.request.bodyFile) body = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "requests", this.request.bodyFile), "utf8"));
766
- const response = await this.config.server.request(this.request.method, this.request.path, body);
787
+ const headers = Object.keys(this.requestHeaders).length > 0 ? this.requestHeaders : void 0;
788
+ const response = await this.config.server.request(this.request.method, this.request.path, body, headers);
767
789
  return new SpecificationResult({
768
790
  config: this.config,
769
791
  requestInfo: {
@@ -1211,7 +1233,13 @@ var Orchestrator = class {
1211
1233
  const composePath = findComposeFile(this.root);
1212
1234
  const composeDir = composePath ? (0, node_path.dirname)(composePath) : this.root;
1213
1235
  const composeConfig = composePath ? parseComposeFile(composePath) : null;
1214
- const containerTasks = this.services.map((handle) => {
1236
+ const containerServices = [];
1237
+ const embeddedServices = [];
1238
+ for (const handle of this.services) {
1239
+ if (handle.defaultPort === 0) {
1240
+ embeddedServices.push(handle);
1241
+ continue;
1242
+ }
1215
1243
  let image = handle.defaultImage;
1216
1244
  let env = { ...handle.environment };
1217
1245
  if (handle.composeName && composeConfig) {
@@ -1225,18 +1253,26 @@ var Orchestrator = class {
1225
1253
  Object.assign(handle.environment, composeService.environment);
1226
1254
  }
1227
1255
  }
1228
- return {
1229
- container: new TestcontainersAdapter({
1230
- image,
1231
- port: handle.defaultPort,
1232
- env
1233
- }),
1256
+ const container = new TestcontainersAdapter({
1257
+ image,
1258
+ port: handle.defaultPort,
1259
+ env
1260
+ });
1261
+ containerServices.push({
1262
+ container,
1234
1263
  handle
1235
- };
1236
- });
1237
- await Promise.all(containerTasks.map(({ container }) => container.start()));
1264
+ });
1265
+ }
1266
+ await Promise.all([...containerServices.map(({ container }) => container.start()), ...embeddedServices.map(async (handle) => {
1267
+ await handle.initialize(composeDir);
1268
+ handle.started = true;
1269
+ this.running.push({
1270
+ handle,
1271
+ container: null
1272
+ });
1273
+ })]);
1238
1274
  const reports = [];
1239
- for (const { container, handle } of containerTasks) {
1275
+ for (const { container, handle } of containerServices) {
1240
1276
  const serviceStartTime = Date.now();
1241
1277
  try {
1242
1278
  const host = container.getHost();
@@ -1302,7 +1338,7 @@ var Orchestrator = class {
1302
1338
  for (const service of composeConfig.infraServices) {
1303
1339
  const type = detectServiceType(service.image);
1304
1340
  if (type === "postgres") {
1305
- const handle = require_redis_adapter.postgres({
1341
+ const handle = require_sqlite_adapter.postgres({
1306
1342
  compose: service.name,
1307
1343
  env: service.environment
1308
1344
  });
@@ -1312,7 +1348,7 @@ var Orchestrator = class {
1312
1348
  handle.started = true;
1313
1349
  this.composeHandles.push(handle);
1314
1350
  } else if (type === "redis") {
1315
- const handle = require_redis_adapter.redis({ compose: service.name });
1351
+ const handle = require_sqlite_adapter.redis({ compose: service.name });
1316
1352
  const port = this.composeStack.getMappedPort(service.name, 6379);
1317
1353
  handle.connectionString = handle.buildConnectionString("localhost", port);
1318
1354
  handle.started = true;
@@ -1450,7 +1486,12 @@ async function startApp(target, options) {
1450
1486
  });
1451
1487
  await orchestrator.start();
1452
1488
  await acquireIsolation(services);
1453
- const honoApp = target.factory();
1489
+ const servicesMap = {};
1490
+ for (const svc of services) {
1491
+ const key = svc.composeName ?? svc.type;
1492
+ servicesMap[key] = svc;
1493
+ }
1494
+ const honoApp = target.factory(servicesMap);
1454
1495
  const database = orchestrator.getDatabase() ?? void 0;
1455
1496
  const databases = orchestrator.getDatabases();
1456
1497
  const runner = createSpecificationRunner({
@@ -1526,13 +1567,17 @@ async function startCommand(target, options) {
1526
1567
  //#endregion
1527
1568
  //#region src/runner/targets.ts
1528
1569
  /**
1529
- * Test against an in-process Hono app. Services are started via testcontainers
1530
- * and the app runs without network overhead.
1570
+ * Test against an in-process Hono app. The factory receives started services
1571
+ * so you can wire connection strings into your app/DI container.
1531
1572
  *
1532
- * @param factory - Function that returns a Hono app instance (called after services start).
1573
+ * @param factory - Function that receives services and returns a Hono app instance.
1533
1574
  *
1534
1575
  * @example
1535
- * await spec(app(() => createApp(db)), { services: [db] });
1576
+ * const db = postgres({ compose: 'db' });
1577
+ * await spec(
1578
+ * app((services) => createApp({ databaseUrl: services.db.connectionString })),
1579
+ * { services: [db] },
1580
+ * );
1536
1581
  */
1537
1582
  function app(factory) {
1538
1583
  return {
@@ -1677,9 +1722,10 @@ exports.integration = integration;
1677
1722
  exports.mockOf = require_mock_of.mockOf;
1678
1723
  exports.mockOfDate = require_mock_of.mockOfDate;
1679
1724
  exports.normalizeOutput = normalizeOutput;
1680
- exports.postgres = require_redis_adapter.postgres;
1681
- exports.redis = require_redis_adapter.redis;
1725
+ exports.postgres = require_sqlite_adapter.postgres;
1726
+ exports.redis = require_sqlite_adapter.redis;
1682
1727
  exports.spec = spec;
1728
+ exports.sqlite = require_sqlite_adapter.sqlite;
1683
1729
  exports.stack = stack;
1684
1730
  exports.stripAnsi = stripAnsi;
1685
1731