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