@stratal/testing 0.0.17 → 0.0.19

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as __decorate, t as FakeStorageService } from "./storage-PcJUKxwp.mjs";
1
+ import { n as __decorate, t as FakeStorageService } from "./storage-DZbrPg-l.mjs";
2
2
  import { Application } from "stratal";
3
3
  import { LogLevel } from "stratal/logger";
4
4
  import { Module } from "stratal/module";
@@ -7,7 +7,9 @@ import { DI_TOKENS } from "stratal/di";
7
7
  import { expect } from "vitest";
8
8
  import { connectionSymbol } from "@stratal/framework/database";
9
9
  import { SEEDER_TOKENS, SeederNotRegisteredError } from "stratal/seeder";
10
+ import { I18N_TOKENS } from "stratal/i18n";
10
11
  import { AUTH_SERVICE } from "@stratal/framework/auth";
12
+ import { Macroable } from "stratal/macroable";
11
13
  import { setSessionCookie } from "better-auth/cookies";
12
14
  import { convertSetCookieToCookie } from "better-auth/test";
13
15
  import { HttpResponse, HttpResponse as HttpResponse$1, http, http as http$1 } from "msw";
@@ -105,6 +107,40 @@ var ProviderOverrideBuilder = class {
105
107
  }
106
108
  };
107
109
  //#endregion
110
+ //#region src/core/http/locale-helper.ts
111
+ /**
112
+ * Resolve the configured detection strategy from the testing module's DI container.
113
+ * Falls back to 'cookie' if I18n is not configured.
114
+ */
115
+ function resolveLocaleStrategy(module) {
116
+ try {
117
+ const detection = module.get(I18N_TOKENS.Options).detection;
118
+ if (detection && "strategy" in detection && detection.strategy) return detection.strategy;
119
+ return "cookie";
120
+ } catch {
121
+ return "cookie";
122
+ }
123
+ }
124
+ /**
125
+ * Apply locale to request headers based on detection strategy.
126
+ */
127
+ function applyLocaleToHeaders(headers, locale, strategy) {
128
+ switch (strategy) {
129
+ case "cookie":
130
+ headers.set("Cookie", `locale=${locale}`);
131
+ break;
132
+ case "header":
133
+ headers.set("Accept-Language", locale);
134
+ break;
135
+ }
136
+ }
137
+ /**
138
+ * Apply locale to URL based on detection strategy.
139
+ */
140
+ function applyLocaleToUrl(url, locale, strategy) {
141
+ if (strategy === "querystring") url.searchParams.set("locale", locale);
142
+ }
143
+ //#endregion
108
144
  //#region src/auth/acting-as.ts
109
145
  async function makeSignature(value, secret) {
110
146
  const algorithm = {
@@ -169,6 +205,35 @@ var ActingAs = class {
169
205
  }
170
206
  };
171
207
  //#endregion
208
+ //#region src/core/http/path-utils.ts
209
+ /**
210
+ * Get value at dot-notation path.
211
+ */
212
+ function getValueAtPath(obj, path) {
213
+ const parts = path.split(".");
214
+ let current = obj;
215
+ for (const part of parts) {
216
+ if (current === null || current === void 0) return;
217
+ current = current[part];
218
+ }
219
+ return current;
220
+ }
221
+ /**
222
+ * Check if a path exists in the object (even if value is null/undefined).
223
+ */
224
+ function hasValueAtPath(obj, path) {
225
+ const parts = path.split(".");
226
+ let current = obj;
227
+ for (const part of parts) {
228
+ if (current === null || current === void 0) return false;
229
+ if (typeof current !== "object") return false;
230
+ const record = current;
231
+ if (!(part in record)) return false;
232
+ current = record[part];
233
+ }
234
+ return true;
235
+ }
236
+ //#endregion
172
237
  //#region src/core/http/test-response.ts
173
238
  /**
174
239
  * TestResponse
@@ -183,10 +248,11 @@ var ActingAs = class {
183
248
  * .assertJsonPath('data.id', userId)
184
249
  * ```
185
250
  */
186
- var TestResponse = class {
251
+ var TestResponse = class extends Macroable {
187
252
  jsonData = null;
188
253
  textData = null;
189
254
  constructor(response) {
255
+ super();
190
256
  this.response = response;
191
257
  }
192
258
  /**
@@ -294,7 +360,7 @@ var TestResponse = class {
294
360
  */
295
361
  async assertJson(expected) {
296
362
  const actual = await this.json();
297
- for (const [key, value] of Object.entries(expected)) expect(actual[key], `Expected JSON key "${key}" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`).toBe(value);
363
+ for (const [key, value] of Object.entries(expected)) expect(actual[key], `Expected JSON key "${key}" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`).toStrictEqual(value);
298
364
  return this;
299
365
  }
300
366
  /**
@@ -304,9 +370,8 @@ var TestResponse = class {
304
370
  * @param expected - Expected value at path
305
371
  */
306
372
  async assertJsonPath(path, expected) {
307
- const json = await this.json();
308
- const actual = this.getValueAtPath(json, path);
309
- expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toBe(expected);
373
+ const actual = getValueAtPath(await this.json(), path);
374
+ expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toStrictEqual(expected);
310
375
  return this;
311
376
  }
312
377
  /**
@@ -323,8 +388,7 @@ var TestResponse = class {
323
388
  * @param path - Dot-notation path (e.g., 'data.user.id')
324
389
  */
325
390
  async assertJsonPathExists(path) {
326
- const json = await this.json();
327
- expect(this.hasValueAtPath(json, path), `Expected JSON path "${path}" to exist`).toBe(true);
391
+ expect(hasValueAtPath(await this.json(), path), `Expected JSON path "${path}" to exist`).toBe(true);
328
392
  return this;
329
393
  }
330
394
  /**
@@ -333,8 +397,7 @@ var TestResponse = class {
333
397
  * @param path - Dot-notation path (e.g., 'data.user.deletedAt')
334
398
  */
335
399
  async assertJsonPathMissing(path) {
336
- const json = await this.json();
337
- expect(this.hasValueAtPath(json, path), `Expected JSON path "${path}" to not exist`).toBe(false);
400
+ expect(hasValueAtPath(await this.json(), path), `Expected JSON path "${path}" to not exist`).toBe(false);
338
401
  return this;
339
402
  }
340
403
  /**
@@ -344,8 +407,7 @@ var TestResponse = class {
344
407
  * @param matcher - Predicate function to validate the value
345
408
  */
346
409
  async assertJsonPathMatches(path, matcher) {
347
- const json = await this.json();
348
- const value = this.getValueAtPath(json, path);
410
+ const value = getValueAtPath(await this.json(), path);
349
411
  expect(matcher(value), `Expected JSON path "${path}" to match predicate, got ${JSON.stringify(value)}`).toBe(true);
350
412
  return this;
351
413
  }
@@ -356,8 +418,7 @@ var TestResponse = class {
356
418
  * @param substring - Substring to search for
357
419
  */
358
420
  async assertJsonPathContains(path, substring) {
359
- const json = await this.json();
360
- const value = this.getValueAtPath(json, path);
421
+ const value = getValueAtPath(await this.json(), path);
361
422
  expect(typeof value === "string", `Expected JSON path "${path}" to be a string, got ${typeof value}`).toBe(true);
362
423
  expect(value.includes(substring), `Expected JSON path "${path}" to contain "${substring}", got "${String(value)}"`).toBe(true);
363
424
  return this;
@@ -369,8 +430,7 @@ var TestResponse = class {
369
430
  * @param item - Item to search for in the array
370
431
  */
371
432
  async assertJsonPathIncludes(path, item) {
372
- const json = await this.json();
373
- const value = this.getValueAtPath(json, path);
433
+ const value = getValueAtPath(await this.json(), path);
374
434
  expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
375
435
  expect(value.includes(item), `Expected JSON path "${path}" to include ${JSON.stringify(item)}`).toBe(true);
376
436
  return this;
@@ -382,8 +442,7 @@ var TestResponse = class {
382
442
  * @param count - Expected array length
383
443
  */
384
444
  async assertJsonPathCount(path, count) {
385
- const json = await this.json();
386
- const value = this.getValueAtPath(json, path);
445
+ const value = getValueAtPath(await this.json(), path);
387
446
  expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
388
447
  expect(value.length, `Expected JSON path "${path}" to have ${count} items, got ${value.length}`).toBe(count);
389
448
  return this;
@@ -396,8 +455,8 @@ var TestResponse = class {
396
455
  async assertJsonPaths(expectations) {
397
456
  const json = await this.json();
398
457
  for (const [path, expected] of Object.entries(expectations)) {
399
- const actual = this.getValueAtPath(json, path);
400
- expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toBe(expected);
458
+ const actual = getValueAtPath(json, path);
459
+ expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toStrictEqual(expected);
401
460
  }
402
461
  return this;
403
462
  }
@@ -418,33 +477,6 @@ var TestResponse = class {
418
477
  expect(actual, `Expected header "${name}" to be absent, but got "${actual}"`).toBeNull();
419
478
  return this;
420
479
  }
421
- /**
422
- * Get value at dot-notation path.
423
- */
424
- getValueAtPath(obj, path) {
425
- const parts = path.split(".");
426
- let current = obj;
427
- for (const part of parts) {
428
- if (current === null || current === void 0) return;
429
- current = current[part];
430
- }
431
- return current;
432
- }
433
- /**
434
- * Check if a path exists in the object (even if value is null/undefined).
435
- */
436
- hasValueAtPath(obj, path) {
437
- const parts = path.split(".");
438
- let current = obj;
439
- for (const part of parts) {
440
- if (current === null || current === void 0) return false;
441
- if (typeof current !== "object") return false;
442
- const record = current;
443
- if (!(part in record)) return false;
444
- current = record[part];
445
- }
446
- return true;
447
- }
448
480
  };
449
481
  //#endregion
450
482
  //#region src/core/http/test-http-request.ts
@@ -470,16 +502,20 @@ var TestResponse = class {
470
502
  * .send()
471
503
  * ```
472
504
  */
473
- var TestHttpRequest = class {
505
+ var TestHttpRequest = class extends Macroable {
474
506
  body = null;
475
507
  requestHeaders;
476
508
  actingAsUser = null;
477
- constructor(method, path, headers, module, host = null) {
509
+ authResolver = null;
510
+ localeConfig;
511
+ constructor(method, path, headers, module, host = null, localeConfig = null) {
512
+ super();
478
513
  this.method = method;
479
514
  this.path = path;
480
515
  this.module = module;
481
516
  this.host = host;
482
517
  this.requestHeaders = new Headers(headers);
518
+ this.localeConfig = localeConfig;
483
519
  }
484
520
  /**
485
521
  * Set the request body
@@ -496,6 +532,22 @@ var TestHttpRequest = class {
496
532
  return this;
497
533
  }
498
534
  /**
535
+ * Set the locale for this request.
536
+ * If strategy is not provided, resolves from the module's I18n configuration.
537
+ *
538
+ * @param locale - Locale code (e.g., 'en', 'fr')
539
+ * @param strategy - Detection strategy override
540
+ */
541
+ withLocale(locale, strategy) {
542
+ const resolved = strategy ?? resolveLocaleStrategy(this.module);
543
+ this.localeConfig = {
544
+ locale,
545
+ strategy: resolved
546
+ };
547
+ applyLocaleToHeaders(this.requestHeaders, locale, resolved);
548
+ return this;
549
+ }
550
+ /**
499
551
  * Set Content-Type to application/json
500
552
  */
501
553
  asJson() {
@@ -507,6 +559,7 @@ var TestHttpRequest = class {
507
559
  */
508
560
  actingAs(user) {
509
561
  this.actingAsUser = user;
562
+ this.authResolver = null;
510
563
  return this;
511
564
  }
512
565
  /**
@@ -518,6 +571,7 @@ var TestHttpRequest = class {
518
571
  await this.applyAuthentication();
519
572
  if (this.body && !this.requestHeaders.has("Content-Type")) this.requestHeaders.set("Content-Type", "application/json");
520
573
  const url = new URL(this.path, `http://${this.host ?? "localhost"}`);
574
+ if (this.localeConfig) applyLocaleToUrl(url, this.localeConfig.locale, this.localeConfig.strategy);
521
575
  const request = new Request(url.toString(), {
522
576
  method: this.method,
523
577
  headers: this.requestHeaders,
@@ -527,9 +581,13 @@ var TestHttpRequest = class {
527
581
  }
528
582
  async applyAuthentication() {
529
583
  if (!this.actingAsUser) return;
584
+ if (this.authResolver) {
585
+ const headers = await this.authResolver(this.module, this.actingAsUser);
586
+ for (const [key, value] of headers.entries()) this.requestHeaders.set(key, value);
587
+ return;
588
+ }
530
589
  await this.module.runInRequestScope(async () => {
531
- const actingAs = new ActingAs(this.module.get(AUTH_SERVICE));
532
- const authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers();
590
+ const authHeaders = await new ActingAs(this.module.get(AUTH_SERVICE)).createSessionForUser(this.actingAsUser);
533
591
  for (const [key, value] of authHeaders.entries()) this.requestHeaders.set(key, value);
534
592
  });
535
593
  }
@@ -552,25 +610,49 @@ var TestHttpRequest = class {
552
610
  * response.assertCreated()
553
611
  * ```
554
612
  */
555
- var TestHttpClient = class {
556
- defaultHeaders = new Headers();
557
- host = null;
558
- constructor(module) {
613
+ var TestHttpClient = class TestHttpClient {
614
+ defaultHeaders;
615
+ host;
616
+ localeConfig;
617
+ constructor(module, host = null, headers = new Headers(), localeConfig = null) {
559
618
  this.module = module;
619
+ this.host = host;
620
+ this.defaultHeaders = headers;
621
+ this.localeConfig = localeConfig;
560
622
  }
561
623
  /**
562
- * Set the host for the request
624
+ * Set the host for the request (returns a new client).
625
+ * Also sets the Host header to ensure domain routing works
626
+ * even when the runtime reads the header instead of the URL host.
563
627
  */
564
628
  forHost(host) {
565
- this.host = host;
566
- return this;
629
+ const newHeaders = new Headers(this.defaultHeaders);
630
+ newHeaders.set("Host", host);
631
+ return new TestHttpClient(this.module, host, newHeaders, this.localeConfig);
567
632
  }
568
633
  /**
569
- * Set default headers for all requests
634
+ * Set default headers for all requests (returns a new client)
570
635
  */
571
636
  withHeaders(headers) {
572
- for (const [key, value] of Object.entries(headers)) this.defaultHeaders.set(key, value);
573
- return this;
637
+ const newHeaders = new Headers(this.defaultHeaders);
638
+ for (const [key, value] of Object.entries(headers)) newHeaders.set(key, value);
639
+ return new TestHttpClient(this.module, this.host, newHeaders, this.localeConfig);
640
+ }
641
+ /**
642
+ * Set the locale for all requests from this client (returns a new client).
643
+ * If strategy is not provided, resolves from the module's I18n configuration.
644
+ *
645
+ * @param locale - Locale code (e.g., 'en', 'fr')
646
+ * @param strategy - Detection strategy override
647
+ */
648
+ withLocale(locale, strategy) {
649
+ const resolved = strategy ?? resolveLocaleStrategy(this.module);
650
+ const newHeaders = new Headers(this.defaultHeaders);
651
+ applyLocaleToHeaders(newHeaders, locale, resolved);
652
+ return new TestHttpClient(this.module, this.host, newHeaders, {
653
+ locale,
654
+ strategy: resolved
655
+ });
574
656
  }
575
657
  /**
576
658
  * Create a GET request
@@ -603,7 +685,7 @@ var TestHttpClient = class {
603
685
  return this.createRequest("DELETE", path);
604
686
  }
605
687
  createRequest(method, path) {
606
- return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);
688
+ return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host, this.localeConfig);
607
689
  }
608
690
  };
609
691
  //#endregion
@@ -930,6 +1012,15 @@ var TestSseRequest = class {
930
1012
  return this;
931
1013
  }
932
1014
  /**
1015
+ * Set the locale for this SSE connection.
1016
+ * If strategy is not provided, resolves from the module's I18n configuration.
1017
+ */
1018
+ withLocale(locale, strategy) {
1019
+ const resolved = strategy ?? resolveLocaleStrategy(this.module);
1020
+ applyLocaleToHeaders(this.requestHeaders, locale, resolved);
1021
+ return this;
1022
+ }
1023
+ /**
933
1024
  * Authenticate the SSE connection as a specific user
934
1025
  */
935
1026
  actingAs(user) {
@@ -1101,6 +1192,15 @@ var TestWsRequest = class {
1101
1192
  return this;
1102
1193
  }
1103
1194
  /**
1195
+ * Set the locale for this WebSocket connection.
1196
+ * If strategy is not provided, resolves from the module's I18n configuration.
1197
+ */
1198
+ withLocale(locale, strategy) {
1199
+ const resolved = strategy ?? resolveLocaleStrategy(this.module);
1200
+ applyLocaleToHeaders(this.requestHeaders, locale, resolved);
1201
+ return this;
1202
+ }
1203
+ /**
1104
1204
  * Authenticate the WebSocket connection as a specific user
1105
1205
  */
1106
1206
  actingAs(user) {
@@ -1189,6 +1289,15 @@ var TestingModule = class {
1189
1289
  return this._http;
1190
1290
  }
1191
1291
  /**
1292
+ * Get Inertia test client for making Inertia requests
1293
+ */
1294
+ get inertia() {
1295
+ return this.http.withHeaders({
1296
+ "X-Inertia": "true",
1297
+ "X-Inertia-Version": "1"
1298
+ });
1299
+ }
1300
+ /**
1192
1301
  * Get fake storage service for assertions
1193
1302
  */
1194
1303
  get storage() {
@@ -1228,7 +1337,7 @@ var TestingModule = class {
1228
1337
  * Execute an HTTP request through HonoApp
1229
1338
  */
1230
1339
  async fetch(request) {
1231
- return this.app.hono.fetch(request, this.env, this.ctx);
1340
+ return (await this.app.ensureHono()).fetch(request, this.env, this.ctx);
1232
1341
  }
1233
1342
  /**
1234
1343
  * Run callback in request scope (for DB operations, service access)
@@ -1364,8 +1473,8 @@ var TestingModuleBuilder = class {
1364
1473
  env,
1365
1474
  ctx
1366
1475
  });
1367
- app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
1368
1476
  await app.initialize();
1477
+ app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
1369
1478
  for (const override of this.overrides) switch (override.type) {
1370
1479
  case "value":
1371
1480
  app.container.registerValue(override.token, override.implementation);
@@ -1582,6 +1691,6 @@ var TestSetupError = class extends TestError {
1582
1691
  }
1583
1692
  };
1584
1693
  //#endregion
1585
- export { ActingAs, HttpResponse, MockFetch, ProviderOverrideBuilder, Test, TestCommandRequest, TestCommandResult, TestError, TestHttpClient, TestHttpRequest, TestResponse, TestSetupError, TestSseConnection, TestSseRequest, TestWsConnection, TestWsRequest, TestingModule, TestingModuleBuilder, createMockFetch, http };
1694
+ export { ActingAs, HttpResponse, MockFetch, ProviderOverrideBuilder, Test, TestCommandRequest, TestCommandResult, TestError, TestHttpClient, TestHttpRequest, TestResponse, TestSetupError, TestSseConnection, TestSseRequest, TestWsConnection, TestWsRequest, TestingModule, TestingModuleBuilder, createMockFetch, getValueAtPath, hasValueAtPath, http };
1586
1695
 
1587
1696
  //# sourceMappingURL=index.mjs.map