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