@cosmicdrift/kumiko-testing 0.374.0 → 0.378.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Test template for Kumiko apps. Test-only: never import it from production code.
4
4
 
5
5
  ```ts
6
- import { seedTenant, setupAppTestStack } from "@cosmicdrift/kumiko-testing";
6
+ import { seedTenant, setupAppTestStack } from "@cosmicdrift/kumiko-dev-server/testing";
7
7
 
8
8
  const stack = await setupAppTestStack([myFeature]);
9
9
  const tenant = await seedTenant(stack, { users: 2 }); // light: no rows, own id/key/users
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-testing",
3
- "version": "0.374.0",
3
+ "version": "0.378.0",
4
4
  "description": "Test template for Kumiko apps: per-flow seedTenant, app test stack, canonical test preloads, bunfig generator and integration runner.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -26,6 +26,10 @@
26
26
  "types": "./src/stack/index.ts",
27
27
  "default": "./src/stack/index.ts"
28
28
  },
29
+ "./provider-env-keys": {
30
+ "types": "./src/provider-env-keys.ts",
31
+ "default": "./src/provider-env-keys.ts"
32
+ },
29
33
  "./helpers": {
30
34
  "types": "./src/helpers/index.ts",
31
35
  "default": "./src/helpers/index.ts"
@@ -95,25 +99,18 @@
95
99
  "kumiko-testing": "./bin/kumiko-testing.ts"
96
100
  },
97
101
  "dependencies": {
98
- "@cosmicdrift/kumiko-framework": "0.374.0",
99
- "@cosmicdrift/kumiko-types": "0.374.0",
102
+ "@cosmicdrift/kumiko-framework": "0.378.0",
103
+ "@cosmicdrift/kumiko-guards": "0.378.0",
104
+ "@cosmicdrift/kumiko-types": "0.378.0",
100
105
  "zod": "^4.4.3"
101
106
  },
102
107
  "peerDependencies": {
103
- "@cosmicdrift/kumiko-bundled-features": ">=0.366.0 <1.0.0",
104
- "@cosmicdrift/kumiko-dev-server": ">=0.366.0 <1.0.0",
105
108
  "@happy-dom/global-registrator": "^20.11.2",
106
109
  "@playwright/test": "^1.62.1",
107
110
  "@testing-library/dom": "^10.4.1",
108
111
  "@testing-library/react": "^16.3.2"
109
112
  },
110
113
  "peerDependenciesMeta": {
111
- "@cosmicdrift/kumiko-bundled-features": {
112
- "optional": true
113
- },
114
- "@cosmicdrift/kumiko-dev-server": {
115
- "optional": true
116
- },
117
114
  "@happy-dom/global-registrator": {
118
115
  "optional": true
119
116
  },
@@ -128,8 +125,8 @@
128
125
  }
129
126
  },
130
127
  "devDependencies": {
131
- "@cosmicdrift/kumiko-bundled-features": "0.374.0",
132
- "@cosmicdrift/kumiko-dev-server": "0.374.0",
128
+ "@cosmicdrift/kumiko-bundled-features": "0.378.0",
129
+ "@cosmicdrift/kumiko-dev-server": "0.378.0",
133
130
  "@happy-dom/global-registrator": "^20.11.2",
134
131
  "@playwright/test": "^1.62.1",
135
132
  "@testing-library/dom": "^10.4.1",
@@ -0,0 +1,30 @@
1
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+
3
+ // kumiko-testing does not import bundled-features or dev-server; the pieces of them it needs are
4
+ // passed in. kumiko-dev-server/testing binds these once, so apps import the ready-made functions.
5
+
6
+ // Structural match for bundled-features' EmailMessage.
7
+ export type InboxMessage = {
8
+ readonly to: string;
9
+ readonly subject: string;
10
+ readonly html: string;
11
+ readonly text?: string;
12
+ readonly from?: string;
13
+ readonly fromName?: string;
14
+ readonly replyTo?: string;
15
+ readonly headers?: Readonly<Record<string, string>>;
16
+ };
17
+
18
+ export type SeedBundledApi = {
19
+ readonly hashPassword: (password: string) => Promise<string>;
20
+ readonly UserHandlers: { readonly create: string; readonly update: string };
21
+ readonly TenantHandlers: { readonly create: string; readonly addMember: string };
22
+ };
23
+
24
+ export type SeedRouteBundledApi = SeedBundledApi & {
25
+ readonly tenantMeQuery: string;
26
+ readonly mailInbox: {
27
+ readonly featureName: string;
28
+ readonly read: (tenantId: TenantId) => readonly InboxMessage[];
29
+ };
30
+ };
package/src/changes.json CHANGED
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "0.376.0",
4
+ "type": "fix",
5
+ "title": "MEILI_URL in the test preload follows the app's Meilisearch port",
6
+ "detail": "service-env-defaults derives MEILI_URL from ports.infra.meilisearch of the app's kumiko.config.ts when the variable is unset; without a configured port the old default stays."
7
+ },
8
+ {
9
+ "version": "0.376.0",
10
+ "type": "breaking",
11
+ "title": "Test stack and seed helpers moved to @cosmicdrift/kumiko-dev-server/testing",
12
+ "detail": "setupAppTestStack, SetupAppTestStackOptions, seedTenant, persistTenantRows and createE2eSeedRoutes are no longer exported from @cosmicdrift/kumiko-testing; the bundled-features and dev-server optional peers are gone.",
13
+ "migration": "Import the five names from @cosmicdrift/kumiko-dev-server/testing and add @cosmicdrift/kumiko-dev-server to your dependencies; run bun kumiko-upgrade --apply to rewrite the imports. Namespace imports are listed as manual.",
14
+ "codemod": "scripts/codemod/testing-injection.ts"
15
+ },
2
16
  {
3
17
  "version": "0.372.0",
4
18
  "type": "improvement",
@@ -1,11 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
- import { AuthMfaHandlers, base32Decode } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
3
- import { currentTotpCode } from "@cosmicdrift/kumiko-bundled-features/auth-mfa/testing";
4
2
  import type { WriteErrorInfo } from "@cosmicdrift/kumiko-framework/errors";
5
3
  import { type APIRequestContext, type APIResponse, expect, type Page } from "@playwright/test";
6
4
  import * as z from "zod";
7
5
  import type { BoundApi } from "../seed-types";
8
- import { CSRF_COOKIE_NAMES, CSRF_HEADER_NAME } from "./constants";
6
+ import {
7
+ AUTH_MFA_ENABLE_CONFIRM_QN,
8
+ AUTH_MFA_ENABLE_START_QN,
9
+ CSRF_COOKIE_NAMES,
10
+ CSRF_HEADER_NAME,
11
+ } from "./constants";
12
+ import { base32Decode, currentTotpCode } from "./totp";
9
13
 
10
14
  export type LoginCredentials = {
11
15
  readonly email: string;
@@ -238,10 +242,10 @@ export async function enrollTotpViaApi(
238
242
  }
239
243
  const started = await apiWrite<{ setupToken: string; totpSecret: string }>(
240
244
  request,
241
- AuthMfaHandlers.enableStart,
245
+ AUTH_MFA_ENABLE_START_QN,
242
246
  {},
243
247
  );
244
- await apiWrite(request, AuthMfaHandlers.enableConfirm, {
248
+ await apiWrite(request, AUTH_MFA_ENABLE_CONFIRM_QN, {
245
249
  setupToken: started.setupToken,
246
250
  code: await totpCode(started.totpSecret),
247
251
  });
@@ -42,6 +42,11 @@ export const PROD_BUNDLES_ENV = "KUMIKO_DEV_PROD_BUNDLES";
42
42
  // duplicated for the same Node/Bun-toolchain reason.
43
43
  export const TRUSTED_PROXY_HOPS_ENV = "KUMIKO_TRUSTED_PROXY_HOPS";
44
44
 
45
+ // Same literals as kumiko-bundled-features' AuthMfaHandlers (enableStart, enableConfirm),
46
+ // duplicated so the e2e kit has no bundled-features import; e2e-mfa-parity.test.ts pins them.
47
+ export const AUTH_MFA_ENABLE_START_QN = "auth-mfa:write:enable-start";
48
+ export const AUTH_MFA_ENABLE_CONFIRM_QN = "auth-mfa:write:enable-confirm";
49
+
45
50
  export const SEED_ROUTE_PREFIX = "/__test";
46
51
 
47
52
  export const SEED_ROUTES = {
@@ -21,7 +21,7 @@ export const DEFAULT_REDUCED_MOTION = "reduce" as const;
21
21
 
22
22
  const MIN_BYTES = 5 * 1024;
23
23
 
24
- // The two themes every sample app renders out of the box: renderer-web's
24
+ // The two themes every sample app renders out of the box: renderer/web's
25
25
  // light default and its `.dark` variant. Apps with extra themes (styleguide's
26
26
  // brand-token override) pass their own pair to runMatrix.
27
27
  export const DEFAULT_THEMES = ["default-light", "default-dark"] as const;
@@ -1,10 +1,4 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
- import type { EmailMessage } from "@cosmicdrift/kumiko-bundled-features/channel-email";
3
- import {
4
- createMailTransportInMemoryFeature,
5
- getInbox,
6
- } from "@cosmicdrift/kumiko-bundled-features/mail-transport-inmemory";
7
- import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
8
2
  import {
9
3
  type ExtraRouteDefinition,
10
4
  ExtraRouteRejection,
@@ -14,21 +8,22 @@ import {
14
8
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
15
9
  import { isSeedModeEnabled } from "@cosmicdrift/kumiko-framework/event-store";
16
10
  import * as z from "zod";
11
+ import type { InboxMessage, SeedRouteBundledApi } from "../bundled-injection.ts";
17
12
  import {
18
13
  persistTenantRows,
19
14
  persistUserRows,
20
15
  type SeedWriter,
21
16
  unwrapSavedRow,
22
17
  unwrapWriteData,
23
- } from "../seed-tenant";
24
- import { SEED_ROUTES, SEED_TOKEN_ENV, SEED_TOKEN_HEADER } from "./constants";
18
+ } from "../seed-tenant.ts";
19
+ import { SEED_ROUTES, SEED_TOKEN_ENV, SEED_TOKEN_HEADER } from "./constants.ts";
25
20
  import {
26
21
  type CapturedMail,
27
22
  createSeedUserRequestSchema,
28
23
  extraSeedRequestSchema,
29
24
  inboxQuerySchema,
30
25
  seedTenantRequestSchema,
31
- } from "./seed-contract";
26
+ } from "./seed-contract.ts";
32
27
 
33
28
  type ParsedBody<T> =
34
29
  | { readonly success: true; readonly data: T }
@@ -137,11 +132,11 @@ export type E2eSeedRoutesOptions = {
137
132
  // Apps that send tenantless mail (signup/forgot-password/magic-link) via
138
133
  // their own raw createInMemoryTransport() pass its `sent` array here so
139
134
  // the inbox route can read it without a tenantId. Independent of
140
- // createMailTransportInMemoryFeature() — both sources may be present at once.
141
- readonly mailOutbox?: { readonly sent: readonly EmailMessage[] };
135
+ // the in-memory mail transport feature — both sources may be present at once.
136
+ readonly mailOutbox?: { readonly sent: readonly InboxMessage[] };
142
137
  };
143
138
 
144
- function toCapturedMail(message: EmailMessage): CapturedMail {
139
+ function toCapturedMail(message: InboxMessage): CapturedMail {
145
140
  return {
146
141
  to: message.to,
147
142
  subject: message.subject,
@@ -162,7 +157,8 @@ function toCapturedMail(message: EmailMessage): CapturedMail {
162
157
  // (kumiko-framework#3120). Each route's own assertGateOpen() 404s outside
163
158
  // the seed condition regardless — this is belt-and-suspenders, not the only gate.
164
159
  export function createE2eSeedRoutes(
165
- options: E2eSeedRoutesOptions = {},
160
+ options: E2eSeedRoutesOptions,
161
+ bundled: SeedRouteBundledApi,
166
162
  ): readonly ExtraRouteDefinition[] {
167
163
  const seedUserRequestSchema = createSeedUserRequestSchema(options.extraRoles);
168
164
  const extraSeeders = options.extraSeeders ?? {};
@@ -183,11 +179,11 @@ export function createE2eSeedRoutes(
183
179
  const write: SeedWriter = async (handlerQn, payload, tenantId) =>
184
180
  unwrapSavedRow(handlerQn, await deps.dispatchSystemWrite({ handlerQn, payload, tenantId }));
185
181
  try {
186
- const persisted = await persistTenantRows(write, {
187
- name: verified.name,
188
- users: verified.members,
189
- admin: verified.admin,
190
- });
182
+ const persisted = await persistTenantRows(
183
+ write,
184
+ { name: verified.name, users: verified.members, admin: verified.admin },
185
+ bundled,
186
+ );
191
187
  seededTenantIds.add(persisted.id);
192
188
  return c.json(persisted);
193
189
  } catch (error) {
@@ -209,7 +205,7 @@ export function createE2eSeedRoutes(
209
205
  handler: async (c, verified, deps) => {
210
206
  // seededTenantIds is never pruned, so a tenant removed after seeding must still 404.
211
207
  const tenantRow = await deps.dispatchSystemQuery({
212
- handlerQn: TenantQueries.me,
208
+ handlerQn: bundled.tenantMeQuery,
213
209
  payload: {},
214
210
  tenantId: verified.tenantId,
215
211
  });
@@ -220,10 +216,13 @@ export function createE2eSeedRoutes(
220
216
  unwrapSavedRow(handlerQn, await deps.dispatchSystemWrite({ handlerQn, payload, tenantId }));
221
217
  try {
222
218
  return c.json(
223
- await persistUserRows(write, verified.tenantId, verified.roles, {
224
- displayName: verified.displayName,
225
- email: verified.email,
226
- }),
219
+ await persistUserRows(
220
+ write,
221
+ verified.tenantId,
222
+ verified.roles,
223
+ { displayName: verified.displayName, email: verified.email },
224
+ bundled,
225
+ ),
227
226
  );
228
227
  } catch (error) {
229
228
  return c.json({ error: error instanceof Error ? error.message : "seed failed" }, 500);
@@ -252,13 +251,12 @@ export function createE2eSeedRoutes(
252
251
  const tenantId = verified.tenantId;
253
252
  const mailOutbox = options.mailOutbox;
254
253
  const hasTenantSource =
255
- tenantId !== undefined &&
256
- deps.registry.features.has(createMailTransportInMemoryFeature().name);
254
+ tenantId !== undefined && deps.registry.features.has(bundled.mailInbox.featureName);
257
255
  if (!hasTenantSource && mailOutbox === undefined) {
258
256
  return c.json(
259
257
  {
260
258
  error:
261
- `no inbox to read: neither ${createMailTransportInMemoryFeature().name} is mounted with a ` +
259
+ `no inbox to read: neither ${bundled.mailInbox.featureName} is mounted with a ` +
262
260
  "tenantId nor was mailOutbox passed to createE2eSeedRoutes()",
263
261
  },
264
262
  501,
@@ -270,7 +268,7 @@ export function createE2eSeedRoutes(
270
268
  // first — that ordering is what mailCapture and its tests rely on.
271
269
  const tenantMessages =
272
270
  hasTenantSource && tenantId !== undefined
273
- ? [...getInbox(tenantId)].filter(isRecipient).reverse().map(toCapturedMail)
271
+ ? [...bundled.mailInbox.read(tenantId)].filter(isRecipient).reverse().map(toCapturedMail)
274
272
  : [];
275
273
  const outboxMessages =
276
274
  mailOutbox !== undefined
@@ -0,0 +1,41 @@
1
+ // RFC 6238 TOTP (HMAC-SHA1, 30 s step, 6 digits) with RFC 4648 base32 secrets. Duplicated from
2
+ // kumiko-bundled-features' auth-mfa because the e2e kit must not depend on bundled-features;
3
+ // __tests__/e2e-mfa-parity.test.ts pins both copies to the same output.
4
+ import { createHmac } from "node:crypto";
5
+
6
+ const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
7
+ const STEP_SECONDS = 30;
8
+ const DIGITS = 6;
9
+
10
+ export function base32Decode(encoded: string): Buffer {
11
+ const clean = encoded.trim().toUpperCase().replace(/\s+/g, "").replace(/=+$/, "");
12
+ let bits = 0;
13
+ let value = 0;
14
+ const bytes: number[] = [];
15
+ for (const char of clean) {
16
+ const index = BASE32_ALPHABET.indexOf(char);
17
+ if (index === -1) throw new Error(`base32Decode: invalid character "${char}"`);
18
+ value = (value << 5) | index;
19
+ bits += 5;
20
+ if (bits >= 8) {
21
+ bytes.push((value >>> (bits - 8)) & 0xff);
22
+ bits -= 8;
23
+ }
24
+ }
25
+ return Buffer.from(bytes);
26
+ }
27
+
28
+ export function currentTotpCode(secret: Buffer, nowMs: number = Date.now()): string {
29
+ const counter = Math.floor(Math.floor(nowMs / 1000) / STEP_SECONDS);
30
+ const counterBuffer = Buffer.alloc(8);
31
+ counterBuffer.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
32
+ counterBuffer.writeUInt32BE(counter % 2 ** 32, 4);
33
+ const hmac = createHmac("sha1", secret).update(counterBuffer).digest();
34
+ const offset = (hmac[hmac.length - 1] ?? 0) & 0x0f;
35
+ const truncated =
36
+ (((hmac[offset] ?? 0) & 0x7f) << 24) |
37
+ (((hmac[offset + 1] ?? 0) & 0xff) << 16) |
38
+ (((hmac[offset + 2] ?? 0) & 0xff) << 8) |
39
+ ((hmac[offset + 3] ?? 0) & 0xff);
40
+ return String(truncated % 10 ** DIGITS).padStart(DIGITS, "0");
41
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,10 @@
1
- export { PROVIDER_ENV_KEYS } from "./provider-env-keys";
1
+ export { PROVIDER_ENV_KEYS } from "@cosmicdrift/kumiko-testing/provider-env-keys";
2
+ export type { InboxMessage, SeedBundledApi, SeedRouteBundledApi } from "./bundled-injection.ts";
2
3
  export {
3
4
  type BoundApi,
4
5
  type InProcessSeededTenant,
5
6
  type PersistedTenant,
6
- persistTenantRows,
7
+ persistTenantRows as persistTenantRowsWith,
7
8
  type SavedRow,
8
9
  type SeededCredentials,
9
10
  type SeededTenant,
@@ -11,9 +12,5 @@ export {
11
12
  type SeedPart,
12
13
  type SeedTenantOptions,
13
14
  type SeedWriter,
14
- seedTenant,
15
- } from "./seed-tenant";
16
- export {
17
- type SetupAppTestStackOptions,
18
- setupAppTestStack,
19
- } from "./setup-app-test-stack";
15
+ seedTenant as seedTenantWith,
16
+ } from "./seed-tenant.ts";
@@ -0,0 +1,13 @@
1
+ import { join } from "node:path";
2
+ import { APP_CONFIG_FILE } from "@cosmicdrift/kumiko-framework/config";
3
+
4
+ /** `ports.infra.meilisearch` of the app's kumiko.config.ts, read statically; undefined when not declared. */
5
+ export async function readAppMeilisearchPort(appRoot: string): Promise<number | undefined> {
6
+ if (!(await Bun.file(join(appRoot, APP_CONFIG_FILE)).exists())) return undefined;
7
+ // ts-morph loads only for apps that have a config file and no MEILI_URL.
8
+ const { readStaticAppConfig } = await import(
9
+ "@cosmicdrift/kumiko-guards/repo-manifest/app-ports"
10
+ );
11
+ const config = readStaticAppConfig(appRoot);
12
+ return config.kind === "defined" ? config.ports.infra?.meilisearch : undefined;
13
+ }
@@ -1,5 +1,14 @@
1
+ import { readAppMeilisearchPort } from "./app-meilisearch-port";
1
2
  import { SERVICE_ENV_DEFAULTS } from "./service-env-defaults-values";
2
3
 
3
- for (const [key, value] of Object.entries(SERVICE_ENV_DEFAULTS)) {
4
+ const meilisearchPort =
5
+ process.env["MEILI_URL"] === undefined ? await readAppMeilisearchPort(process.cwd()) : undefined;
6
+
7
+ const defaults: Record<string, string> = {
8
+ ...SERVICE_ENV_DEFAULTS,
9
+ ...(meilisearchPort === undefined ? {} : { MEILI_URL: `http://localhost:${meilisearchPort}` }),
10
+ };
11
+
12
+ for (const [key, value] of Object.entries(defaults)) {
4
13
  process.env[key] ??= value;
5
14
  }
package/src/scaffold.ts CHANGED
@@ -61,7 +61,7 @@ function renderE2eServer(appName: string): string {
61
61
  TEST_RUNTIME_DIRECTIVE,
62
62
  'import { randomUUID } from "node:crypto";',
63
63
  'import { runDevApp } from "@cosmicdrift/kumiko-dev-server";',
64
- 'import { createE2eSeedRoutes } from "@cosmicdrift/kumiko-testing/e2e/seed-route";',
64
+ 'import { createE2eSeedRoutes } from "@cosmicdrift/kumiko-dev-server/testing";',
65
65
  'import { APP_FEATURES } from "../src/run-config";',
66
66
  "",
67
67
  "await runDevApp({",
@@ -131,7 +131,7 @@ function renderE2eHelpers(): string {
131
131
 
132
132
  function renderIntegrationSetup(): string {
133
133
  return [
134
- 'import { setupAppTestStack } from "@cosmicdrift/kumiko-testing";',
134
+ 'import { setupAppTestStack } from "@cosmicdrift/kumiko-dev-server/testing";',
135
135
  'import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";',
136
136
  'import { tasksFeature } from "../features/tasks";',
137
137
  "",
@@ -165,7 +165,7 @@ function renderUnitTest(): string {
165
165
  function renderIntegrationTest(): string {
166
166
  return [
167
167
  'import { afterAll, beforeAll, describe, expect, test } from "bun:test";',
168
- 'import { seedTenant } from "@cosmicdrift/kumiko-testing";',
168
+ 'import { seedTenant } from "@cosmicdrift/kumiko-dev-server/testing";',
169
169
  'import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";',
170
170
  'import { setupTasksStack } from "./_setup";',
171
171
  "",
@@ -1,7 +1,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { hashPassword } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
3
- import { TenantHandlers } from "@cosmicdrift/kumiko-bundled-features/tenant";
4
- import { UserHandlers } from "@cosmicdrift/kumiko-bundled-features/user";
5
2
  import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
6
3
  import {
7
4
  createSystemUser,
@@ -11,6 +8,8 @@ import {
11
8
  type WriteResult,
12
9
  } from "@cosmicdrift/kumiko-framework/engine";
13
10
  import { isPlainObject } from "@cosmicdrift/kumiko-framework/utils";
11
+ import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
12
+ import type { SeedBundledApi } from "./bundled-injection.ts";
14
13
  import {
15
14
  type BoundApi,
16
15
  type SeedAdminIdentity,
@@ -19,8 +18,7 @@ import {
19
18
  type SeedTenantOptions,
20
19
  splitSeedRoles,
21
20
  withSession,
22
- } from "./seed-types";
23
- import type { TestStack } from "./stack/index.ts";
21
+ } from "./seed-types.ts";
24
22
 
25
23
  export type {
26
24
  BoundApi,
@@ -30,7 +28,7 @@ export type {
30
28
  SeededUser,
31
29
  SeedPart,
32
30
  SeedTenantOptions,
33
- } from "./seed-types";
31
+ } from "./seed-types.ts";
34
32
 
35
33
  export type SavedRow = { readonly id: string; readonly data: { readonly version: number } };
36
34
 
@@ -114,8 +112,10 @@ export async function persistUserRows(
114
112
  write: SeedWriter,
115
113
  tenantId: TenantId,
116
114
  roles: readonly string[],
117
- identity: SeedAdminIdentity = {},
115
+ identity: SeedAdminIdentity,
116
+ bundled: SeedBundledApi,
118
117
  ): Promise<SeededCredentials> {
118
+ const { hashPassword, UserHandlers, TenantHandlers } = bundled;
119
119
  const light = lightCredentials(
120
120
  identity.email !== undefined ? substituteTenantId(identity.email, tenantId) : undefined,
121
121
  );
@@ -149,14 +149,16 @@ export async function persistTenantRows(
149
149
  readonly name?: string;
150
150
  readonly users?: number;
151
151
  readonly admin?: SeedAdminIdentity;
152
- } = {},
152
+ },
153
+ bundled: SeedBundledApi,
153
154
  ): Promise<PersistedTenant> {
155
+ const { TenantHandlers } = bundled;
154
156
  const { id, key, name } = newTenantIdentity(opts.name);
155
157
  await write(TenantHandlers.create, { id, key, name }, id);
156
- const admin = await persistUserRows(write, id, [ROLES.TenantAdmin], opts.admin);
158
+ const admin = await persistUserRows(write, id, [ROLES.TenantAdmin], opts.admin ?? {}, bundled);
157
159
  const members: SeededCredentials[] = [];
158
160
  for (let i = 0; i < (opts.users ?? 0); i++) {
159
- members.push(await persistUserRows(write, id, [ROLES.Member]));
161
+ members.push(await persistUserRows(write, id, [ROLES.Member], {}, bundled));
160
162
  }
161
163
  return { id, key, name, admin, members };
162
164
  }
@@ -176,11 +178,12 @@ export type InProcessSeededTenant = SeededTenant & {
176
178
 
177
179
  export async function seedTenant(
178
180
  stack: TestStack,
179
- opts: SeedTenantOptions = {},
181
+ opts: SeedTenantOptions,
182
+ bundled: SeedBundledApi,
180
183
  ): Promise<InProcessSeededTenant> {
181
184
  const write = stackSeedWriter(stack);
182
185
  const persist = opts.persist === true;
183
- const rows = persist ? await persistTenantRows(write, opts) : lightTenantRows(opts);
186
+ const rows = persist ? await persistTenantRows(write, opts, bundled) : lightTenantRows(opts);
184
187
  const { id } = rows;
185
188
 
186
189
  // setupTestStack subscribes events.sse only to test tenant 1; a seeded tenant has a fresh random id.
@@ -201,7 +204,7 @@ export async function seedTenant(
201
204
  addUser: async (roles = [ROLES.Member], identity = {}) =>
202
205
  withSession(
203
206
  persist
204
- ? await persistUserRows(write, id, roles, identity)
207
+ ? await persistUserRows(write, id, roles, identity, bundled)
205
208
  : lightCredentials(
206
209
  identity.email !== undefined ? substituteTenantId(identity.email, id) : undefined,
207
210
  ),
@@ -1,30 +0,0 @@
1
- import {
2
- type SetupTestStackFromFeaturesOptions,
3
- setupTestStackFromFeatures,
4
- } from "@cosmicdrift/kumiko-dev-server/setup-test-stack-from-features";
5
- import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
6
- import { pushEntityProjectionTables, type TestStack } from "./stack/index.ts";
7
-
8
- export type SetupAppTestStackOptions = SetupTestStackFromFeaturesOptions & {
9
- readonly registryTables?: boolean;
10
- };
11
-
12
- export async function setupAppTestStack(
13
- features: readonly FeatureDefinition[],
14
- options: SetupAppTestStackOptions = {},
15
- ): Promise<TestStack> {
16
- const { registryTables = true, includeBundled = true, ...stackOptions } = options;
17
- const stack = await setupTestStackFromFeatures(features, {
18
- ...stackOptions,
19
- includeBundled,
20
- entityTables: registryTables,
21
- });
22
- if (!registryTables) return stack;
23
- try {
24
- await pushEntityProjectionTables(stack, stack.registry);
25
- } catch (error) {
26
- await stack.cleanup();
27
- throw error;
28
- }
29
- return stack;
30
- }