{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import express from 'express'\nimport {\n  Application,\n  Container,\n  buildPipeline,\n  runContributors,\n  type AppAdapter,\n  type AppModule,\n  type AppModuleClass,\n  type AppModuleEntry,\n  type ApplicationOptions,\n  type ContextDecorator,\n  type ContextMetaKey,\n  type ContributorRegistration,\n  type ExecutionContext,\n  MissingContextValueError,\n  type KickPlugin,\n  type MetaValue,\n  type ModuleRoutes,\n} from '@forinda/kickjs'\n\n/**\n * Bootstrap options forwarded verbatim to the underlying Application so\n * test apps exercise the same HTTP pipeline shape as production.\n */\ntype BootstrapPassthroughOptions = Pick<\n  ApplicationOptions,\n  | 'port'\n  | 'apiPrefix'\n  | 'defaultVersion'\n  | 'middlewares'\n  | 'onError'\n  | 'onNotFound'\n  | 'plugins'\n  | 'trustProxy'\n  | 'jsonLimit'\n  | 'security'\n  | 'contributors'\n  | 'contextStore'\n  // The engine under test. Without this every test ran Express while\n  // production ran Fastify or h3 — the one thing an integration test exists\n  // to rule out.\n  | 'runtime'\n  // Without this the option is accepted by the type and dropped on the way\n  // through, so `health: false` silently kept the endpoints mounted.\n  | 'health'\n>\n\n/**\n * Options for creating a test application.\n * Disables helmet, cors, compression, and morgan by default.\n */\nexport interface CreateTestAppOptions extends BootstrapPassthroughOptions {\n  /**\n   * Modules registered for the test run. Accepts both legacy class form\n   * (`UserModule extends AppModule`) and `defineModule` factory output\n   * (call the factory: `UserModule()`) — `Application` discriminates\n   * at boot.\n   */\n  modules: AppModuleEntry[]\n  /** Adapters to attach (auth, queue, devtools, etc.) */\n  adapters?: AppAdapter[]\n  /**\n   * DI overrides applied after module registration.\n   *\n   * An object literal covers string and symbol keys. It cannot cover\n   * `createToken()`, though — a token is a frozen OBJECT identified by\n   * reference, and TypeScript rejects an object as a computed key\n   * (`TS2464: A computed property name must be of type 'string', 'number',\n   * 'symbol', or 'any'`). Since tokens are the documented way to bind an\n   * interface to an implementation, that excluded exactly the key type most\n   * worth overriding in a test.\n   *\n   * Worse, the obvious workaround compiles and does nothing: `[TOKEN.name]` is\n   * a string, and the container keys tokens by reference, so the override is\n   * accepted and never applied.\n   *\n   * Pass entries to override a token:\n   *\n   * ```ts\n   * const DATABASE = createToken<Database>('app/Db/connection')\n   *\n   * await createTestApp({\n   *   modules: [UserModule()],\n   *   overrides: [[DATABASE, fakeDb()]],\n   * })\n   * ```\n   *\n   * A `Map` works too. The object form is unchanged.\n   */\n  overrides?:\n    | Record<symbol | string, any>\n    | ReadonlyArray<readonly [token: unknown, value: unknown]>\n    | ReadonlyMap<unknown, unknown>\n  /**\n   * Use an isolated container instead of the global singleton.\n   * Prevents concurrent tests from interfering with each other's DI state.\n   * When true, Container.create() is used instead of Container.reset() + getInstance().\n   */\n  isolated?: boolean\n}\n\n/**\n * Create an Application instance configured for testing.\n * Resets the DI container, registers modules, applies overrides,\n * and returns the Application, which drives whichever runtime is configured.\n *\n * Drive it through `app` — `app.handle` is the Application's own Node request\n * listener and follows whichever runtime is configured, so the same test runs\n * against the engine you actually deploy.\n *\n * @example\n * ```ts\n * const { app, container } = await createTestApp({\n *   modules: [UserModule],\n *   overrides: { [USER_REPO]: new InMemoryUserRepo() },\n * })\n * const res = await request(app.handle.bind(app)).get('/api/v1/users')\n * ```\n *\n * @example Test the engine you actually deploy\n * ```ts\n * import { fastifyRuntime } from '@forinda/kickjs/fastify'\n *\n * const { app } = await createTestApp({ modules: [UserModule], runtime: fastifyRuntime() })\n * const res = await request(app.handle.bind(app)).get('/api/v1/users')\n * ```\n */\n/**\n * Normalize every accepted `overrides` shape to `[token, value]` pairs.\n *\n * An array or Map keeps the token's identity, which is what the container\n * matches on; an object literal is limited to string and symbol keys by the\n * language itself.\n */\nfunction overrideEntries(\n  overrides: NonNullable<CreateTestAppOptions['overrides']>,\n): Array<[unknown, unknown]> {\n  if (Array.isArray(overrides)) return overrides.map(([token, value]) => [token, value])\n  if (overrides instanceof Map) return [...overrides.entries()]\n  const record = overrides as Record<symbol | string, unknown>\n  return Reflect.ownKeys(record).map((key) => [key, record[key as never]])\n}\n\nexport async function createTestApp(options: CreateTestAppOptions): Promise<{\n  app: Application\n  /**\n   * @deprecated Use `app.handle.bind(app)`. Only valid under the Express\n   * runtime — under\n   * any other engine this throws rather than handing back that engine's\n   * instance mistyped as `express.Express`, which is how a suite ends up\n   * silently exercising the wrong runtime.\n   */\n  expressApp: express.Express\n  container: Container\n}> {\n  let container: Container\n  if (options.isolated) {\n    // Isolated container — safe for concurrent tests\n    container = Container.create()\n  } else {\n    // Global singleton — reset for serial test isolation (default)\n    Container.reset()\n    container = Container.getInstance()\n  }\n\n  const app = new Application({\n    modules: options.modules,\n    adapters: options.adapters,\n    port: options.port,\n    apiPrefix: options.apiPrefix,\n    defaultVersion: options.defaultVersion,\n    // Left undefined so the Application applies its OWN defaults, including\n    // the shared body-parsing policy (#590). Naming a middleware list here\n    // put the Application on its user-declared branch, so a test app parsed\n    // only `application/json` while the same app in production also parsed\n    // `+json`, forms and text — the harness quietly tested a different\n    // pipeline. The Application already guards on\n    // `RuntimeCapabilities.nativeBodyParsing`, which is what kept a connect\n    // parser off Fastify and h3.\n    middlewares: options.middlewares,\n    onError: options.onError,\n    onNotFound: options.onNotFound,\n    plugins: options.plugins,\n    trustProxy: options.trustProxy,\n    jsonLimit: options.jsonLimit,\n    security: options.security,\n    contributors: options.contributors,\n    contextStore: options.contextStore,\n    // Without this the option is accepted and ignored, which is worse than\n    // not offering it: the suite reports Express while claiming Fastify.\n    runtime: options.runtime,\n    health: options.health,\n  })\n\n  // Run setup — mounts routes, registers modules, initializes adapters.\n  // Awaited to support future async adapter hooks.\n  await Promise.resolve(app.setup())\n\n  // When using an isolated container, Application.setup() registers modules\n  // on the global singleton (Container.getInstance()). Re-register them on\n  // the isolated container so that bindings are available there too.\n  if (options.isolated) {\n    for (const entry of options.modules) {\n      // Discriminate class vs instance — `defineModule` factories return\n      // AppModule instances, legacy classes need `new`. Same dispatch\n      // Application.setup() runs at boot.\n      const mod: AppModule = typeof entry === 'function' ? new (entry as AppModuleClass)() : entry\n      // register() is optional — see AppModule docs\n      mod.register?.(container)\n    }\n    container.bootstrap()\n  }\n\n  // Apply DI overrides AFTER setup so they take precedence over\n  // bindings registered by modules during register().\n  if (options.overrides) {\n    for (const [token, value] of overrideEntries(options.overrides)) {\n      container.registerInstance(token, value)\n    }\n  }\n\n  const runtimeName = app.getActiveRuntime().name\n\n  return {\n    app,\n    get expressApp(): express.Express {\n      if (runtimeName !== 'express') {\n        throw new Error(\n          `createTestApp: 'expressApp' is only available under the Express runtime — ` +\n            `this app is running '${runtimeName}'. Drive the app instead, which follows ` +\n            `whichever engine is configured: request(app.handle.bind(app)).get('/...').`,\n        )\n      }\n      return app.getExpressApp()\n    },\n    container,\n  }\n}\n\n/**\n * Build a quick TestModule that explicitly registers dependencies.\n * Useful for integration tests that need to control the DI graph.\n *\n * @example\n * ```ts\n * const TestModule = createTestModule({\n *   register: (c) => {\n *     c.registerFactory(USER_REPO, () => new InMemoryUserRepo())\n *     c.register(UserController)\n *   },\n *   routes: () => buildRoutes(UserController, '/users'),\n * })\n * ```\n */\nexport function createTestModule(config: {\n  register: (container: Container) => void\n  routes: () => ModuleRoutes | ModuleRoutes[] | null\n}): AppModuleClass {\n  class TestModule implements AppModule {\n    register(container: Container) {\n      config.register(container)\n    }\n    routes() {\n      return config.routes()\n    }\n  }\n  return TestModule\n}\n\n// ── Context Contributor unit-test helper (#107) ─────────────────────────\n\n/**\n * Options for {@link runContributor}.\n */\nexport interface RunContributorOptions {\n  /**\n   * Resolved deps passed to `resolve(ctx, deps)`. Skips the DI container\n   * entirely — bypass for unit tests that want to assert pure resolve()\n   * behaviour without standing up a container.\n   *\n   * Property names must match the spec's `deps` keys; values can be real\n   * service instances, mocks, or test doubles.\n   */\n  deps?: Record<string, unknown>\n\n  /**\n   * Pre-populate the fake context's metadata Map. Useful for testing\n   * contributors with `dependsOn` without running the full pipeline:\n   * pre-populate the dependency keys, then assert that `resolve()` reads\n   * them and produces the expected output.\n   */\n  initial?: Record<string, unknown>\n\n  /** Override the fake context's `requestId` (default: `'test-req'`). */\n  requestId?: string\n}\n\n/**\n * Result of {@link runContributor}.\n */\nexport interface RunContributorResult<K extends string> {\n  /** Value returned by the contributor's `resolve()` call. */\n  value: MetaValue<K>\n  /** The fake `ExecutionContext` used during the run. */\n  ctx: ExecutionContext\n  /**\n   * Final state of the fake context's metadata Map after `resolve()`.\n   * Includes any `ctx.set(...)` calls the resolver made plus the final\n   * resolved value under the contributor's own key.\n   */\n  meta: Map<string, unknown>\n}\n\n/**\n * Run a single context contributor in isolation against a fake\n * {@link ExecutionContext}. Skips the container, the topo-sort, and the\n * §20.9 error matrix — calls `decorator.registration.resolve(ctx, deps)`\n * directly so unit tests can assert pure resolve behaviour.\n *\n * Errors thrown by `resolve()` propagate so tests can `expect(...).rejects`\n * or `await expect(...).rejects.toThrow()` against them. To exercise the\n * full error matrix (optional skip, onError replacement, etc.), build a\n * one-element pipeline with `buildPipeline()` and use `runContributors()`\n * directly.\n *\n * @example\n * ```ts\n * const LoadProject = defineContextDecorator({\n *   key: 'project',\n *   dependsOn: ['tenant'],\n *   deps: { repo: ProjectRepo },\n *   resolve: (ctx, { repo }) => repo.findByTenant(ctx.get('tenant')!.id),\n * })\n *\n * const { value } = await runContributor(LoadProject, {\n *   initial: { tenant: { id: 't-1' } },\n *   deps: { repo: new InMemoryProjectRepo([{ id: 'p-1', tenantId: 't-1' }]) },\n * })\n * expect(value).toEqual({ id: 'p-1', tenantId: 't-1' })\n * ```\n */\nexport async function runContributor<\n  K extends ContextMetaKey,\n  D extends Record<string, any> = Record<string, never>,\n>(\n  decorator: ContextDecorator<K, D>,\n  options: RunContributorOptions = {},\n): Promise<RunContributorResult<K>> {\n  const meta = new Map<string, unknown>(Object.entries(options.initial ?? {}))\n  const ctx: ExecutionContext = {\n    get(key) {\n      return meta.get(key) as never\n    },\n    require(key) {\n      // Same contract as RequestContext.require — a resolver under test\n      // that depends on an upstream key should fail here exactly the way\n      // it would in production, not read `undefined`.\n      const value = meta.get(key)\n      if (value === undefined) throw new MissingContextValueError(key)\n      return value as never\n    },\n    set(key, value) {\n      meta.set(key, value)\n    },\n    requestId: options.requestId ?? 'test-req',\n  }\n\n  const value = await decorator.registration.resolve(ctx, (options.deps ?? {}) as never)\n  meta.set(decorator.registration.key, value)\n\n  return { value: value as MetaValue<K>, ctx, meta }\n}\n\n// ── Plugin unit-test harness (architecture.md §21.3.2) ──────────────────\n\n/**\n * Options for {@link createTestPlugin}.\n */\nexport interface CreateTestPluginOptions {\n  /**\n   * Use an isolated container (default: `true`). Isolated harnesses never\n   * touch the global `Container.getInstance()` singleton, so concurrent\n   * tests can run without stomping each other's DI state.\n   */\n  isolated?: boolean\n\n  /**\n   * Skip auto-invoking `plugin.register(container)` — useful when a test\n   * wants to assert container state *before* the plugin touches it.\n   * Defaults to `false` (register is called eagerly).\n   */\n  skipRegister?: boolean\n}\n\n/**\n * Result of {@link createTestPlugin}. Mirrors the spec sketch in\n * `architecture.md` §21.3.2 — `.container`, lifecycle invokers, and a\n * `runContributors()` helper that builds a one-plugin pipeline against a\n * fake `ExecutionContext`.\n */\nexport interface PluginTestHarness {\n  /** The plugin under test — exposed for direct introspection. */\n  readonly plugin: KickPlugin\n  /** Isolated (or shared) DI container the plugin registered into. */\n  readonly container: Container\n\n  /**\n   * Invoke `plugin.onReady(container)`. No-op if the plugin does not\n   * define the hook.\n   */\n  callOnReady(): Promise<void>\n\n  /**\n   * Invoke `plugin.shutdown()`. No-op if the plugin does not define the\n   * hook.\n   */\n  shutdown(): Promise<void>\n\n  /**\n   * Returns the module classes the plugin ships via `plugin.modules?()`.\n   * The harness does **not** auto-instantiate them — use `createTestApp`\n   * for that. Useful for assertions like \"the plugin exposes the module I\n   * expect\".\n   */\n  modules(): AppModuleEntry[]\n\n  /**\n   * Returns adapter instances the plugin ships via `plugin.adapters?()`.\n   * Useful for testing the adapters alongside the plugin's DI bindings.\n   */\n  adapters(): AppAdapter[]\n\n  /**\n   * Returns middleware entries the plugin ships via `plugin.middleware?()`.\n   */\n  middleware(): any[]\n\n  /**\n   * Returns contributor registrations the plugin ships via\n   * `plugin.contributors?()`.\n   */\n  contributors(): ContributorRegistration[]\n\n  /**\n   * Build a fake {@link ExecutionContext} pre-populated with the given\n   * metadata. Symmetric with {@link runContributor}'s fake context.\n   */\n  makeContext(initial?: Record<string, unknown>): ExecutionContext\n\n  /**\n   * Run every contributor the plugin ships through a built pipeline\n   * against the given context. Dependencies resolve through the harness's\n   * container (same path production uses). Throws if the plugin's\n   * contributors reference `dependsOn` keys no one in the plugin provides.\n   */\n  runContributors(ctx: ExecutionContext): Promise<void>\n}\n\n/**\n * Build an isolated test harness around a single {@link KickPlugin}.\n * Skips the full HTTP layer — symmetric with {@link runContributor} for\n * contributors and {@link createTestApp} for integration runs.\n *\n * @example\n * ```ts\n * const harness = await createTestPlugin(FlagsPlugin({ provider: scripted }))\n *\n * // The plugin's register() already ran — resolve any bindings.\n * const flags = harness.container.resolve(FLAGS_SERVICE)\n *\n * // Drive the post-bootstrap lifecycle.\n * await harness.callOnReady()\n *\n * // Exercise contributors shipped by the plugin.\n * const ctx = harness.makeContext({ requestId: 'req-1' })\n * await harness.runContributors(ctx)\n * expect(ctx.get('flags')).toEqual({ beta: true })\n *\n * await harness.shutdown()\n * ```\n */\nexport async function createTestPlugin(\n  plugin: KickPlugin,\n  options: CreateTestPluginOptions = {},\n): Promise<PluginTestHarness> {\n  const isolated = options.isolated ?? true\n\n  const container = isolated ? Container.create() : (Container.reset(), Container.getInstance())\n\n  if (!options.skipRegister) plugin.register?.(container)\n\n  return {\n    plugin,\n    container,\n\n    async callOnReady() {\n      await plugin.onReady?.(container)\n    },\n\n    async shutdown() {\n      await plugin.shutdown?.()\n    },\n\n    modules() {\n      return plugin.modules?.() ?? []\n    },\n\n    adapters() {\n      return plugin.adapters?.() ?? []\n    },\n\n    middleware() {\n      return plugin.middleware?.() ?? []\n    },\n\n    contributors() {\n      return [...(plugin.contributors?.() ?? [])]\n    },\n\n    makeContext(initial: Record<string, unknown> = {}) {\n      const meta = new Map<string, unknown>(Object.entries(initial))\n      const ctx: ExecutionContext = {\n        get(key) {\n          return meta.get(key) as never\n        },\n        require(key) {\n          const value = meta.get(key)\n          if (value === undefined) throw new MissingContextValueError(key)\n          return value as never\n        },\n        set(key, value) {\n          meta.set(key, value)\n        },\n        requestId: 'test-req',\n      }\n      return ctx\n    },\n\n    async runContributors(ctx: ExecutionContext) {\n      const regs = plugin.contributors?.() ?? []\n      const sources = [...regs].map((registration) => ({\n        registration,\n        source: 'adapter' as const,\n        label: plugin.name,\n      }))\n      const pipeline = buildPipeline(sources)\n      await runContributors({ pipeline, ctx, container })\n    },\n  }\n}\n\n/** Alias for {@link createTestPlugin}, matching the `testPlugin` name used in the architecture spec. */\nexport const testPlugin = createTestPlugin\n"],"mappings":"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"}