@wopr-network/platform-core 1.68.0 → 1.70.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.
Files changed (93) hide show
  1. package/dist/backup/types.d.ts +1 -1
  2. package/dist/db/schema/pool-config.d.ts +41 -0
  3. package/dist/db/schema/pool-config.js +5 -0
  4. package/dist/db/schema/pool-instances.d.ts +126 -0
  5. package/dist/db/schema/pool-instances.js +10 -0
  6. package/dist/server/__tests__/build-container.test.d.ts +1 -0
  7. package/dist/server/__tests__/build-container.test.js +339 -0
  8. package/dist/server/__tests__/container.test.d.ts +1 -0
  9. package/dist/server/__tests__/container.test.js +173 -0
  10. package/dist/server/__tests__/lifecycle.test.d.ts +1 -0
  11. package/dist/server/__tests__/lifecycle.test.js +90 -0
  12. package/dist/server/__tests__/mount-routes.test.d.ts +1 -0
  13. package/dist/server/__tests__/mount-routes.test.js +151 -0
  14. package/dist/server/boot-config.d.ts +51 -0
  15. package/dist/server/boot-config.js +7 -0
  16. package/dist/server/container.d.ts +97 -0
  17. package/dist/server/container.js +148 -0
  18. package/dist/server/index.d.ts +33 -0
  19. package/dist/server/index.js +66 -0
  20. package/dist/server/lifecycle.d.ts +25 -0
  21. package/dist/server/lifecycle.js +56 -0
  22. package/dist/server/middleware/__tests__/admin-auth.test.d.ts +1 -0
  23. package/dist/server/middleware/__tests__/admin-auth.test.js +59 -0
  24. package/dist/server/middleware/__tests__/tenant-proxy.test.d.ts +1 -0
  25. package/dist/server/middleware/__tests__/tenant-proxy.test.js +268 -0
  26. package/dist/server/middleware/admin-auth.d.ts +18 -0
  27. package/dist/server/middleware/admin-auth.js +38 -0
  28. package/dist/server/middleware/tenant-proxy.d.ts +56 -0
  29. package/dist/server/middleware/tenant-proxy.js +162 -0
  30. package/dist/server/mount-routes.d.ts +30 -0
  31. package/dist/server/mount-routes.js +74 -0
  32. package/dist/server/routes/__tests__/admin.test.d.ts +1 -0
  33. package/dist/server/routes/__tests__/admin.test.js +267 -0
  34. package/dist/server/routes/__tests__/crypto-webhook.test.d.ts +1 -0
  35. package/dist/server/routes/__tests__/crypto-webhook.test.js +137 -0
  36. package/dist/server/routes/__tests__/provision-webhook.test.d.ts +1 -0
  37. package/dist/server/routes/__tests__/provision-webhook.test.js +212 -0
  38. package/dist/server/routes/__tests__/stripe-webhook.test.d.ts +1 -0
  39. package/dist/server/routes/__tests__/stripe-webhook.test.js +65 -0
  40. package/dist/server/routes/admin.d.ts +129 -0
  41. package/dist/server/routes/admin.js +294 -0
  42. package/dist/server/routes/crypto-webhook.d.ts +23 -0
  43. package/dist/server/routes/crypto-webhook.js +82 -0
  44. package/dist/server/routes/provision-webhook.d.ts +38 -0
  45. package/dist/server/routes/provision-webhook.js +160 -0
  46. package/dist/server/routes/stripe-webhook.d.ts +10 -0
  47. package/dist/server/routes/stripe-webhook.js +29 -0
  48. package/dist/server/services/hot-pool-claim.d.ts +30 -0
  49. package/dist/server/services/hot-pool-claim.js +92 -0
  50. package/dist/server/services/hot-pool.d.ts +25 -0
  51. package/dist/server/services/hot-pool.js +129 -0
  52. package/dist/server/services/pool-repository.d.ts +44 -0
  53. package/dist/server/services/pool-repository.js +72 -0
  54. package/dist/server/test-container.d.ts +15 -0
  55. package/dist/server/test-container.js +103 -0
  56. package/dist/trpc/auth-helpers.d.ts +17 -0
  57. package/dist/trpc/auth-helpers.js +26 -0
  58. package/dist/trpc/container-factories.d.ts +300 -0
  59. package/dist/trpc/container-factories.js +80 -0
  60. package/dist/trpc/index.d.ts +2 -0
  61. package/dist/trpc/index.js +2 -0
  62. package/drizzle/migrations/0025_hot_pool_tables.sql +29 -0
  63. package/package.json +5 -1
  64. package/src/db/schema/pool-config.ts +6 -0
  65. package/src/db/schema/pool-instances.ts +11 -0
  66. package/src/server/__tests__/build-container.test.ts +402 -0
  67. package/src/server/__tests__/container.test.ts +207 -0
  68. package/src/server/__tests__/lifecycle.test.ts +106 -0
  69. package/src/server/__tests__/mount-routes.test.ts +169 -0
  70. package/src/server/boot-config.ts +84 -0
  71. package/src/server/container.ts +264 -0
  72. package/src/server/index.ts +92 -0
  73. package/src/server/lifecycle.ts +72 -0
  74. package/src/server/middleware/__tests__/admin-auth.test.ts +67 -0
  75. package/src/server/middleware/__tests__/tenant-proxy.test.ts +308 -0
  76. package/src/server/middleware/admin-auth.ts +51 -0
  77. package/src/server/middleware/tenant-proxy.ts +192 -0
  78. package/src/server/mount-routes.ts +113 -0
  79. package/src/server/routes/__tests__/admin.test.ts +320 -0
  80. package/src/server/routes/__tests__/crypto-webhook.test.ts +167 -0
  81. package/src/server/routes/__tests__/provision-webhook.test.ts +323 -0
  82. package/src/server/routes/__tests__/stripe-webhook.test.ts +73 -0
  83. package/src/server/routes/admin.ts +360 -0
  84. package/src/server/routes/crypto-webhook.ts +110 -0
  85. package/src/server/routes/provision-webhook.ts +212 -0
  86. package/src/server/routes/stripe-webhook.ts +36 -0
  87. package/src/server/services/hot-pool-claim.ts +130 -0
  88. package/src/server/services/hot-pool.ts +174 -0
  89. package/src/server/services/pool-repository.ts +107 -0
  90. package/src/server/test-container.ts +120 -0
  91. package/src/trpc/auth-helpers.ts +28 -0
  92. package/src/trpc/container-factories.ts +114 -0
  93. package/src/trpc/index.ts +9 -0
@@ -0,0 +1,402 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { BootConfig } from "../boot-config.js";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Mocks — all class mocks use real classes (not arrow fns) so `new` works.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ // Mock pg.Pool
9
+ const mockPoolEnd = vi.fn().mockResolvedValue(undefined);
10
+ const poolConstructorCalls: Array<{ connectionString: string }> = [];
11
+ class MockPoolClass {
12
+ connectionString: string;
13
+ end = mockPoolEnd;
14
+ query = vi.fn().mockResolvedValue({ rows: [] });
15
+ constructor(opts: { connectionString: string }) {
16
+ this.connectionString = opts.connectionString;
17
+ poolConstructorCalls.push(opts);
18
+ }
19
+ }
20
+ vi.mock("pg", () => ({ Pool: MockPoolClass }));
21
+
22
+ // Mock drizzle db creation
23
+ const mockDb = { __brand: "drizzle-db" } as never;
24
+ vi.mock("../../db/index.js", () => ({
25
+ createDb: vi.fn(() => mockDb),
26
+ }));
27
+
28
+ // Mock drizzle migrator
29
+ const mockMigrate = vi.fn().mockResolvedValue(undefined);
30
+ vi.mock("drizzle-orm/node-postgres/migrator", () => ({
31
+ migrate: mockMigrate,
32
+ }));
33
+
34
+ // Mock platformBoot
35
+ const mockProductConfig = {
36
+ product: {
37
+ slug: "test",
38
+ brandName: "Test",
39
+ domain: "test.dev",
40
+ appDomain: "app.test.dev",
41
+ fromEmail: "hi@test.dev",
42
+ emailSupport: "support@test.dev",
43
+ },
44
+ navItems: [],
45
+ domains: [],
46
+ features: null,
47
+ fleet: null,
48
+ billing: null,
49
+ };
50
+ const mockPlatformBoot = vi.fn().mockResolvedValue({
51
+ service: {},
52
+ config: mockProductConfig,
53
+ corsOrigins: ["https://test.dev"],
54
+ seeded: false,
55
+ });
56
+ vi.mock("../../product-config/boot.js", () => ({
57
+ platformBoot: (...args: unknown[]) => mockPlatformBoot(...args),
58
+ }));
59
+
60
+ // Mock DrizzleLedger
61
+ const mockSeedSystemAccounts = vi.fn().mockResolvedValue(undefined);
62
+ class MockDrizzleLedgerClass {
63
+ seedSystemAccounts = mockSeedSystemAccounts;
64
+ balance = vi.fn().mockResolvedValue(0);
65
+ credit = vi.fn();
66
+ debit = vi.fn();
67
+ post = vi.fn();
68
+ hasReferenceId = vi.fn().mockResolvedValue(false);
69
+ history = vi.fn().mockResolvedValue([]);
70
+ tenantsWithBalance = vi.fn().mockResolvedValue([]);
71
+ memberUsage = vi.fn().mockResolvedValue([]);
72
+ lifetimeSpend = vi.fn().mockResolvedValue(0);
73
+ lifetimeSpendBatch = vi.fn().mockResolvedValue(new Map());
74
+ expiredCredits = vi.fn().mockResolvedValue([]);
75
+ trialBalance = vi.fn().mockResolvedValue({ balanced: true });
76
+ accountBalance = vi.fn().mockResolvedValue(0);
77
+ existsByReferenceIdLike = vi.fn().mockResolvedValue(false);
78
+ sumPurchasesForPeriod = vi.fn().mockResolvedValue(0);
79
+ getActiveTenantIdsInWindow = vi.fn().mockResolvedValue([]);
80
+ debitCapped = vi.fn().mockResolvedValue(null);
81
+ }
82
+ vi.mock("../../credits/ledger.js", async (importOriginal) => {
83
+ const orig = await importOriginal<Record<string, unknown>>();
84
+ return { ...orig, DrizzleLedger: MockDrizzleLedgerClass };
85
+ });
86
+
87
+ // Mock org/tenancy
88
+ class MockDrizzleOrgMemberRepositoryClass {
89
+ listMembers = vi.fn().mockResolvedValue([]);
90
+ addMember = vi.fn();
91
+ }
92
+ vi.mock("../../tenancy/org-member-repository.js", () => ({
93
+ DrizzleOrgMemberRepository: MockDrizzleOrgMemberRepositoryClass,
94
+ }));
95
+
96
+ class MockDrizzleOrgRepositoryClass {}
97
+ vi.mock("../../tenancy/drizzle-org-repository.js", () => ({
98
+ DrizzleOrgRepository: MockDrizzleOrgRepositoryClass,
99
+ }));
100
+
101
+ const orgServiceConstructorCalls: unknown[][] = [];
102
+ class MockOrgServiceClass {
103
+ getOrCreatePersonalOrg = vi.fn();
104
+ constructor(...args: unknown[]) {
105
+ orgServiceConstructorCalls.push(args);
106
+ }
107
+ }
108
+ vi.mock("../../tenancy/org-service.js", () => ({
109
+ OrgService: MockOrgServiceClass,
110
+ }));
111
+
112
+ // Mock auth
113
+ class MockBetterAuthUserRepositoryClass {}
114
+ vi.mock("../../db/auth-user-repository.js", () => ({
115
+ BetterAuthUserRepository: MockBetterAuthUserRepositoryClass,
116
+ }));
117
+
118
+ class MockDrizzleUserRoleRepositoryClass {
119
+ isPlatformAdmin = vi.fn().mockResolvedValue(false);
120
+ listRolesByUser = vi.fn().mockResolvedValue([]);
121
+ getTenantIdByUserId = vi.fn().mockResolvedValue(null);
122
+ grantRole = vi.fn();
123
+ revokeRole = vi.fn().mockResolvedValue(false);
124
+ listUsersByRole = vi.fn().mockResolvedValue([]);
125
+ }
126
+ vi.mock("../../auth/user-role-repository.js", () => ({
127
+ DrizzleUserRoleRepository: MockDrizzleUserRoleRepositoryClass,
128
+ }));
129
+
130
+ // Mock fleet deps (only imported when fleet is enabled)
131
+ class MockFleetManagerClass {
132
+ __brand = "fleet-manager";
133
+ }
134
+ vi.mock("../../fleet/fleet-manager.js", () => ({
135
+ FleetManager: MockFleetManagerClass,
136
+ }));
137
+
138
+ class MockProfileStoreClass {
139
+ __brand = "profile-store";
140
+ }
141
+ vi.mock("../../fleet/profile-store.js", () => ({
142
+ ProfileStore: MockProfileStoreClass,
143
+ }));
144
+
145
+ class MockProxyManagerClass {
146
+ __brand = "proxy-manager";
147
+ }
148
+ vi.mock("../../proxy/manager.js", () => ({
149
+ ProxyManager: MockProxyManagerClass,
150
+ }));
151
+
152
+ class MockDrizzleServiceKeyRepositoryClass {
153
+ __brand = "service-key-repo";
154
+ }
155
+ vi.mock("../../gateway/service-key-repository.js", () => ({
156
+ DrizzleServiceKeyRepository: MockDrizzleServiceKeyRepositoryClass,
157
+ }));
158
+
159
+ class MockDockerClass {
160
+ __brand = "docker";
161
+ }
162
+ vi.mock("dockerode", () => ({
163
+ default: MockDockerClass,
164
+ }));
165
+
166
+ // Mock crypto deps
167
+ class MockDrizzleCryptoChargeRepositoryClass {
168
+ __brand = "charge-repo";
169
+ }
170
+ vi.mock("../../billing/crypto/charge-store.js", () => ({
171
+ DrizzleCryptoChargeRepository: MockDrizzleCryptoChargeRepositoryClass,
172
+ }));
173
+
174
+ class MockDrizzleWebhookSeenRepositoryClass {
175
+ __brand = "webhook-seen-repo";
176
+ }
177
+ vi.mock("../../billing/drizzle-webhook-seen-repository.js", () => ({
178
+ DrizzleWebhookSeenRepository: MockDrizzleWebhookSeenRepositoryClass,
179
+ }));
180
+
181
+ // Mock stripe deps
182
+ class MockStripeClass {
183
+ __brand = "stripe-client";
184
+ }
185
+ vi.mock("stripe", () => ({
186
+ default: MockStripeClass,
187
+ }));
188
+
189
+ class MockDrizzleTenantCustomerRepositoryClass {
190
+ __brand = "tenant-customer-repo";
191
+ }
192
+ vi.mock("../../billing/stripe/tenant-store.js", () => ({
193
+ DrizzleTenantCustomerRepository: MockDrizzleTenantCustomerRepositoryClass,
194
+ }));
195
+
196
+ vi.mock("../../billing/stripe/credit-prices.js", () => ({
197
+ loadCreditPriceMap: vi.fn(() => new Map()),
198
+ }));
199
+
200
+ class MockStripePaymentProcessorClass {
201
+ __brand = "stripe-processor";
202
+ handleWebhook = vi.fn();
203
+ }
204
+ vi.mock("../../billing/stripe/stripe-payment-processor.js", () => ({
205
+ StripePaymentProcessor: MockStripePaymentProcessorClass,
206
+ }));
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // Helpers
210
+ // ---------------------------------------------------------------------------
211
+
212
+ function baseBootConfig(overrides?: Partial<BootConfig>): BootConfig {
213
+ return {
214
+ slug: "test-product",
215
+ databaseUrl: "postgres://localhost:5432/testdb",
216
+ provisionSecret: "test-secret",
217
+ features: {
218
+ fleet: false,
219
+ crypto: false,
220
+ stripe: false,
221
+ gateway: false,
222
+ hotPool: false,
223
+ },
224
+ ...overrides,
225
+ };
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Tests
230
+ // ---------------------------------------------------------------------------
231
+
232
+ describe("buildContainer", () => {
233
+ // Dynamic import so mocks are registered before the module loads
234
+ async function loadBuildContainer() {
235
+ const mod = await import("../container.js");
236
+ return mod.buildContainer;
237
+ }
238
+
239
+ it("throws on empty databaseUrl", async () => {
240
+ const buildContainer = await loadBuildContainer();
241
+ const config = baseBootConfig({ databaseUrl: "" });
242
+
243
+ await expect(buildContainer(config)).rejects.toThrow("databaseUrl is required");
244
+ });
245
+
246
+ it("creates pool with correct connectionString", async () => {
247
+ const buildContainer = await loadBuildContainer();
248
+ const config = baseBootConfig();
249
+ poolConstructorCalls.length = 0;
250
+
251
+ const container = await buildContainer(config);
252
+
253
+ expect(poolConstructorCalls).toContainEqual({
254
+ connectionString: "postgres://localhost:5432/testdb",
255
+ });
256
+ expect(container.pool).toBeDefined();
257
+ });
258
+
259
+ it("calls platformBoot with correct slug", async () => {
260
+ const buildContainer = await loadBuildContainer();
261
+ const config = baseBootConfig({ slug: "paperclip" });
262
+
263
+ await buildContainer(config);
264
+
265
+ expect(mockPlatformBoot).toHaveBeenCalledWith(expect.objectContaining({ slug: "paperclip" }));
266
+ });
267
+
268
+ it("seeds system accounts after creating ledger", async () => {
269
+ const buildContainer = await loadBuildContainer();
270
+ const config = baseBootConfig();
271
+
272
+ await buildContainer(config);
273
+
274
+ expect(mockSeedSystemAccounts).toHaveBeenCalled();
275
+ });
276
+
277
+ it("core services are always present", async () => {
278
+ const buildContainer = await loadBuildContainer();
279
+ const config = baseBootConfig();
280
+
281
+ const container = await buildContainer(config);
282
+
283
+ expect(container.db).toBeDefined();
284
+ expect(container.pool).toBeDefined();
285
+ expect(container.productConfig).toBeDefined();
286
+ expect(container.creditLedger).toBeDefined();
287
+ expect(container.orgMemberRepo).toBeDefined();
288
+ expect(container.orgService).toBeDefined();
289
+ expect(container.userRoleRepo).toBeDefined();
290
+ });
291
+
292
+ it("returns null for all disabled features", async () => {
293
+ const buildContainer = await loadBuildContainer();
294
+ const config = baseBootConfig();
295
+
296
+ const container = await buildContainer(config);
297
+
298
+ expect(container.fleet).toBeNull();
299
+ expect(container.crypto).toBeNull();
300
+ expect(container.stripe).toBeNull();
301
+ expect(container.gateway).toBeNull();
302
+ expect(container.hotPool).toBeNull();
303
+ });
304
+
305
+ it("builds fleet services when feature is enabled", async () => {
306
+ const buildContainer = await loadBuildContainer();
307
+ const config = baseBootConfig({
308
+ features: { fleet: true, crypto: false, stripe: false, gateway: false, hotPool: false },
309
+ });
310
+
311
+ const container = await buildContainer(config);
312
+
313
+ expect(container.fleet).not.toBeNull();
314
+ expect(container.fleet?.manager).toBeDefined();
315
+ expect(container.fleet?.docker).toBeDefined();
316
+ expect(container.fleet?.proxy).toBeDefined();
317
+ expect(container.fleet?.profileStore).toBeDefined();
318
+ expect(container.fleet?.serviceKeyRepo).toBeDefined();
319
+ });
320
+
321
+ it("builds crypto services when feature is enabled", async () => {
322
+ const buildContainer = await loadBuildContainer();
323
+ const config = baseBootConfig({
324
+ features: { fleet: false, crypto: true, stripe: false, gateway: false, hotPool: false },
325
+ });
326
+
327
+ const container = await buildContainer(config);
328
+
329
+ expect(container.crypto).not.toBeNull();
330
+ expect(container.crypto?.chargeRepo).toBeDefined();
331
+ expect(container.crypto?.webhookSeenRepo).toBeDefined();
332
+ });
333
+
334
+ it("builds stripe services when feature is enabled and key provided", async () => {
335
+ const buildContainer = await loadBuildContainer();
336
+ const config = baseBootConfig({
337
+ features: { fleet: false, crypto: false, stripe: true, gateway: false, hotPool: false },
338
+ stripeSecretKey: "sk_test_123",
339
+ stripeWebhookSecret: "whsec_test_456",
340
+ });
341
+
342
+ const container = await buildContainer(config);
343
+
344
+ expect(container.stripe).not.toBeNull();
345
+ expect(container.stripe?.stripe).toBeDefined();
346
+ expect(container.stripe?.webhookSecret).toBe("whsec_test_456");
347
+ expect(container.stripe?.customerRepo).toBeDefined();
348
+ expect(container.stripe?.processor).toBeDefined();
349
+ });
350
+
351
+ it("returns null stripe when feature enabled but no secret key", async () => {
352
+ const buildContainer = await loadBuildContainer();
353
+ const config = baseBootConfig({
354
+ features: { fleet: false, crypto: false, stripe: true, gateway: false, hotPool: false },
355
+ // stripeSecretKey intentionally omitted
356
+ });
357
+
358
+ const container = await buildContainer(config);
359
+
360
+ expect(container.stripe).toBeNull();
361
+ });
362
+
363
+ it("builds gateway services when feature is enabled", async () => {
364
+ const buildContainer = await loadBuildContainer();
365
+ const config = baseBootConfig({
366
+ features: { fleet: false, crypto: false, stripe: false, gateway: true, hotPool: false },
367
+ });
368
+
369
+ const container = await buildContainer(config);
370
+
371
+ expect(container.gateway).not.toBeNull();
372
+ expect(container.gateway?.serviceKeyRepo).toBeDefined();
373
+ });
374
+
375
+ it("runs migrations before building services", async () => {
376
+ const buildContainer = await loadBuildContainer();
377
+ const config = baseBootConfig();
378
+
379
+ await buildContainer(config);
380
+
381
+ expect(mockMigrate).toHaveBeenCalled();
382
+ // Migration should be called before platformBoot
383
+ const migrateOrder = mockMigrate.mock.invocationCallOrder[0];
384
+ const bootOrder = mockPlatformBoot.mock.invocationCallOrder[0];
385
+ expect(migrateOrder).toBeLessThan(bootOrder);
386
+ });
387
+
388
+ it("constructs OrgService with orgRepo, memberRepo, db, and authUserRepo", async () => {
389
+ const buildContainer = await loadBuildContainer();
390
+ const config = baseBootConfig();
391
+ orgServiceConstructorCalls.length = 0;
392
+
393
+ await buildContainer(config);
394
+
395
+ expect(orgServiceConstructorCalls.length).toBeGreaterThan(0);
396
+ const [orgRepo, memberRepo, db, options] = orgServiceConstructorCalls[0];
397
+ expect(orgRepo).toBeInstanceOf(MockDrizzleOrgRepositoryClass);
398
+ expect(memberRepo).toBeInstanceOf(MockDrizzleOrgMemberRepositoryClass);
399
+ expect(db).toBe(mockDb);
400
+ expect(options).toEqual(expect.objectContaining({ userRepo: expect.any(MockBetterAuthUserRepositoryClass) }));
401
+ });
402
+ });
@@ -0,0 +1,207 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { BootConfig, FeatureFlags } from "../boot-config.js";
3
+ import type { CryptoServices, FleetServices, GatewayServices, HotPoolServices, StripeServices } from "../container.js";
4
+ import { createTestContainer } from "../test-container.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // PlatformContainer interface
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe("PlatformContainer", () => {
11
+ it("allows null feature services", () => {
12
+ const c = createTestContainer();
13
+ expect(c.fleet).toBeNull();
14
+ expect(c.crypto).toBeNull();
15
+ expect(c.stripe).toBeNull();
16
+ expect(c.gateway).toBeNull();
17
+ expect(c.hotPool).toBeNull();
18
+ });
19
+
20
+ it("requires core services to be present", () => {
21
+ const c = createTestContainer();
22
+ expect(c.db).toBeDefined();
23
+ expect(c.pool).toBeDefined();
24
+ expect(c.productConfig).toBeDefined();
25
+ expect(c.creditLedger).toBeDefined();
26
+ expect(c.orgMemberRepo).toBeDefined();
27
+ expect(c.orgService).toBeDefined();
28
+ expect(c.userRoleRepo).toBeDefined();
29
+ });
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // BootConfig shape
34
+ // ---------------------------------------------------------------------------
35
+
36
+ describe("BootConfig", () => {
37
+ it("requires slug and provisionSecret", () => {
38
+ const config: BootConfig = {
39
+ slug: "test-product",
40
+ databaseUrl: "postgres://localhost/test",
41
+ provisionSecret: "s3cret",
42
+ features: {
43
+ fleet: false,
44
+ crypto: false,
45
+ stripe: false,
46
+ gateway: false,
47
+ hotPool: false,
48
+ },
49
+ };
50
+
51
+ expect(config.slug).toBe("test-product");
52
+ expect(config.provisionSecret).toBe("s3cret");
53
+ });
54
+
55
+ it("accepts optional fields", () => {
56
+ const config: BootConfig = {
57
+ slug: "full",
58
+ databaseUrl: "postgres://localhost/full",
59
+ provisionSecret: "secret",
60
+ host: "127.0.0.1",
61
+ port: 4000,
62
+ features: {
63
+ fleet: true,
64
+ crypto: true,
65
+ stripe: true,
66
+ gateway: true,
67
+ hotPool: true,
68
+ },
69
+ stripeSecretKey: "sk_test_xxx",
70
+ stripeWebhookSecret: "whsec_xxx",
71
+ cryptoServiceKey: "csk_xxx",
72
+ routes: [],
73
+ };
74
+
75
+ expect(config.host).toBe("127.0.0.1");
76
+ expect(config.port).toBe(4000);
77
+ expect(config.features.fleet).toBe(true);
78
+ });
79
+
80
+ it("FeatureFlags has all five toggles", () => {
81
+ const flags: FeatureFlags = {
82
+ fleet: true,
83
+ crypto: false,
84
+ stripe: true,
85
+ gateway: false,
86
+ hotPool: true,
87
+ };
88
+
89
+ expect(Object.keys(flags)).toHaveLength(5);
90
+ });
91
+ });
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // createTestContainer
95
+ // ---------------------------------------------------------------------------
96
+
97
+ describe("createTestContainer", () => {
98
+ it("returns valid defaults", () => {
99
+ const c = createTestContainer();
100
+
101
+ expect(c.fleet).toBeNull();
102
+ expect(c.crypto).toBeNull();
103
+ expect(c.stripe).toBeNull();
104
+ expect(c.gateway).toBeNull();
105
+ expect(c.hotPool).toBeNull();
106
+ expect(c.productConfig.product).toBeDefined();
107
+ });
108
+
109
+ it("allows overrides for core services", () => {
110
+ const customConfig = {
111
+ product: { slug: "custom", name: "Custom" } as never,
112
+ navItems: [],
113
+ domains: [],
114
+ features: null,
115
+ fleet: null,
116
+ billing: null,
117
+ };
118
+
119
+ const c = createTestContainer({ productConfig: customConfig });
120
+ expect(c.productConfig.product).toEqual({ slug: "custom", name: "Custom" });
121
+ });
122
+
123
+ it("stub ledger methods return sensible defaults", async () => {
124
+ const c = createTestContainer();
125
+
126
+ expect(await c.creditLedger.balance("t1")).toBe(0);
127
+ expect(await c.creditLedger.hasReferenceId("ref")).toBe(false);
128
+ expect(await c.creditLedger.history("t1")).toEqual([]);
129
+ expect(await c.creditLedger.tenantsWithBalance()).toEqual([]);
130
+ expect(await c.creditLedger.lifetimeSpendBatch([])).toEqual(new Map());
131
+ });
132
+
133
+ it("stub orgMemberRepo methods return sensible defaults", async () => {
134
+ const c = createTestContainer();
135
+
136
+ expect(await c.orgMemberRepo.findMember("org1", "u1")).toBeNull();
137
+ expect(await c.orgMemberRepo.listMembers("org1")).toEqual([]);
138
+ expect(await c.orgMemberRepo.countAdminsAndOwners("org1")).toBe(0);
139
+ });
140
+
141
+ it("stub userRoleRepo methods return sensible defaults", async () => {
142
+ const c = createTestContainer();
143
+
144
+ expect(await c.userRoleRepo.getTenantIdByUserId("u1")).toBeNull();
145
+ expect(await c.userRoleRepo.isPlatformAdmin("u1")).toBe(false);
146
+ expect(await c.userRoleRepo.listRolesByUser("u1")).toEqual([]);
147
+ });
148
+
149
+ it("allows enabling feature services via overrides", () => {
150
+ const fleet: FleetServices = {
151
+ manager: {} as never,
152
+ docker: {} as never,
153
+ proxy: {} as never,
154
+ profileStore: {} as never,
155
+ serviceKeyRepo: {} as never,
156
+ };
157
+
158
+ const crypto: CryptoServices = {
159
+ chargeRepo: {} as never,
160
+ webhookSeenRepo: {} as never,
161
+ };
162
+
163
+ const stripe: StripeServices = {
164
+ stripe: {} as never,
165
+ webhookSecret: "whsec_test",
166
+ customerRepo: {} as never,
167
+ processor: {} as never,
168
+ };
169
+
170
+ const gateway: GatewayServices = {
171
+ serviceKeyRepo: {} as never,
172
+ };
173
+
174
+ const hotPool: HotPoolServices = {
175
+ start: async () => ({ stop: () => {} }),
176
+ claim: async () => null,
177
+ getPoolSize: async () => 2,
178
+ setPoolSize: async () => {},
179
+ };
180
+
181
+ const c = createTestContainer({ fleet, crypto, stripe, gateway, hotPool });
182
+
183
+ expect(c.fleet).not.toBeNull();
184
+ expect(c.crypto).not.toBeNull();
185
+ expect(c.stripe).not.toBeNull();
186
+ expect(c.stripe?.webhookSecret).toBe("whsec_test");
187
+ expect(c.gateway).not.toBeNull();
188
+ expect(c.hotPool).not.toBeNull();
189
+ });
190
+
191
+ it("overrides merge without affecting other defaults", () => {
192
+ const c = createTestContainer({ gateway: { serviceKeyRepo: {} as never } });
193
+
194
+ // Overridden field
195
+ expect(c.gateway).not.toBeNull();
196
+
197
+ // Other feature services remain null
198
+ expect(c.fleet).toBeNull();
199
+ expect(c.crypto).toBeNull();
200
+ expect(c.stripe).toBeNull();
201
+ expect(c.hotPool).toBeNull();
202
+
203
+ // Core services still present
204
+ expect(c.creditLedger).toBeDefined();
205
+ expect(c.orgMemberRepo).toBeDefined();
206
+ });
207
+ });