@celerity-sdk/testing 0.8.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/dist/index.js ADDED
@@ -0,0 +1,1018 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/test-app.ts
5
+ import "reflect-metadata";
6
+ import { CelerityFactory } from "@celerity-sdk/core";
7
+
8
+ // src/discovery.ts
9
+ import "reflect-metadata";
10
+ import { isRuntimeProvidedToken } from "@celerity-sdk/common";
11
+ import { buildModuleGraph, getClassDependencyTokens } from "@celerity-sdk/core";
12
+ function discoverResourceTokens(rootModule) {
13
+ const graph = buildModuleGraph(rootModule);
14
+ const seen = /* @__PURE__ */ new Set();
15
+ const result = [];
16
+ for (const [, node] of graph) {
17
+ const classes = [
18
+ ...node.controllers,
19
+ ...node.guards.filter((g) => typeof g === "function")
20
+ ];
21
+ for (const provider of node.providers) {
22
+ if (typeof provider === "function") {
23
+ classes.push(provider);
24
+ } else if ("useClass" in provider) {
25
+ classes.push(provider.useClass);
26
+ }
27
+ }
28
+ for (const cls of classes) {
29
+ const depTokens = getClassDependencyTokens(cls);
30
+ for (const dep of depTokens) {
31
+ if (typeof dep !== "symbol") continue;
32
+ if (!isRuntimeProvidedToken(dep)) continue;
33
+ if (seen.has(dep)) continue;
34
+ seen.add(dep);
35
+ const parsed = parseResourceToken(dep);
36
+ if (parsed) {
37
+ result.push(parsed);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ return result;
43
+ }
44
+ __name(discoverResourceTokens, "discoverResourceTokens");
45
+ function parseResourceToken(token) {
46
+ const desc = token.description;
47
+ if (!desc) return null;
48
+ const parts = desc.split(":");
49
+ if (parts.length < 3 || parts[0] !== "celerity") return null;
50
+ return {
51
+ token,
52
+ type: parts[1],
53
+ name: parts.slice(2).join(":")
54
+ };
55
+ }
56
+ __name(parseResourceToken, "parseResourceToken");
57
+
58
+ // src/blueprint.ts
59
+ import { readFileSync, existsSync } from "fs";
60
+ import { resolve, join, extname } from "path";
61
+ import yaml from "js-yaml";
62
+ import stripJsonComments from "strip-json-comments";
63
+ function loadBlueprintResources(blueprintPath) {
64
+ const path = blueprintPath ?? findBlueprintPath();
65
+ if (!path) return /* @__PURE__ */ new Map();
66
+ const bp = parseBlueprint(path);
67
+ const resources = /* @__PURE__ */ new Map();
68
+ const bpResources = bp?.resources;
69
+ if (!bpResources || typeof bpResources !== "object") return resources;
70
+ for (const [id, resource] of Object.entries(bpResources)) {
71
+ if (!resource?.type) continue;
72
+ resources.set(id, {
73
+ resourceId: id,
74
+ type: resource.type,
75
+ physicalName: resource.spec?.name ?? id
76
+ });
77
+ }
78
+ return resources;
79
+ }
80
+ __name(loadBlueprintResources, "loadBlueprintResources");
81
+ function loadWebSocketConfig(blueprintPath) {
82
+ const path = blueprintPath ?? findBlueprintPath();
83
+ if (!path) return null;
84
+ const bp = parseBlueprint(path);
85
+ if (!bp || typeof bp !== "object") return null;
86
+ const resources = bp.resources;
87
+ if (!resources || typeof resources !== "object") return null;
88
+ for (const resource of Object.values(resources)) {
89
+ if (!resource || typeof resource !== "object") continue;
90
+ const res = resource;
91
+ if (res.type !== "celerity/api" || !res.spec) continue;
92
+ const spec = res.spec;
93
+ const { routeKey, authStrategy } = findWsProtocolConfig(spec.protocols);
94
+ const basePath = findWsBasePath(spec.domain);
95
+ return {
96
+ basePath,
97
+ routeKey,
98
+ authStrategy
99
+ };
100
+ }
101
+ return null;
102
+ }
103
+ __name(loadWebSocketConfig, "loadWebSocketConfig");
104
+ function findWsProtocolConfig(protocols) {
105
+ const defaults = {
106
+ routeKey: "action",
107
+ authStrategy: "authMessage"
108
+ };
109
+ if (!Array.isArray(protocols)) return defaults;
110
+ for (const proto of protocols) {
111
+ if (typeof proto !== "object" || !proto) continue;
112
+ const wsCfg = proto.websocketConfig;
113
+ if (!wsCfg || typeof wsCfg !== "object") continue;
114
+ const cfg = wsCfg;
115
+ return {
116
+ routeKey: typeof cfg.routeKey === "string" ? cfg.routeKey : defaults.routeKey,
117
+ authStrategy: cfg.authStrategy === "connect" || cfg.authStrategy === "authMessage" ? cfg.authStrategy : defaults.authStrategy
118
+ };
119
+ }
120
+ return defaults;
121
+ }
122
+ __name(findWsProtocolConfig, "findWsProtocolConfig");
123
+ function findWsBasePath(domain) {
124
+ if (!domain || typeof domain !== "object") return "/ws";
125
+ const basePaths = domain.basePaths;
126
+ if (!Array.isArray(basePaths)) return "/ws";
127
+ for (const entry of basePaths) {
128
+ if (typeof entry !== "object" || !entry) continue;
129
+ const bp = entry;
130
+ if (bp.protocol !== "websocket") continue;
131
+ return typeof bp.basePath === "string" ? bp.basePath : "/ws";
132
+ }
133
+ return "/ws";
134
+ }
135
+ __name(findWsBasePath, "findWsBasePath");
136
+ function parseBlueprint(filePath) {
137
+ const content = readFileSync(filePath, "utf-8");
138
+ const ext = extname(filePath);
139
+ if (ext === ".jsonc" || ext === ".json") {
140
+ return JSON.parse(stripJsonComments(content, {
141
+ trailingCommas: true
142
+ }));
143
+ }
144
+ return yaml.load(content);
145
+ }
146
+ __name(parseBlueprint, "parseBlueprint");
147
+ function findBlueprintPath() {
148
+ const cwd = process.cwd();
149
+ const candidates = [
150
+ join(cwd, "app.blueprint.yaml"),
151
+ join(cwd, "app.blueprint.yml"),
152
+ join(cwd, "app.blueprint.jsonc"),
153
+ join(cwd, "app.blueprint.json")
154
+ ];
155
+ for (const candidate of candidates) {
156
+ if (existsSync(candidate)) {
157
+ return resolve(candidate);
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+ __name(findBlueprintPath, "findBlueprintPath");
163
+
164
+ // src/mocks.ts
165
+ function detectMockFnCreator() {
166
+ const g = globalThis;
167
+ if (g.jest?.fn) {
168
+ return g.jest.fn;
169
+ }
170
+ if (g.vi?.fn) {
171
+ return g.vi.fn;
172
+ }
173
+ return () => {
174
+ const fn = /* @__PURE__ */ __name((..._args) => void 0, "fn");
175
+ return fn;
176
+ };
177
+ }
178
+ __name(detectMockFnCreator, "detectMockFnCreator");
179
+ function createResourceMock(resourceType, mockFn) {
180
+ const create = mockFn ?? detectMockFnCreator();
181
+ switch (resourceType) {
182
+ case "datastore":
183
+ return createDatastoreMock(create);
184
+ case "topic":
185
+ return createTopicMock(create);
186
+ case "queue":
187
+ return createQueueMock(create);
188
+ case "cache":
189
+ return createCacheMock(create);
190
+ case "bucket":
191
+ return createBucketMock(create);
192
+ case "config":
193
+ return createConfigMock(create);
194
+ default:
195
+ return null;
196
+ }
197
+ }
198
+ __name(createResourceMock, "createResourceMock");
199
+ function createDatastoreMock(fn) {
200
+ return {
201
+ getItem: fn(),
202
+ putItem: fn(),
203
+ deleteItem: fn(),
204
+ query: fn(),
205
+ scan: fn(),
206
+ batchGetItems: fn(),
207
+ batchWriteItems: fn()
208
+ };
209
+ }
210
+ __name(createDatastoreMock, "createDatastoreMock");
211
+ function createTopicMock(fn) {
212
+ return {
213
+ publish: fn(),
214
+ publishBatch: fn()
215
+ };
216
+ }
217
+ __name(createTopicMock, "createTopicMock");
218
+ function createQueueMock(fn) {
219
+ return {
220
+ sendMessage: fn(),
221
+ sendMessageBatch: fn()
222
+ };
223
+ }
224
+ __name(createQueueMock, "createQueueMock");
225
+ function createCacheMock(fn) {
226
+ return {
227
+ get: fn(),
228
+ set: fn(),
229
+ delete: fn(),
230
+ incr: fn(),
231
+ decr: fn(),
232
+ incrFloat: fn(),
233
+ mget: fn(),
234
+ mset: fn(),
235
+ mdelete: fn(),
236
+ exists: fn(),
237
+ expire: fn(),
238
+ persist: fn(),
239
+ ttl: fn(),
240
+ rename: fn(),
241
+ getSet: fn(),
242
+ append: fn(),
243
+ keyType: fn(),
244
+ scanKeys: fn(),
245
+ hashGet: fn(),
246
+ hashSet: fn(),
247
+ hashDelete: fn(),
248
+ hashGetAll: fn(),
249
+ hashExists: fn(),
250
+ hashIncr: fn(),
251
+ hashKeys: fn(),
252
+ hashLen: fn(),
253
+ listPush: fn(),
254
+ listPop: fn(),
255
+ listRange: fn(),
256
+ listLen: fn(),
257
+ listTrim: fn(),
258
+ listIndex: fn(),
259
+ setAdd: fn(),
260
+ setRemove: fn(),
261
+ setMembers: fn(),
262
+ setIsMember: fn(),
263
+ setLen: fn(),
264
+ setUnion: fn(),
265
+ setIntersect: fn(),
266
+ setDiff: fn(),
267
+ sortedSetAdd: fn(),
268
+ sortedSetRemove: fn(),
269
+ sortedSetScore: fn(),
270
+ sortedSetRank: fn(),
271
+ sortedSetRange: fn(),
272
+ sortedSetRangeByScore: fn(),
273
+ sortedSetIncr: fn(),
274
+ sortedSetLen: fn(),
275
+ transaction: fn()
276
+ };
277
+ }
278
+ __name(createCacheMock, "createCacheMock");
279
+ function createBucketMock(fn) {
280
+ return {
281
+ get: fn(),
282
+ put: fn(),
283
+ delete: fn(),
284
+ info: fn(),
285
+ exists: fn(),
286
+ list: fn(),
287
+ copy: fn(),
288
+ signUrl: fn()
289
+ };
290
+ }
291
+ __name(createBucketMock, "createBucketMock");
292
+ function createConfigMock(fn) {
293
+ return {
294
+ get: fn(),
295
+ getOrThrow: fn(),
296
+ getAll: fn(),
297
+ parse: fn()
298
+ };
299
+ }
300
+ __name(createConfigMock, "createConfigMock");
301
+ function createMocksForTokens(tokens, mockFn) {
302
+ const mocks = /* @__PURE__ */ new Map();
303
+ for (const info of tokens) {
304
+ const mock = createResourceMock(info.type, mockFn);
305
+ if (mock) {
306
+ mocks.set(info.token, mock);
307
+ }
308
+ }
309
+ return mocks;
310
+ }
311
+ __name(createMocksForTokens, "createMocksForTokens");
312
+
313
+ // src/clients.ts
314
+ async function createRealClients(tokens, blueprintResources) {
315
+ const handles = /* @__PURE__ */ new Map();
316
+ const closeables = [];
317
+ const byType = /* @__PURE__ */ new Map();
318
+ for (const info of tokens) {
319
+ const group = byType.get(info.type) ?? [];
320
+ group.push(info);
321
+ byType.set(info.type, group);
322
+ }
323
+ for (const [type, infos] of byType) {
324
+ try {
325
+ switch (type) {
326
+ case "datastore":
327
+ await createDatastoreHandles(infos, blueprintResources, handles, closeables);
328
+ break;
329
+ case "topic":
330
+ await createTopicHandles(infos, blueprintResources, handles, closeables);
331
+ break;
332
+ case "queue":
333
+ await createQueueHandles(infos, blueprintResources, handles, closeables);
334
+ break;
335
+ case "cache":
336
+ await createCacheHandles(infos, handles, closeables);
337
+ break;
338
+ case "bucket":
339
+ await createBucketHandles(infos, blueprintResources, handles, closeables);
340
+ break;
341
+ case "sqlDatabase":
342
+ await createSqlHandles(infos, blueprintResources, handles, closeables);
343
+ break;
344
+ case "config":
345
+ await createConfigHandles(infos, handles);
346
+ break;
347
+ }
348
+ } catch (err) {
349
+ const pkg = type === "sqlDatabase" ? "sql-database" : type;
350
+ throw new Error(`Failed to create ${type} client for integration test. Is @celerity-sdk/${pkg} installed? Error: ${err}`);
351
+ }
352
+ }
353
+ return {
354
+ handles,
355
+ closeables
356
+ };
357
+ }
358
+ __name(createRealClients, "createRealClients");
359
+ function physicalName(info, bp) {
360
+ return bp.get(info.name)?.physicalName ?? info.name;
361
+ }
362
+ __name(physicalName, "physicalName");
363
+ async function createConfigHandles(infos, handles) {
364
+ const { ConfigNamespaceImpl, LocalConfigBackend } = await import("@celerity-sdk/config");
365
+ const backend = new LocalConfigBackend();
366
+ for (const info of infos) {
367
+ const envKey = info.name.toUpperCase();
368
+ const storeId = process.env[`CELERITY_CONFIG_${envKey}_STORE_ID`] ?? info.name;
369
+ const ns = new ConfigNamespaceImpl(backend, storeId);
370
+ handles.set(info.token, ns);
371
+ }
372
+ }
373
+ __name(createConfigHandles, "createConfigHandles");
374
+ async function createDatastoreHandles(infos, bp, handles, closeables) {
375
+ const { createDatastoreClient } = await import("@celerity-sdk/datastore");
376
+ const client = await createDatastoreClient({
377
+ provider: "local"
378
+ });
379
+ closeables.push(client);
380
+ for (const info of infos) {
381
+ handles.set(info.token, client.datastore(physicalName(info, bp)));
382
+ }
383
+ }
384
+ __name(createDatastoreHandles, "createDatastoreHandles");
385
+ async function createTopicHandles(infos, bp, handles, closeables) {
386
+ const { createTopicClient } = await import("@celerity-sdk/topic");
387
+ const client = await createTopicClient({
388
+ provider: "local"
389
+ });
390
+ closeables.push(client);
391
+ for (const info of infos) {
392
+ handles.set(info.token, client.topic(physicalName(info, bp)));
393
+ }
394
+ }
395
+ __name(createTopicHandles, "createTopicHandles");
396
+ async function createQueueHandles(infos, bp, handles, closeables) {
397
+ const { createQueueClient } = await import("@celerity-sdk/queue");
398
+ const client = await createQueueClient({
399
+ provider: "local"
400
+ });
401
+ closeables.push(client);
402
+ for (const info of infos) {
403
+ handles.set(info.token, client.queue(physicalName(info, bp)));
404
+ }
405
+ }
406
+ __name(createQueueHandles, "createQueueHandles");
407
+ async function createCacheHandles(infos, handles, closeables) {
408
+ const { createCacheClient } = await import("@celerity-sdk/cache");
409
+ const endpoint = process.env.CELERITY_REDIS_ENDPOINT ?? "redis://localhost:6379";
410
+ const url = new URL(endpoint);
411
+ const client = await createCacheClient({
412
+ config: {
413
+ host: url.hostname,
414
+ port: Number.parseInt(url.port || "6379", 10),
415
+ tls: false,
416
+ clusterMode: false,
417
+ authMode: "password",
418
+ connectionConfig: {
419
+ connectTimeoutMs: 5e3,
420
+ commandTimeoutMs: 5e3,
421
+ keepAliveMs: 0,
422
+ maxRetries: 3,
423
+ retryDelayMs: 100,
424
+ lazyConnect: false
425
+ }
426
+ }
427
+ });
428
+ closeables.push(client);
429
+ for (const info of infos) {
430
+ handles.set(info.token, client.cache(info.name));
431
+ }
432
+ }
433
+ __name(createCacheHandles, "createCacheHandles");
434
+ async function createBucketHandles(infos, bp, handles, closeables) {
435
+ const { createObjectStorage } = await import("@celerity-sdk/bucket");
436
+ const client = await createObjectStorage({
437
+ provider: "local"
438
+ });
439
+ closeables.push(client);
440
+ for (const info of infos) {
441
+ handles.set(info.token, client.bucket(physicalName(info, bp)));
442
+ }
443
+ }
444
+ __name(createBucketHandles, "createBucketHandles");
445
+ async function createSqlHandles(infos, bp, handles, closeables) {
446
+ const connStr = process.env.CELERITY_LOCAL_SQL_DATABASE_ENDPOINT;
447
+ if (!connStr) {
448
+ throw new Error("CELERITY_LOCAL_SQL_DATABASE_ENDPOINT must be set for SQL database integration tests");
449
+ }
450
+ const { createKnexInstance } = await import("@celerity-sdk/sql-database");
451
+ const knexByResource = /* @__PURE__ */ new Map();
452
+ for (const info of infos) {
453
+ const resourceName = sqlResourceName(info.name);
454
+ const lookupInfo = {
455
+ ...info,
456
+ name: resourceName
457
+ };
458
+ const dbName = physicalName(lookupInfo, bp);
459
+ if (!knexByResource.has(dbName)) {
460
+ const url = new URL(connStr);
461
+ const engine = sqlEngineFromProtocol(url.protocol);
462
+ const defaultPort = engine === "mysql" ? "3306" : "5432";
463
+ const knexPromise = createKnexInstance({
464
+ credentials: {
465
+ getConnectionInfo: /* @__PURE__ */ __name(async () => ({
466
+ engine,
467
+ host: url.hostname,
468
+ port: Number.parseInt(url.port || defaultPort, 10),
469
+ user: url.username,
470
+ database: dbName,
471
+ ssl: false,
472
+ authMode: "password"
473
+ }), "getConnectionInfo"),
474
+ getPasswordAuth: /* @__PURE__ */ __name(async () => ({
475
+ password: url.password,
476
+ url: connStr
477
+ }), "getPasswordAuth"),
478
+ getIamAuth: /* @__PURE__ */ __name(async () => {
479
+ throw new Error("IAM auth not supported in local mode");
480
+ }, "getIamAuth")
481
+ },
482
+ deployTarget: "functions"
483
+ });
484
+ knexByResource.set(dbName, knexPromise);
485
+ closeables.push({
486
+ close: /* @__PURE__ */ __name(async () => (await knexPromise).destroy(), "close")
487
+ });
488
+ }
489
+ const knex = await knexByResource.get(dbName);
490
+ handles.set(info.token, knex);
491
+ }
492
+ }
493
+ __name(createSqlHandles, "createSqlHandles");
494
+ function sqlEngineFromProtocol(protocol) {
495
+ if (protocol === "mysql:" || protocol === "mysql2:") return "mysql";
496
+ return "postgres";
497
+ }
498
+ __name(sqlEngineFromProtocol, "sqlEngineFromProtocol");
499
+ function sqlResourceName(tokenName) {
500
+ const colonIdx = tokenName.indexOf(":");
501
+ if (colonIdx === -1) return tokenName;
502
+ return tokenName.slice(colonIdx + 1);
503
+ }
504
+ __name(sqlResourceName, "sqlResourceName");
505
+
506
+ // src/test-app.ts
507
+ var TestApp = class {
508
+ static {
509
+ __name(this, "TestApp");
510
+ }
511
+ mocks;
512
+ closeables;
513
+ inner;
514
+ constructor(inner, mocks, closeables) {
515
+ this.inner = inner;
516
+ this.mocks = mocks;
517
+ this.closeables = closeables;
518
+ }
519
+ // -- Delegate to TestingApplication --
520
+ injectHttp(request) {
521
+ return this.inner.injectHttp(request);
522
+ }
523
+ injectWebSocket(route, message) {
524
+ return this.inner.injectWebSocket(route, message);
525
+ }
526
+ injectConsumer(handlerTag, event) {
527
+ return this.inner.injectConsumer(handlerTag, event);
528
+ }
529
+ injectSchedule(handlerTag, event) {
530
+ return this.inner.injectSchedule(handlerTag, event);
531
+ }
532
+ injectCustom(name, payload) {
533
+ return this.inner.injectCustom(name, payload);
534
+ }
535
+ getContainer() {
536
+ return this.inner.getContainer();
537
+ }
538
+ getRegistry() {
539
+ return this.inner.getRegistry();
540
+ }
541
+ // -- Mock access (unit mode) --
542
+ /** Retrieve the auto-generated mock for a resource token. */
543
+ getMock(token) {
544
+ const mock = this.mocks.get(token);
545
+ if (!mock) {
546
+ throw new Error(`No mock found for token: ${String(token)}`);
547
+ }
548
+ return mock;
549
+ }
550
+ /** Get the mock Datastore for a named resource (e.g., "usersDatastore"). */
551
+ getDatastoreMock(name) {
552
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:datastore:${name}`));
553
+ }
554
+ /** Get the mock Topic for a named resource. */
555
+ getTopicMock(name) {
556
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:topic:${name}`));
557
+ }
558
+ /** Get the mock Queue for a named resource. */
559
+ getQueueMock(name) {
560
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:queue:${name}`));
561
+ }
562
+ /** Get the mock Cache for a named resource. */
563
+ getCacheMock(name) {
564
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:cache:${name}`));
565
+ }
566
+ /** Get the mock Bucket for a named resource. */
567
+ getBucketMock(name) {
568
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:bucket:${name}`));
569
+ }
570
+ /** Get the mock Config namespace for a named resource. */
571
+ getConfigMock(name) {
572
+ return this.getMock(/* @__PURE__ */ Symbol.for(`celerity:config:${name}`));
573
+ }
574
+ // -- Lifecycle --
575
+ /** Close all real resource clients (integration mode). No-op in unit mode. */
576
+ async close() {
577
+ for (const client of this.closeables) {
578
+ try {
579
+ await client.close?.();
580
+ } catch {
581
+ }
582
+ }
583
+ }
584
+ };
585
+ async function createTestApp(options) {
586
+ const { providers = [], controllers = [], guards = [], overrides = {} } = options;
587
+ const { inner, mocks, closeables } = await setupResources(options);
588
+ const container = inner.getContainer();
589
+ for (const provider of providers) {
590
+ if (typeof provider === "function") {
591
+ container.registerClass(provider);
592
+ } else {
593
+ container.register(provider.provide, provider);
594
+ }
595
+ }
596
+ for (const ctrl of controllers) {
597
+ if (!container.has(ctrl)) container.registerClass(ctrl);
598
+ }
599
+ for (const guard of guards) {
600
+ if (!container.has(guard)) container.registerClass(guard);
601
+ }
602
+ applyOverrides(container, overrides);
603
+ return new TestApp(inner, mocks, closeables);
604
+ }
605
+ __name(createTestApp, "createTestApp");
606
+ async function setupResources(options) {
607
+ const { module: rootModule, integration = false, blueprintPath } = options;
608
+ const resourceInfos = discoverResourceTokens(rootModule);
609
+ let mocks = /* @__PURE__ */ new Map();
610
+ let closeables = [];
611
+ let resourceHandles = /* @__PURE__ */ new Map();
612
+ if (integration) {
613
+ const blueprintResources = loadBlueprintResources(blueprintPath);
614
+ const result = await createRealClients(resourceInfos, blueprintResources);
615
+ resourceHandles = result.handles;
616
+ closeables = result.closeables;
617
+ } else {
618
+ mocks = createMocksForTokens(resourceInfos);
619
+ for (const [token, mock] of mocks) {
620
+ resourceHandles.set(token, mock);
621
+ }
622
+ }
623
+ const inner = await CelerityFactory.createTestingApp(rootModule, {
624
+ systemLayers: []
625
+ });
626
+ const container = inner.getContainer();
627
+ for (const [token, handle] of resourceHandles) {
628
+ container.registerValue(token, handle);
629
+ }
630
+ return {
631
+ inner,
632
+ mocks,
633
+ closeables
634
+ };
635
+ }
636
+ __name(setupResources, "setupResources");
637
+ function applyOverrides(container, overrides) {
638
+ for (const [token, value] of Object.entries(overrides)) {
639
+ container.registerValue(token, value);
640
+ }
641
+ for (const sym of Object.getOwnPropertySymbols(overrides)) {
642
+ container.registerValue(sym, overrides[sym]);
643
+ }
644
+ }
645
+ __name(applyOverrides, "applyOverrides");
646
+
647
+ // src/jwt.ts
648
+ async function generateTestToken(options) {
649
+ const baseURL = process.env.CELERITY_DEV_AUTH_BASE_URL ?? "http://localhost:9099";
650
+ const body = {
651
+ sub: options?.sub ?? "test-user"
652
+ };
653
+ if (options?.claims) {
654
+ body.claims = options.claims;
655
+ }
656
+ if (options?.expiresIn) {
657
+ body.expiresIn = options.expiresIn;
658
+ }
659
+ const response = await fetch(`${baseURL}/token`, {
660
+ method: "POST",
661
+ headers: {
662
+ "Content-Type": "application/json"
663
+ },
664
+ body: JSON.stringify(body)
665
+ });
666
+ if (!response.ok) {
667
+ const text = await response.text();
668
+ throw new Error(`Dev auth server returned ${response.status}: ${text}`);
669
+ }
670
+ const data = await response.json();
671
+ return data.access_token;
672
+ }
673
+ __name(generateTestToken, "generateTestToken");
674
+
675
+ // src/http.ts
676
+ var TestRequest = class {
677
+ static {
678
+ __name(this, "TestRequest");
679
+ }
680
+ baseUrl;
681
+ method;
682
+ path;
683
+ _headers = {};
684
+ _body;
685
+ _expectations = [];
686
+ constructor(baseUrl, method, path) {
687
+ this.baseUrl = baseUrl;
688
+ this.method = method;
689
+ this.path = path;
690
+ }
691
+ /** Set an authorization bearer token. */
692
+ auth(token) {
693
+ this._headers["Authorization"] = `Bearer ${token}`;
694
+ return this;
695
+ }
696
+ /** Set a request header. */
697
+ set(key, value) {
698
+ this._headers[key] = value;
699
+ return this;
700
+ }
701
+ /** Set the request JSON body. */
702
+ send(body) {
703
+ this._body = body;
704
+ return this;
705
+ }
706
+ expect(first, second) {
707
+ if (typeof first === "number") {
708
+ this._expectations.push({
709
+ type: "status",
710
+ value: first
711
+ });
712
+ } else if (typeof first === "string" && second !== void 0) {
713
+ this._expectations.push({
714
+ type: "header",
715
+ key: first.toLowerCase(),
716
+ value: second
717
+ });
718
+ } else if (typeof first === "function") {
719
+ this._expectations.push({
720
+ type: "body",
721
+ value: first
722
+ });
723
+ } else {
724
+ this._expectations.push({
725
+ type: "body",
726
+ value: first
727
+ });
728
+ }
729
+ return this;
730
+ }
731
+ /** Execute the request and run all expectations. Returns the response. */
732
+ async end() {
733
+ const headers = {
734
+ ...this._headers
735
+ };
736
+ if (this._body !== void 0) {
737
+ headers["Content-Type"] = "application/json";
738
+ }
739
+ const response = await fetch(`${this.baseUrl}${this.path}`, {
740
+ method: this.method,
741
+ headers,
742
+ body: this._body !== void 0 ? JSON.stringify(this._body) : void 0
743
+ });
744
+ const text = await response.text();
745
+ let body;
746
+ try {
747
+ body = JSON.parse(text);
748
+ } catch {
749
+ body = text;
750
+ }
751
+ const result = {
752
+ status: response.status,
753
+ headers: response.headers,
754
+ body,
755
+ text
756
+ };
757
+ for (const exp of this._expectations) {
758
+ this.assertExpectation(exp, result, text);
759
+ }
760
+ return result;
761
+ }
762
+ assertExpectation(exp, result, text) {
763
+ if (exp.type === "status") {
764
+ if (result.status !== exp.value) {
765
+ throw new Error(`Expected status ${exp.value} but got ${result.status}.
766
+ Body: ${text}`);
767
+ }
768
+ return;
769
+ }
770
+ if (exp.type === "body") {
771
+ if (typeof exp.value === "function") {
772
+ exp.value(result.body);
773
+ } else {
774
+ const expected = JSON.stringify(exp.value);
775
+ const actual = JSON.stringify(result.body);
776
+ if (expected !== actual) {
777
+ throw new Error(`Expected body ${expected} but got ${actual}`);
778
+ }
779
+ }
780
+ return;
781
+ }
782
+ if (exp.type === "header") {
783
+ this.assertHeader(exp.key, exp.value, result.headers);
784
+ }
785
+ }
786
+ assertHeader(key, expected, headers) {
787
+ const actual = headers.get(key);
788
+ if (expected instanceof RegExp) {
789
+ if (!actual || !expected.test(actual)) {
790
+ throw new Error(`Expected header "${key}" to match ${expected} but got "${actual}"`);
791
+ }
792
+ return;
793
+ }
794
+ if (actual !== expected) {
795
+ throw new Error(`Expected header "${key}" to be "${expected}" but got "${actual}"`);
796
+ }
797
+ }
798
+ /** Implements PromiseLike so the request chain can be awaited directly. */
799
+ then(resolve2, reject) {
800
+ return this.end().then(resolve2, reject);
801
+ }
802
+ };
803
+ var TestHttpClient = class {
804
+ static {
805
+ __name(this, "TestHttpClient");
806
+ }
807
+ baseUrl;
808
+ constructor(baseUrl) {
809
+ this.baseUrl = baseUrl;
810
+ }
811
+ get(path) {
812
+ return new TestRequest(this.baseUrl, "GET", path);
813
+ }
814
+ post(path) {
815
+ return new TestRequest(this.baseUrl, "POST", path);
816
+ }
817
+ put(path) {
818
+ return new TestRequest(this.baseUrl, "PUT", path);
819
+ }
820
+ patch(path) {
821
+ return new TestRequest(this.baseUrl, "PATCH", path);
822
+ }
823
+ delete(path) {
824
+ return new TestRequest(this.baseUrl, "DELETE", path);
825
+ }
826
+ };
827
+ function createTestClient(options) {
828
+ const baseUrl = options?.baseUrl ?? process.env.CELERITY_TEST_BASE_URL ?? "http://localhost:8081";
829
+ return new TestHttpClient(baseUrl);
830
+ }
831
+ __name(createTestClient, "createTestClient");
832
+
833
+ // src/wait.ts
834
+ async function waitFor(predicate, options) {
835
+ const timeout = options?.timeout ?? 5e3;
836
+ const interval = options?.interval ?? 100;
837
+ const deadline = Date.now() + timeout;
838
+ while (Date.now() < deadline) {
839
+ const result = await predicate();
840
+ if (result) return;
841
+ await new Promise((resolve2) => setTimeout(resolve2, interval));
842
+ }
843
+ throw new Error(`waitFor timed out after ${timeout}ms`);
844
+ }
845
+ __name(waitFor, "waitFor");
846
+
847
+ // src/ws.ts
848
+ async function createTestWsClient(options) {
849
+ const wsModule = await import("@celerity-sdk/ws-client");
850
+ const wsConfig = loadWebSocketConfig(options?.blueprintPath);
851
+ const basePath = wsConfig?.basePath ?? "/ws";
852
+ const routeKey = wsConfig?.routeKey ?? "action";
853
+ const authStrategy = wsConfig?.authStrategy ?? "authMessage";
854
+ const baseUrl = process.env.CELERITY_TEST_BASE_URL ?? "http://localhost:8081";
855
+ const wsUrl = options?.url ?? baseUrl.replace(/^http/, "ws") + basePath;
856
+ const authConfig = resolveAuthConfig(options?.token, authStrategy);
857
+ const innerClient = await wsModule.createWsClient({
858
+ url: wsUrl,
859
+ routeKey,
860
+ auth: authConfig,
861
+ ...options?.clientConfig
862
+ });
863
+ return new TestWsClient(innerClient);
864
+ }
865
+ __name(createTestWsClient, "createTestWsClient");
866
+ var TestWsClient = class {
867
+ static {
868
+ __name(this, "TestWsClient");
869
+ }
870
+ inner;
871
+ messageBuffer = [];
872
+ waiters = [];
873
+ cleanups = [];
874
+ connectionError = null;
875
+ constructor(inner) {
876
+ this.inner = inner;
877
+ }
878
+ /**
879
+ * Connect and complete the full handshake (including auth if configured).
880
+ *
881
+ * After connect resolves, `nextMessage()` will receive application messages.
882
+ * If auth fails or connection is rejected, the promise rejects with the error.
883
+ */
884
+ async connect() {
885
+ const errorUnsub = this.inner.on("error", (err) => {
886
+ this.connectionError = err;
887
+ this.rejectAllWaiters(err);
888
+ });
889
+ this.cleanups.push(errorUnsub);
890
+ const disconnectUnsub = this.inner.on("disconnected", () => {
891
+ this.rejectAllWaiters(new Error("WebSocket disconnected while waiting for message"));
892
+ });
893
+ this.cleanups.push(disconnectUnsub);
894
+ const messageUnsub = this.inner.on("*", (data, meta) => {
895
+ const route = meta?.route ?? "";
896
+ const msg = {
897
+ route,
898
+ data
899
+ };
900
+ if (this.waiters.length > 0) {
901
+ const waiter = this.waiters.shift();
902
+ clearTimeout(waiter.timer);
903
+ waiter.resolve(msg);
904
+ } else {
905
+ this.messageBuffer.push(msg);
906
+ }
907
+ });
908
+ this.cleanups.push(messageUnsub);
909
+ await this.inner.connect();
910
+ }
911
+ /** Send a routed application message. Returns the message ID. */
912
+ send(route, data) {
913
+ return this.inner.send(route, data);
914
+ }
915
+ /**
916
+ * Wait for the next application message from the server.
917
+ *
918
+ * If a message is already buffered, resolves immediately.
919
+ * Otherwise blocks until a message arrives, the connection closes, or timeout expires.
920
+ *
921
+ * @param timeout - Maximum wait time in ms. Default: 5000.
922
+ */
923
+ nextMessage(timeout = 5e3) {
924
+ if (this.connectionError) {
925
+ return Promise.reject(this.connectionError);
926
+ }
927
+ if (this.messageBuffer.length > 0) {
928
+ return Promise.resolve(this.messageBuffer.shift());
929
+ }
930
+ return new Promise((resolve2, reject) => {
931
+ const timer = setTimeout(() => {
932
+ const idx = this.waiters.findIndex((w) => w.resolve === resolve2);
933
+ if (idx !== -1) this.waiters.splice(idx, 1);
934
+ reject(new Error(`nextMessage timed out after ${timeout}ms`));
935
+ }, timeout);
936
+ this.waiters.push({
937
+ resolve: resolve2,
938
+ reject,
939
+ timer
940
+ });
941
+ });
942
+ }
943
+ /** Disconnect and clean up all listeners and pending waiters. */
944
+ async disconnect() {
945
+ this.cleanup();
946
+ await this.inner.disconnect();
947
+ }
948
+ /** Force destroy without waiting for close handshake. */
949
+ destroy() {
950
+ this.cleanup();
951
+ this.inner.destroy();
952
+ }
953
+ /** Current connection state. */
954
+ get state() {
955
+ return this.inner.state;
956
+ }
957
+ /** Access the underlying CelerityWsClient for advanced usage. */
958
+ get raw() {
959
+ return this.inner;
960
+ }
961
+ cleanup() {
962
+ for (const unsub of this.cleanups) unsub();
963
+ this.cleanups = [];
964
+ this.rejectAllWaiters(new Error("WebSocket client closed"));
965
+ this.messageBuffer = [];
966
+ }
967
+ rejectAllWaiters(err) {
968
+ for (const waiter of this.waiters) {
969
+ clearTimeout(waiter.timer);
970
+ waiter.reject(err);
971
+ }
972
+ this.waiters = [];
973
+ }
974
+ };
975
+ function resolveAuthConfig(token, authStrategy) {
976
+ if (token === null) return void 0;
977
+ if (typeof token === "string") return {
978
+ strategy: authStrategy,
979
+ token
980
+ };
981
+ const tokenOpts = token ?? {
982
+ sub: "test-user",
983
+ claims: {
984
+ roles: [
985
+ "admin"
986
+ ]
987
+ }
988
+ };
989
+ return {
990
+ strategy: authStrategy,
991
+ token: /* @__PURE__ */ __name(() => generateTestToken(tokenOpts), "token")
992
+ };
993
+ }
994
+ __name(resolveAuthConfig, "resolveAuthConfig");
995
+
996
+ // src/index.ts
997
+ import { mockRequest, mockWebSocketMessage, mockConsumerEvent, mockScheduleEvent } from "@celerity-sdk/core";
998
+ export {
999
+ TestApp,
1000
+ TestHttpClient,
1001
+ TestRequest,
1002
+ TestWsClient,
1003
+ createMocksForTokens,
1004
+ createResourceMock,
1005
+ createTestApp,
1006
+ createTestClient,
1007
+ createTestWsClient,
1008
+ discoverResourceTokens,
1009
+ generateTestToken,
1010
+ loadBlueprintResources,
1011
+ loadWebSocketConfig,
1012
+ mockConsumerEvent,
1013
+ mockRequest,
1014
+ mockScheduleEvent,
1015
+ mockWebSocketMessage,
1016
+ waitFor
1017
+ };
1018
+ //# sourceMappingURL=index.js.map