@classytic/arc 2.10.8 → 2.11.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 (136) hide show
  1. package/dist/{BaseController-DVNKvoX4.mjs → BaseController-JNV08qOT.mjs} +480 -442
  2. package/dist/{queryCachePlugin-Dumka73q.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  3. package/dist/adapters/index.d.mts +2 -2
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  6. package/dist/audit/index.d.mts +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +5 -5
  9. package/dist/{betterAuthOpenApi--rdY15Ld.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  10. package/dist/cache/index.d.mts +3 -2
  11. package/dist/cache/index.mjs +3 -3
  12. package/dist/cli/commands/docs.mjs +2 -2
  13. package/dist/cli/commands/generate.mjs +37 -27
  14. package/dist/cli/commands/init.mjs +46 -33
  15. package/dist/cli/commands/introspect.mjs +1 -1
  16. package/dist/context/index.mjs +1 -1
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -3
  19. package/dist/core-DXdSSFW-.mjs +1037 -0
  20. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  21. package/dist/{createApp-BwnEAO2h.mjs → createApp-DvNYEhpb.mjs} +75 -27
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-Dci0AYLT.mjs → elevation-DOFoxoDs.mjs} +1 -1
  25. package/dist/{errorHandler-CSxe7KIM.mjs → errorHandler-BQm8ZxTK.mjs} +1 -1
  26. package/dist/{eventPlugin-ByU4Cv0e.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  27. package/dist/events/index.d.mts +3 -3
  28. package/dist/events/index.mjs +2 -2
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/factory/index.d.mts +1 -1
  31. package/dist/factory/index.mjs +2 -2
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.d.mts +3 -3
  35. package/dist/idempotency/index.mjs +1 -1
  36. package/dist/idempotency/redis.d.mts +1 -1
  37. package/dist/{index-C_Noptz-.d.mts → index-BYCqHCVu.d.mts} +2 -2
  38. package/dist/{index-BGbpGVyM.d.mts → index-Cm0vUrr_.d.mts} +699 -494
  39. package/dist/{index-BziRPS4H.d.mts → index-DAushRTt.d.mts} +29 -10
  40. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  41. package/dist/{index-EqQN6p0W.d.mts → index-t8pLpPFW.d.mts} +11 -8
  42. package/dist/index.d.mts +6 -38
  43. package/dist/index.mjs +9 -9
  44. package/dist/integrations/event-gateway.d.mts +1 -1
  45. package/dist/integrations/event-gateway.mjs +1 -1
  46. package/dist/integrations/index.d.mts +2 -2
  47. package/dist/integrations/mcp/index.d.mts +2 -2
  48. package/dist/integrations/mcp/index.mjs +1 -1
  49. package/dist/integrations/mcp/testing.d.mts +1 -1
  50. package/dist/integrations/mcp/testing.mjs +1 -1
  51. package/dist/integrations/streamline.d.mts +46 -5
  52. package/dist/integrations/streamline.mjs +50 -21
  53. package/dist/integrations/websocket-redis.d.mts +1 -1
  54. package/dist/integrations/websocket.d.mts +2 -154
  55. package/dist/integrations/websocket.mjs +292 -224
  56. package/dist/{keys-nWQGUTu1.mjs → keys-CARyUjiR.mjs} +2 -0
  57. package/dist/{loadResources-Bksk8ydA.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  58. package/dist/middleware/index.d.mts +1 -1
  59. package/dist/middleware/index.mjs +1 -1
  60. package/dist/{openapi-DpNpqBmo.mjs → openapi-C0L9ar7m.mjs} +4 -4
  61. package/dist/org/index.d.mts +1 -1
  62. package/dist/permissions/index.d.mts +1 -1
  63. package/dist/permissions/index.mjs +2 -4
  64. package/dist/{permissions-wkqRwicB.mjs → permissions-B4vU9L0Q.mjs} +221 -3
  65. package/dist/{pipe-CGJxqDGx.mjs → pipe-DVoIheVC.mjs} +1 -1
  66. package/dist/pipeline/index.d.mts +1 -1
  67. package/dist/pipeline/index.mjs +1 -1
  68. package/dist/plugins/index.d.mts +4 -4
  69. package/dist/plugins/index.mjs +10 -10
  70. package/dist/plugins/response-cache.mjs +1 -1
  71. package/dist/plugins/tracing-entry.d.mts +1 -1
  72. package/dist/plugins/tracing-entry.mjs +42 -24
  73. package/dist/presets/filesUpload.d.mts +1 -1
  74. package/dist/presets/filesUpload.mjs +3 -3
  75. package/dist/presets/index.d.mts +1 -1
  76. package/dist/presets/index.mjs +1 -1
  77. package/dist/presets/multiTenant.d.mts +1 -1
  78. package/dist/presets/multiTenant.mjs +6 -0
  79. package/dist/presets/search.d.mts +1 -1
  80. package/dist/presets/search.mjs +1 -1
  81. package/dist/{presets-CrwOvuXI.mjs → presets-k604Lj99.mjs} +1 -1
  82. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  83. package/dist/{queryCachePlugin-ChLNZvFT.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  84. package/dist/{redis-MXLp1oOf.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  85. package/dist/registry/index.d.mts +1 -1
  86. package/dist/registry/index.mjs +2 -2
  87. package/dist/{resourceToTools-BhF3JV5p.mjs → resourceToTools--okX6QBr.mjs} +534 -420
  88. package/dist/routerShared-DeESFp4a.mjs +515 -0
  89. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  90. package/dist/scope/index.mjs +2 -2
  91. package/dist/testing/index.d.mts +367 -711
  92. package/dist/testing/index.mjs +637 -1434
  93. package/dist/{tracing-xqXzWeaf.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  94. package/dist/types/index.d.mts +3 -3
  95. package/dist/types/index.mjs +1 -3
  96. package/dist/{types-CVdgPXBW.d.mts → types-CgikqKAj.d.mts} +118 -19
  97. package/dist/{types-CVKBssX5.d.mts → types-D9NqiYIw.d.mts} +1 -1
  98. package/dist/utils/index.d.mts +2 -968
  99. package/dist/utils/index.mjs +5 -6
  100. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  101. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  102. package/package.json +7 -5
  103. package/skills/arc/SKILL.md +123 -38
  104. package/skills/arc/references/testing.md +212 -183
  105. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  106. package/dist/core-3MWJosCH.mjs +0 -1459
  107. package/dist/createActionRouter-C8UUB3Px.mjs +0 -249
  108. package/dist/errors-BI8kEKsO.d.mts +0 -140
  109. package/dist/fields-CTMWOUDt.mjs +0 -126
  110. package/dist/queryParser-NR__Qiju.mjs +0 -419
  111. package/dist/types-CDnTEpga.mjs +0 -27
  112. package/dist/utils-LMwVidKy.mjs +0 -947
  113. /package/dist/{HookSystem-BjFu7zf1.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  114. /package/dist/{ResourceRegistry-CcN2LVrc.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  115. /package/dist/{actionPermissions-TUVR3uiZ.mjs → actionPermissions-C8YYU92K.mjs} +0 -0
  116. /package/dist/{caching-3h93rkJM.mjs → caching-CheW3m-S.mjs} +0 -0
  117. /package/dist/{errorHandler-2ii4RIYr.d.mts → errorHandler-Co3lnVmJ.d.mts} +0 -0
  118. /package/dist/{errors-BqdUDja_.mjs → errors-D5c-5BJL.mjs} +0 -0
  119. /package/dist/{eventPlugin-D1ThQ1Pp.d.mts → eventPlugin-CUNjYYRY.d.mts} +0 -0
  120. /package/dist/{interface-B-pe8fhj.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  121. /package/dist/{interface-yhyb_pLY.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  122. /package/dist/{memory-DqI-449b.mjs → memory-DikHSvWa.mjs} +0 -0
  123. /package/dist/{metrics-TuOmguhi.mjs → metrics-Csh4nsvv.mjs} +0 -0
  124. /package/dist/{multipartBody-CUQGVlM_.mjs → multipartBody-CvTR1Un6.mjs} +0 -0
  125. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-BneOJkpi.mjs} +0 -0
  126. /package/dist/{redis-stream-bkO88VHx.d.mts → redis-stream-CM8TXTix.d.mts} +0 -0
  127. /package/dist/{registry-B0Wl7uVV.mjs → registry-D63ee7fl.mjs} +0 -0
  128. /package/dist/{replyHelpers-BLojtuvR.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  129. /package/dist/{requestContext-C38GskNt.mjs → requestContext-CfRkaxwf.mjs} +0 -0
  130. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  131. /package/dist/{sse-D8UeDwis.mjs → sse-V7aXc3bW.mjs} +0 -0
  132. /package/dist/{store-helpers-DYYUQbQN.mjs → store-helpers-BhrzxvyQ.mjs} +0 -0
  133. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  134. /package/dist/{types-D57iXYb8.mjs → types-DV9WDfeg.mjs} +0 -0
  135. /package/dist/{versioning-B6mimogM.mjs → versioning-CGPjkqAg.mjs} +0 -0
  136. /package/dist/{versioning-CeUXHfjw.d.mts → versioning-M9lNLhO8.d.mts} +0 -0
@@ -1,477 +1,373 @@
1
- import { B as ResourceDefinition, Yt as AnyRecord } from "../index-BGbpGVyM.mjs";
2
- import { d as ResourceLike, r as CreateAppOptions } from "../types-CVdgPXBW.mjs";
1
+ import { B as ResourceDefinition, Pt as AnyRecord } from "../index-Cm0vUrr_.mjs";
2
+ import { d as ResourceLike, r as CreateAppOptions } from "../types-CgikqKAj.mjs";
3
3
  import { StorageContractSetup, StorageContractSetupResult, runStorageContract } from "./storageContract.mjs";
4
- import Fastify, { FastifyInstance, FastifyServerOptions } from "fastify";
5
- import { Connection } from "mongoose";
4
+ import { FastifyInstance, FastifyServerOptions } from "fastify";
6
5
  import { Mock } from "vitest";
7
6
  import { StandardRepo } from "@classytic/repo-core/repository";
8
7
 
9
- //#region src/testing/authHelpers.d.ts
8
+ //#region src/testing/assertions.d.ts
9
+ /**
10
+ * expectArc — Arc-specific response assertions
11
+ *
12
+ * Wraps a Fastify `app.inject` response and exposes fluent assertions for
13
+ * the arc response envelope. Replaces the ~6 patterns repeated hundreds of
14
+ * times across the test suite:
15
+ *
16
+ * expect(res.statusCode).toBe(200);
17
+ * expect(JSON.parse(res.body).success).toBe(true);
18
+ * expect(JSON.parse(res.body).data.password).toBeUndefined();
19
+ *
20
+ * becomes
21
+ *
22
+ * expectArc(res).ok().hidesField('password');
23
+ *
24
+ * Every helper returns the assertion object so you can chain. `.body` /
25
+ * `.data` are lazy accessors — they parse once and cache, so repeated access
26
+ * is cheap.
27
+ *
28
+ * Assertions use `vitest`'s `expect` internally — import this only from
29
+ * test files or modules that run under vitest.
30
+ */
31
+ interface ArcResponseLike {
32
+ statusCode: number;
33
+ body: string;
34
+ }
35
+ interface ArcAssertion {
36
+ /** Raw response — kept for ad-hoc drill-down. */
37
+ readonly response: ArcResponseLike;
38
+ /** Parsed body (JSON). Cached. */
39
+ readonly body: Record<string, unknown>;
40
+ /** `body.data` — undefined for failed responses. */
41
+ readonly data: unknown;
42
+ /** Full fluent chain below — every method returns `this`. */
43
+ ok(status?: number): ArcAssertion;
44
+ failed(status?: number): ArcAssertion;
45
+ unauthorized(): ArcAssertion;
46
+ forbidden(): ArcAssertion;
47
+ notFound(): ArcAssertion;
48
+ validationError(): ArcAssertion;
49
+ conflict(): ArcAssertion;
50
+ hasData(): ArcAssertion;
51
+ hasStatus(status: number): ArcAssertion;
52
+ hidesField(field: string): ArcAssertion;
53
+ showsField(field: string): ArcAssertion;
54
+ /**
55
+ * Asserts the arc paginated-list envelope: `success`, `docs[]`, and at
56
+ * least one of `page`/`limit`/`total`/`hasNext`/`hasPrev`. `expected`
57
+ * optionally pins specific fields.
58
+ */
59
+ paginated(expected?: {
60
+ page?: number;
61
+ limit?: number;
62
+ total?: number;
63
+ hasNext?: boolean;
64
+ hasPrev?: boolean;
65
+ }): ArcAssertion;
66
+ /** Assert `body.error` (or `body.message`) matches the given string or regex. */
67
+ hasError(matcher: string | RegExp): ArcAssertion;
68
+ /** Assert a specific key on `body.meta` or flattened top-level (matches sendControllerResponse flattening). */
69
+ hasMeta(key: string, value?: unknown): ArcAssertion;
70
+ }
71
+ declare function expectArc(response: ArcResponseLike): ArcAssertion;
72
+ //#endregion
73
+ //#region src/testing/authSession.d.ts
74
+ /**
75
+ * A concrete auth session — headers ready to drop into `app.inject`.
76
+ *
77
+ * Frozen so tests can safely cache + share sessions between `it` blocks
78
+ * without worrying about one mutation leaking into another.
79
+ */
80
+ interface TestAuthSession {
81
+ readonly role: string;
82
+ readonly token: string;
83
+ readonly orgId: string | undefined;
84
+ readonly user: Record<string, unknown> | undefined;
85
+ readonly headers: Readonly<Record<string, string>>;
86
+ /**
87
+ * Return a new session with extra headers merged over the defaults.
88
+ * Does not mutate the original — use for one-off requests that need
89
+ * a tracing header, idempotency key, etc.
90
+ */
91
+ withExtra(headers: Record<string, string>): TestAuthSession;
92
+ }
93
+ /**
94
+ * Per-role auth config. `user` + `token` are mutually exclusive:
95
+ * - `user` → the provider signs a fresh JWT (for apps using @fastify/jwt)
96
+ * - `token` → pre-signed token (Better Auth, external issuer, fixtures)
97
+ */
98
+ interface RoleConfig {
99
+ /** JWT payload — signed on-the-fly by the provider (JWT apps only) */
100
+ user?: Record<string, unknown>;
101
+ /** Pre-signed bearer token (Better Auth, external issuer) */
102
+ token?: string;
103
+ /** Injected as `x-organization-id` header; falls back to provider default */
104
+ orgId?: string;
105
+ /** Custom headers merged into every session for this role */
106
+ extraHeaders?: Record<string, string>;
107
+ }
108
+ interface TestAuthProvider {
109
+ /** Register (or re-register) a named role. Later calls replace the earlier config. */
110
+ register(role: string, config: RoleConfig): void;
111
+ /** Resolve a session for a registered role. Throws if the role is unknown. */
112
+ as(role: string): TestAuthSession;
113
+ /** Unauthenticated session — empty headers. Useful for 401 tests. */
114
+ anonymous(): TestAuthSession;
115
+ /** Snapshot of registered role names (stable reference; mutates the array is UB). */
116
+ readonly roles: readonly string[];
117
+ }
118
+ /**
119
+ * JWT provider — signs tokens on-the-fly using `app.jwt.sign()`.
120
+ * Requires `@fastify/jwt` registered on the app.
121
+ *
122
+ * Accepts both `user` (payload to sign) and `token` (pre-signed) role configs,
123
+ * so the same provider handles mixed flows in a single test.
124
+ */
125
+ declare function createJwtAuthProvider(app: FastifyInstance, opts?: {
126
+ defaultOrgId?: string;
127
+ }): TestAuthProvider;
128
+ /**
129
+ * Better Auth provider — uses pre-signed tokens (from signUp/signIn flows).
130
+ * No signing: role configs MUST carry `token`. A `user` alone will throw.
131
+ */
132
+ declare function createBetterAuthProvider(opts?: {
133
+ defaultOrgId?: string;
134
+ }): TestAuthProvider;
135
+ /**
136
+ * Custom provider — plug in your own token minting logic. Useful for
137
+ * mocked external issuers, session-cookie flows, or fixtures that pre-mint.
138
+ */
139
+ declare function createCustomAuthProvider(mintToken: (role: string, config: RoleConfig) => string, opts?: {
140
+ defaultOrgId?: string;
141
+ }): TestAuthProvider;
142
+ //#endregion
143
+ //#region src/testing/betterAuth.d.ts
10
144
  interface BetterAuthTestHelpersOptions {
11
- /** Base path for auth routes (default: '/api/auth') */
145
+ /** Base path where Better Auth is mounted (default: '/api/auth'). */
12
146
  basePath?: string;
13
147
  }
148
+ interface SignUpInput {
149
+ email: string;
150
+ password: string;
151
+ name: string;
152
+ }
153
+ interface SignInInput {
154
+ email: string;
155
+ password: string;
156
+ }
157
+ interface CreateOrgInput {
158
+ name: string;
159
+ slug?: string;
160
+ metadata?: Record<string, unknown>;
161
+ }
162
+ /** Fastify-ish injection response — minimal shape we read. */
163
+ interface InjectResponse {
164
+ statusCode: number;
165
+ body: string;
166
+ headers?: Record<string, unknown>;
167
+ }
168
+ /** Abstracted so helpers work with both Fastify and Fastify-like test instances. */
169
+ interface Injector {
170
+ inject(opts: {
171
+ method: string;
172
+ url: string;
173
+ payload?: unknown;
174
+ headers?: Record<string, string>;
175
+ }): Promise<InjectResponse>;
176
+ }
14
177
  interface AuthResponse {
15
178
  statusCode: number;
16
179
  token: string;
17
- user: any;
18
- body: any;
180
+ userId: string;
181
+ body: unknown;
19
182
  }
20
183
  interface OrgResponse {
21
184
  statusCode: number;
22
185
  orgId: string;
23
- body: any;
186
+ body: unknown;
24
187
  }
25
188
  interface BetterAuthTestHelpers {
26
- signUp(app: FastifyInstance, data: {
27
- email: string;
28
- password: string;
29
- name: string;
30
- }): Promise<AuthResponse>;
31
- signIn(app: FastifyInstance, data: {
32
- email: string;
33
- password: string;
34
- }): Promise<AuthResponse>;
35
- createOrg(app: FastifyInstance, token: string, data: {
36
- name: string;
37
- slug: string;
38
- }): Promise<OrgResponse>;
39
- setActiveOrg(app: FastifyInstance, token: string, orgId: string): Promise<{
40
- statusCode: number;
41
- body: any;
42
- }>;
189
+ /** POST {basePath}/sign-up/email — create a user account. */
190
+ signUp(app: Injector, input: SignUpInput): Promise<AuthResponse>;
191
+ /** POST {basePath}/sign-in/email — authenticate an existing user. */
192
+ signIn(app: Injector, input: SignInInput): Promise<AuthResponse>;
193
+ /** POST {basePath}/organization/create — create an org owned by the caller. */
194
+ createOrg(app: Injector, token: string, input: CreateOrgInput): Promise<OrgResponse>;
195
+ /** POST {basePath}/organization/set-active — switch the caller's active org. */
196
+ setActiveOrg(app: Injector, token: string, orgId: string): Promise<InjectResponse>;
197
+ /** Build `{ authorization: 'Bearer ...', 'x-organization-id': ... }` headers. */
43
198
  authHeaders(token: string, orgId?: string): Record<string, string>;
44
199
  }
45
- interface TestUserContext {
46
- token: string;
47
- userId: string;
48
- email: string;
49
- }
50
- interface TestOrgContext<T = Record<string, TestUserContext>> {
51
- app: FastifyInstance;
52
- orgId: string;
53
- users: T;
54
- teardown: () => Promise<void>;
55
- }
56
- interface SetupUserConfig {
57
- /** Key used to reference this user in the context (e.g. 'admin', 'member') */
200
+ interface BetterAuthTestUser {
201
+ /** Identity key the caller passed in (e.g. 'admin' / 'member' / 'viewer'). */
58
202
  key: string;
59
203
  email: string;
60
204
  password: string;
61
205
  name: string;
62
- /** Organization role assigned after joining */
63
- role: string;
64
- /** If true, this user creates the org (becomes org owner). Exactly one user should have this. */
206
+ role?: string;
207
+ /**
208
+ * True for the user who creates the org. The creator's signup runs first
209
+ * and produces the `orgId` that every subsequent user is added to.
210
+ */
65
211
  isCreator?: boolean;
66
212
  }
67
- interface SetupBetterAuthOrgOptions {
68
- /** Factory function to create the Fastify app instance */
69
- createApp: () => Promise<FastifyInstance>;
70
- /** Organization to create */
71
- org: {
72
- name: string;
73
- slug: string;
74
- };
75
- /** Users to create and add to the organization */
76
- users: SetupUserConfig[];
77
- /**
78
- * Callback to add a member to the org.
79
- * Apps wire Better Auth differently some use auth.api.addMember, others use HTTP.
213
+ interface SetupBetterAuthTestAppInput {
214
+ /** A built Fastify instance with Better Auth registered — app lifecycle is the caller's responsibility. */
215
+ app: FastifyInstance;
216
+ /** Org to create. The creator user (from `users[]`) owns it. */
217
+ org: CreateOrgInput;
218
+ /** Users to create. Exactly one MUST have `isCreator: true`. */
219
+ users: ReadonlyArray<BetterAuthTestUser>;
220
+ /**
221
+ * Add a non-creator user to the org. Called once per user with
222
+ * `isCreator !== true`. Consumer implements this — Better Auth apps can
223
+ * use invitations or direct member-add depending on plugin config.
224
+ *
225
+ * A successful status code in the returned `InjectResponse` is what the
226
+ * helper checks; body shape is app-specific.
80
227
  */
81
- addMember: (data: {
82
- organizationId: string;
228
+ addMember?: (data: {
229
+ app: FastifyInstance;
230
+ creatorToken: string;
231
+ orgId: string;
83
232
  userId: string;
84
233
  role: string;
85
- }) => Promise<{
86
- statusCode: number;
234
+ }) => Promise<InjectResponse>;
235
+ /** Better Auth base path override (default: '/api/auth'). */
236
+ basePath?: string;
237
+ }
238
+ interface SetupBetterAuthTestAppResult {
239
+ /** Same app the caller passed in (returned for convenience). */
240
+ app: FastifyInstance;
241
+ /** The org created by the first `isCreator: true` user. */
242
+ orgId: string;
243
+ /** Keyed by the user's `key` field — tokens + ids for every user. */
244
+ users: Record<string, {
245
+ userId: string;
246
+ token: string;
247
+ email: string;
248
+ role?: string;
87
249
  }>;
88
250
  /**
89
- * Optional hook for app-specific initialization after all users are set up.
90
- * Use this for things like recruiter→account manager hierarchy.
251
+ * A `TestAuthProvider` pre-populated with one role per user, so the
252
+ * 2.11 pattern `auth.as('admin').headers` works immediately:
253
+ *
254
+ * const res = await app.inject({
255
+ * url: '/jobs',
256
+ * headers: result.auth.as('admin').headers,
257
+ * });
258
+ *
259
+ * Pre-signed tokens from the signup/signin flow are registered — no
260
+ * on-the-fly JWT signing involved (Better Auth issues opaque session
261
+ * tokens, not signed JWTs).
91
262
  */
92
- afterSetup?: (ctx: TestOrgContext) => Promise<void>;
93
- /** Override auth helper options (e.g. custom basePath) */
94
- authHelpers?: BetterAuthTestHelpersOptions;
263
+ auth: TestAuthProvider;
264
+ /** Close the app. Exposed as a single handle so tests can await it in afterAll. */
265
+ teardown: () => Promise<void>;
95
266
  }
96
267
  /**
97
- * Safely parse a JSON response body.
98
- * Returns null if parsing fails.
268
+ * Parse a JSON body safely. Returns null when empty or malformed — Better
269
+ * Auth endpoints occasionally emit empty 204 bodies (e.g. set-active) and
270
+ * tests shouldn't crash on the parse.
99
271
  */
100
- declare function safeParseBody(body: string): any;
272
+ declare function safeParseBody<T = unknown>(body: string | undefined): T | null;
101
273
  /**
102
- * Create stateless Better Auth test helpers.
103
- *
104
- * All methods take the app instance as a parameter, making them
105
- * safe to use across multiple test suites.
274
+ * Stateless Better Auth helpers. Each function takes the app as a positional
275
+ * argument, so a single helper instance works across multiple test apps in
276
+ * the same suite.
106
277
  */
107
278
  declare function createBetterAuthTestHelpers(options?: BetterAuthTestHelpersOptions): BetterAuthTestHelpers;
108
279
  /**
109
- * Set up a complete test organization with users.
110
- *
111
- * Creates the app, signs up users, creates an org, adds members,
112
- * and returns a context object with tokens and a teardown function.
113
- *
114
- * @example
115
- * ```typescript
116
- * const ctx = await setupBetterAuthOrg({
117
- * createApp: () => createAppInstance(),
118
- * org: { name: 'Test Corp', slug: 'test-corp' },
119
- * users: [
120
- * { key: 'admin', email: 'admin@test.com', password: 'pass', name: 'Admin', role: 'admin', isCreator: true },
121
- * { key: 'member', email: 'user@test.com', password: 'pass', name: 'User', role: 'member' },
122
- * ],
123
- * addMember: async (data) => {
124
- * await auth.api.addMember({ body: data });
125
- * return { statusCode: 200 };
126
- * },
127
- * });
128
- *
129
- * // Use in tests:
130
- * const res = await ctx.app.inject({
131
- * method: 'GET',
132
- * url: '/api/products',
133
- * headers: auth.authHeaders(ctx.users.admin.token, ctx.orgId),
134
- * });
135
- *
136
- * // Cleanup:
137
- * await ctx.teardown();
138
- * ```
139
- */
140
- declare function setupBetterAuthOrg(options: SetupBetterAuthOrgOptions): Promise<TestOrgContext>;
280
+ * Composite setup for Better Auth apps. Replaces the pre-v2.11
281
+ * `setupBetterAuthOrg` with a tighter contract:
282
+ *
283
+ * 1. Accept an already-built `app` (caller owns its lifecycle arc's
284
+ * `createTestApp` composes naturally, but any built Fastify works).
285
+ * 2. Sign up every user in order.
286
+ * 3. The creator user creates the org; orgId is captured.
287
+ * 4. Every non-creator user is added via the caller-supplied `addMember`
288
+ * (Better Auth's org-member API is app-specific, so arc doesn't
289
+ * hardcode it).
290
+ * 5. Set the active org on every user.
291
+ * 6. Register each user into a fresh `TestAuthProvider` the 2.11
292
+ * `.as(key).headers` pattern works out of the box on the result.
293
+ *
294
+ * Exactly one user must be `isCreator: true`. Throws if zero or multiple
295
+ * creators are supplied (ambiguous ownership is a boot-time bug, not a
296
+ * runtime one).
297
+ */
298
+ declare function setupBetterAuthTestApp(input: SetupBetterAuthTestAppInput): Promise<SetupBetterAuthTestAppResult>;
141
299
  //#endregion
142
- //#region src/testing/dbHelpers.d.ts
143
- /**
144
- * Test database manager
145
- */
146
- declare class TestDatabase {
147
- private connection?;
148
- private dbName;
149
- constructor(dbName?: string);
150
- /**
151
- * Connect to test database
152
- */
153
- connect(uri?: string): Promise<Connection>;
154
- /**
155
- * Disconnect and cleanup
156
- */
157
- disconnect(): Promise<void>;
158
- /**
159
- * Clear all collections
160
- */
161
- clear(): Promise<void>;
162
- /**
163
- * Get connection
164
- */
165
- getConnection(): Connection;
300
+ //#region src/testing/fixtures.d.ts
301
+ type FixtureFactory<T extends AnyRecord = AnyRecord> = (data: Partial<T>) => Promise<T>;
302
+ /** Delete hook invoked by `clear()` for records that were created through a factory. */
303
+ type FixtureDestroyer<T extends AnyRecord = AnyRecord> = (record: T) => Promise<void>;
304
+ interface FixtureRegistration<T extends AnyRecord = AnyRecord> {
305
+ create: FixtureFactory<T>;
306
+ /** Optional cleanup. Defaults to a no-op; adapters that support deletion should provide one. */
307
+ destroy?: FixtureDestroyer<T>;
166
308
  }
167
- /**
168
- * Higher-order function to wrap tests with database setup/teardown
169
- *
170
- * @example
171
- * describe('Product Tests', () => {
172
- * withTestDb(async (db) => {
173
- * test('create product', async () => {
174
- * const Product = db.getConnection().model('Product', schema);
175
- * const product = await Product.create({ name: 'Test' });
176
- * expect(product.name).toBe('Test');
177
- * });
178
- * });
179
- * });
180
- */
181
- declare function withTestDb(tests: (db: TestDatabase) => void | Promise<void>, options?: {
182
- uri?: string;
183
- dbName?: string;
184
- }): void;
185
- /**
186
- * Create test fixtures
187
- *
188
- * @example
189
- * const fixtures = new TestFixtures(connection);
190
- *
191
- * await fixtures.load('products', [
192
- * { name: 'Product 1', price: 100 },
193
- * { name: 'Product 2', price: 200 },
194
- * ]);
195
- *
196
- * const products = await fixtures.get('products');
197
- */
198
- declare class TestFixtures {
199
- private fixtures;
200
- private connection;
201
- constructor(connection: Connection);
202
- /**
203
- * Load fixtures into a collection
204
- */
205
- load<T = any>(collectionName: string, data: Partial<T>[]): Promise<T[]>;
206
- /**
207
- * Get loaded fixtures
208
- */
209
- get<T = any>(collectionName: string): T[];
210
- /**
211
- * Get first fixture
212
- */
213
- getFirst<T = any>(collectionName: string): T | null;
214
- /**
215
- * Clear all fixtures
309
+ interface TestFixtures {
310
+ /** Register a named factory. Later calls replace the earlier registration. */
311
+ register<T extends AnyRecord = AnyRecord>(name: string, factoryOrRegistration: FixtureFactory<T> | FixtureRegistration<T>): void;
312
+ /** Create one record through the named factory. Tracked for cleanup. */
313
+ create<T extends AnyRecord = AnyRecord>(name: string, data?: Partial<T>): Promise<T>;
314
+ /** Create many records with a shared template. Tracked for cleanup. */
315
+ createMany<T extends AnyRecord = AnyRecord>(name: string, count: number, template?: Partial<T>): Promise<T[]>;
316
+ /**
317
+ * Run every registered `destroy` hook over the records this instance
318
+ * created, then forget them. Safe to call multiple times (idempotent).
319
+ * Factories without a `destroy` hook silently skip — assume the test
320
+ * harness tears the whole DB down at the end.
216
321
  */
217
322
  clear(): Promise<void>;
323
+ /** All records ever created by a given factory name (read-only snapshot). */
324
+ all<T extends AnyRecord = AnyRecord>(name: string): readonly T[];
325
+ /** Registered factory names. */
326
+ readonly names: readonly string[];
218
327
  }
219
- /**
220
- * In-memory MongoDB for ultra-fast tests
221
- *
222
- * Requires: mongodb-memory-server
223
- *
224
- * @example
225
- * import { InMemoryDatabase } from '@classytic/arc/testing';
226
- *
227
- * describe('Fast Tests', () => {
228
- * const memoryDb = new InMemoryDatabase();
229
- *
230
- * beforeAll(async () => {
231
- * await memoryDb.start();
232
- * });
233
- *
234
- * afterAll(async () => {
235
- * await memoryDb.stop();
236
- * });
237
- *
238
- * test('create user', async () => {
239
- * const uri = memoryDb.getUri();
240
- * // Use uri for connection
241
- * });
242
- * });
243
- */
244
- declare class InMemoryDatabase {
245
- private mongod?;
246
- private uri?;
247
- /**
248
- * Start in-memory MongoDB
249
- */
250
- start(): Promise<string>;
251
- /**
252
- * Stop in-memory MongoDB
253
- */
254
- stop(): Promise<void>;
255
- /**
256
- * Get connection URI
257
- */
258
- getUri(): string;
259
- }
260
- /**
261
- * Database transaction helper for testing
262
- */
263
- declare class TestTransaction {
264
- private session?;
265
- private connection;
266
- constructor(connection: Connection);
267
- /**
268
- * Start transaction
269
- */
270
- start(): Promise<void>;
271
- /**
272
- * Commit transaction
273
- */
274
- commit(): Promise<void>;
275
- /**
276
- * Rollback transaction
277
- */
278
- rollback(): Promise<void>;
279
- /**
280
- * Get session
281
- */
282
- getSession(): any;
283
- }
284
- /**
285
- * Seed data helper
286
- */
287
- declare class TestSeeder {
288
- private connection;
289
- constructor(connection: Connection);
290
- /**
291
- * Seed collection with data
292
- */
293
- seed<T>(collectionName: string, generator: () => T[], count?: number): Promise<T[]>;
294
- /**
295
- * Clear collection
296
- */
297
- clear(collectionName: string): Promise<void>;
298
- /**
299
- * Clear all collections
300
- */
301
- clearAll(): Promise<void>;
302
- }
303
- /**
304
- * Database snapshot helper for rollback testing
305
- */
306
- declare class DatabaseSnapshot {
307
- private snapshots;
308
- private connection;
309
- constructor(connection: Connection);
310
- /**
311
- * Take snapshot of current database state
312
- */
313
- take(): Promise<void>;
314
- /**
315
- * Restore database to snapshot
316
- */
317
- restore(): Promise<void>;
318
- /**
319
- * Clear snapshot
320
- */
321
- clear(): void;
322
- }
328
+ declare function createTestFixtures(): TestFixtures;
323
329
  //#endregion
324
330
  //#region src/testing/HttpTestHarness.d.ts
325
- /**
326
- * Abstraction for generating auth headers in tests.
327
- * Supports JWT, Better Auth, or any custom auth mechanism.
328
- */
329
- interface AuthProvider {
330
- /** Get HTTP headers for a given role key */
331
- getHeaders(role: string): Record<string, string>;
332
- /** Available role keys (e.g. ['admin', 'member', 'viewer']) */
333
- availableRoles: string[];
334
- /** Role key that has full CRUD access */
335
- adminRole: string;
336
- }
337
- /**
338
- * Create an auth provider for JWT-based apps.
339
- *
340
- * Generates JWT tokens on the fly using the app's JWT plugin.
341
- *
342
- * @example
343
- * ```typescript
344
- * const auth = createJwtAuthProvider({
345
- * app,
346
- * users: {
347
- * admin: { payload: { id: '1', roles: ['admin'] }, organizationId: 'org1' },
348
- * viewer: { payload: { id: '2', roles: ['viewer'] } },
349
- * },
350
- * adminRole: 'admin',
351
- * });
352
- * ```
353
- */
354
- declare function createJwtAuthProvider(options: {
355
- app: FastifyInstance;
356
- users: Record<string, {
357
- payload: Record<string, unknown>;
358
- organizationId?: string;
359
- }>;
360
- adminRole: string;
361
- }): AuthProvider;
362
- /**
363
- * Create an auth provider for Better Auth apps.
364
- *
365
- * Uses pre-existing tokens (from signUp/signIn) rather than generating them.
366
- *
367
- * @example
368
- * ```typescript
369
- * const auth = createBetterAuthProvider({
370
- * tokens: {
371
- * admin: ctx.users.admin.token,
372
- * member: ctx.users.member.token,
373
- * },
374
- * orgId: ctx.orgId,
375
- * adminRole: 'admin',
376
- * });
377
- * ```
378
- */
379
- declare function createBetterAuthProvider(options: {
380
- tokens: Record<string, string>;
381
- orgId: string;
382
- adminRole: string;
383
- }): AuthProvider;
384
331
  interface HttpTestHarnessOptions<T = unknown> {
385
- /** Fastify app instance (must be ready) */
332
+ /** Fastify app (must be ready). */
386
333
  app: FastifyInstance;
387
- /** Test data fixtures */
334
+ /** Auth provider (from `createTestApp` or one of the auth factories). */
335
+ auth: TestAuthProvider;
336
+ /** Role name registered on `auth` that has full CRUD access. */
337
+ adminRole: string;
338
+ /** Request bodies for CRUD probes. */
388
339
  fixtures: {
389
- /** Valid payload for creating a resource */valid: Partial<T>; /** Payload for updating a resource (defaults to valid) */
390
- update?: Partial<T>; /** Invalid payload that should fail validation */
340
+ valid: Partial<T>;
341
+ update?: Partial<T>;
391
342
  invalid?: Partial<T>;
392
343
  };
393
- /** Auth provider for generating request headers */
394
- auth: AuthProvider;
395
- /** API path prefix (default: '/api' for eager, '' for deferred) */
344
+ /** URL prefix (default: `""`; apps mounted under `/api` pass `/api`). */
396
345
  apiPrefix?: string;
397
346
  }
398
- /** Options can be passed directly or as a getter for deferred resolution */
399
347
  type OptionsOrGetter<T> = HttpTestHarnessOptions<T> | (() => HttpTestHarnessOptions<T>);
400
- /**
401
- * HTTP-level test harness for Arc resources.
402
- *
403
- * Generates tests that exercise the full HTTP lifecycle:
404
- * routes, auth, permissions, pipeline, and response envelope.
405
- *
406
- * Supports deferred options via a getter function, which is essential
407
- * when the app instance comes from async `beforeAll()` setup.
408
- */
409
348
  declare class HttpTestHarness<T = unknown> {
410
349
  private resource;
411
350
  private optionsOrGetter;
412
- private eagerBaseUrl;
413
351
  private enabledRoutes;
414
- private updateMethod;
415
- constructor(resource: ResourceDefinition<unknown>, optionsOrGetter: OptionsOrGetter<T>);
416
- /** Resolve options (supports both direct and deferred) */
417
- private getOptions;
418
352
  /**
419
- * Resolve the base URL for requests.
420
- *
421
- * - Eager mode: uses pre-computed baseUrl from constructor
422
- * - Deferred mode: reads apiPrefix from the getter options at runtime
423
- *
424
- * Must only be called inside it()/afterAll() callbacks (after beforeAll has run).
353
+ * Update verbs exercised by this harness instance. One entry for single-method
354
+ * resources (`"PATCH"` or `"PUT"`), two for `updateMethod: "both"` so both
355
+ * verbs are covered the framework mounts both, and the harness should
356
+ * probe both.
425
357
  */
358
+ private updateMethods;
359
+ constructor(resource: ResourceDefinition<unknown>, optionsOrGetter: OptionsOrGetter<T>);
360
+ private getOptions;
426
361
  private getBaseUrl;
427
- /**
428
- * Run all test suites: CRUD + permissions + validation
429
- */
362
+ private adminHeaders;
430
363
  runAll(): void;
431
- /**
432
- * Run HTTP-level CRUD tests.
433
- *
434
- * Tests each enabled CRUD operation through app.inject():
435
- * - POST (create) → 200/201 with { success: true, data }
436
- * - GET (list) → 200 with array or paginated response
437
- * - GET /:id → 200 with { success: true, data }
438
- * - PATCH/PUT /:id → 200 with { success: true, data }
439
- * - DELETE /:id → 200
440
- * - GET /:id with non-existent ID → 404
441
- */
442
364
  runCrud(): void;
443
- /**
444
- * Run permission tests.
445
- *
446
- * Tests that:
447
- * - Unauthenticated requests return 401
448
- * - Admin role gets 2xx for all operations
449
- */
450
365
  runPermissions(): void;
451
- /**
452
- * Run validation tests.
453
- *
454
- * Tests that invalid payloads return 400.
455
- */
456
366
  runValidation(): void;
457
367
  }
458
368
  /**
459
- * Create an HTTP test harness for an Arc resource.
460
- *
461
- * Accepts options directly or as a getter function for deferred resolution.
462
- *
463
- * @example Deferred (recommended for async setup)
464
- * ```typescript
465
- * let ctx: TestContext;
466
- * beforeAll(async () => { ctx = await setupTestOrg(); });
467
- *
468
- * createHttpTestHarness(jobResource, () => ({
469
- * app: ctx.app,
470
- * apiPrefix: '',
471
- * fixtures: { valid: { title: 'Test' } },
472
- * auth: createBetterAuthProvider({ ... }),
473
- * })).runAll();
474
- * ```
369
+ * Create an HTTP test harness. `optionsOrGetter` may be a plain object
370
+ * (for eager app setup) or a getter function (for async `beforeAll` apps).
475
371
  */
476
372
  declare function createHttpTestHarness<T = unknown>(resource: ResourceDefinition<unknown>, optionsOrGetter: HttpTestHarnessOptions<T> | (() => HttpTestHarnessOptions<T>)): HttpTestHarness<T>;
477
373
  //#endregion
@@ -597,321 +493,81 @@ declare function preloadResources(globResult: EagerGlobResult): ResourceLike[];
597
493
  */
598
494
  declare function preloadResourcesAsync(globResult: LazyGlobResult): Promise<ResourceLike[]>;
599
495
  //#endregion
600
- //#region src/testing/TestHarness.d.ts
601
- /**
602
- * Test fixtures for a resource
603
- */
604
- interface TestFixtures$1<T = any> {
605
- /** Valid create payload */
606
- valid: Partial<T>;
607
- /** Update payload (optional, defaults to valid) */
608
- update?: Partial<T>;
609
- /** Invalid payload for validation tests (optional) */
610
- invalid?: Partial<T>;
611
- }
612
- /**
613
- * Test harness options
614
- */
615
- interface TestHarnessOptions<T = any> {
616
- /** Test data fixtures */
617
- fixtures: TestFixtures$1<T>;
618
- /** Custom setup function (runs before all tests) */
619
- setupFn?: () => Promise<void> | void;
620
- /** Custom teardown function (runs after all tests) */
621
- teardownFn?: () => Promise<void> | void;
622
- /** MongoDB connection URI (defaults to process.env.MONGO_URI) */
623
- mongoUri?: string;
624
- }
625
- declare class TestHarness<T = unknown> {
626
- private resource;
627
- private Model;
628
- private fixtures;
629
- private setupFn?;
630
- private teardownFn?;
631
- private mongoUri;
632
- private _createdIds;
633
- constructor(resource: ResourceDefinition<unknown>, options: TestHarnessOptions<T>);
634
- /**
635
- * Run all baseline tests (schema, presets, field permissions, pipeline, events).
636
- *
637
- * For HTTP-level CRUD coverage (routes, auth, permissions), use
638
- * {@link HttpTestHarness} instead.
639
- */
640
- runAll(): void;
641
- /**
642
- * Run validation tests
643
- *
644
- * Tests schema validation, required fields, etc.
645
- */
646
- runValidation(): void;
647
- /**
648
- * Run preset-specific tests
496
+ //#region src/testing/testApp.d.ts
497
+ type DbMode = "in-memory" | {
498
+ uri: string;
499
+ } | false;
500
+ type AuthMode = "jwt" | "better-auth" | "none";
501
+ interface CreateTestAppOptions extends Partial<Omit<CreateAppOptions, "resources">> {
502
+ /**
503
+ * Resources to auto-register. Pass `defineResource` results directly —
504
+ * createTestApp registers each as a Fastify plugin under their `prefix`.
505
+ * For apps that need custom registration, use `plugins: async (f) => { ... }`
506
+ * instead (standard createApp hook, passed through).
507
+ */
508
+ resources?: ReadonlyArray<ResourceDefinition<unknown>>;
509
+ /**
510
+ * Database mode:
511
+ * - `'in-memory'` (default) boot a MongoMemoryServer, expose `dbUri`,
512
+ * tear down on `close()`. Requires `mongodb-memory-server`.
513
+ * - `{ uri }` — external Mongo URI; lifecycle is the caller's responsibility.
514
+ * - `false` no DB wiring at all. Useful for pure Fastify unit tests.
649
515
  *
650
- * Auto-detects applied presets and tests their functionality:
651
- * - softDelete: deletedAt field, soft delete/restore
652
- * - slugLookup: slug generation
653
- * - tree: parent references, displayOrder
654
- * - multiTenant: organizationId requirement
655
- * - ownedByUser: userId requirement
516
+ * `dbUri` is returned on the context in every mode except `false`. Arc does
517
+ * NOT automatically thread it into resource adapters — set
518
+ * `connectMongoose: true` (Mongoose apps) or connect your adapter manually
519
+ * before importing resources.
656
520
  */
657
- runPresets(): void;
521
+ db?: DbMode;
658
522
  /**
659
- * Run field-level permission tests
523
+ * When `true`, runs `mongoose.connect(dbUri)` before booting the Fastify
524
+ * app and `mongoose.disconnect()` on `close()`. Turns the `db: 'in-memory'`
525
+ * path into a one-liner for Mongoose-backed tests. Defaults to `false`.
660
526
  *
661
- * Auto-generates tests for each field permission:
662
- * - hidden: field is stripped from responses
663
- * - visibleTo: field only shown to specified roles
664
- * - writableBy: field stripped from writes by non-privileged users
665
- * - redactFor: field shows redacted value for specified roles
527
+ * Non-Mongoose adapters (Prisma, sqlitekit, custom) should leave this
528
+ * `false` and wire their own connection to `ctx.dbUri`.
666
529
  */
667
- runFieldPermissions(): void;
530
+ connectMongoose?: boolean;
668
531
  /**
669
- * Run pipeline configuration tests
532
+ * Auth mode attached to `ctx.auth` (and, for `'jwt'`, the default auth
533
+ * plugin on the app):
670
534
  *
671
- * Validates that pipeline steps are properly configured:
672
- * - All steps have names
673
- * - All steps have valid _type discriminants
674
- * - Operation filters (if set) use valid CRUD operation names
675
- */
676
- runPipeline(): void;
677
- /**
678
- * Run event definition tests
679
- *
680
- * Validates that events are properly defined:
681
- * - All events have handler functions
682
- * - Event names follow resource:action convention
683
- * - Schema definitions (if present) are valid objects
684
- */
685
- runEvents(): void;
535
+ * - `'jwt'` (default) provider signs tokens via `app.jwt.sign()`; the
536
+ * factory applies a default `auth: { type: 'jwt', jwt: {...} }` config
537
+ * UNLESS the caller supplies their own `auth` in options.
538
+ * - `'better-auth'` provider uses pre-signed tokens you register.
539
+ * **No default auth config is applied** — the caller MUST pass their
540
+ * own `auth: { type: 'better-auth', ... }` via options, otherwise the
541
+ * app runs without an auth plugin and every request is unauthenticated.
542
+ * Mismatched `authMode: 'better-auth'` with a JWT-configured app would
543
+ * be a subtle bug (tests look like they pass but hit the wrong
544
+ * middleware), so we reject it at setup time.
545
+ * - `'none'` no `ctx.auth` attached; no default auth config.
546
+ */
547
+ authMode?: AuthMode;
548
+ /** Default org ID stamped on every session unless the role overrides. */
549
+ defaultOrgId?: string;
686
550
  }
687
- /**
688
- * Create a test harness for an Arc resource
689
- *
690
- * @param resource - The Arc resource definition to test
691
- * @param options - Test harness configuration
692
- * @returns Test harness instance
693
- *
694
- * @example
695
- * import { createTestHarness } from '@classytic/arc/testing';
696
- *
697
- * const harness = createTestHarness(productResource, {
698
- * fixtures: {
699
- * valid: { name: 'Product', price: 100 },
700
- * update: { name: 'Updated' },
701
- * },
702
- * });
703
- *
704
- * harness.runAll(); // Generates 50+ baseline tests
705
- */
706
- declare function createTestHarness<T = any>(resource: ResourceDefinition, options: TestHarnessOptions<T>): TestHarness<T>;
707
- /**
708
- * Test file generation options
709
- */
710
- interface GenerateTestFileOptions {
711
- /** Applied presets (e.g., ['softDelete', 'slugLookup']) */
712
- presets?: string[];
713
- /** Module path for imports (default: '.') */
714
- modulePath?: string;
715
- }
716
- /**
717
- * Generate test file content for a resource
718
- *
719
- * Useful for scaffolding new resource tests via CLI
720
- *
721
- * @param resourceName - Resource name in kebab-case (e.g., 'product')
722
- * @param options - Generation options
723
- * @returns Complete test file content as string
724
- *
725
- * @example
726
- * const testContent = generateTestFile('product', {
727
- * presets: ['softDelete'],
728
- * modulePath: './modules/catalog',
729
- * });
730
- * fs.writeFileSync('product.test.js', testContent);
731
- */
732
- declare function generateTestFile(resourceName: string, options?: GenerateTestFileOptions): string;
733
- /**
734
- * Run config-level tests for a resource (no DB required)
735
- *
736
- * Tests field permissions, pipeline configuration, and event definitions.
737
- * Works with any adapter — no Mongoose dependency.
738
- *
739
- * @param resource - The Arc resource definition to test
740
- *
741
- * @example
742
- * ```typescript
743
- * import { createConfigTestSuite } from '@classytic/arc/testing';
744
- * import productResource from './product.resource.js';
745
- *
746
- * // Generates field permission, pipeline, and event tests
747
- * createConfigTestSuite(productResource);
748
- * ```
749
- */
750
- declare function createConfigTestSuite(resource: ResourceDefinition<unknown>): void;
751
- //#endregion
752
- //#region src/testing/testFactory.d.ts
753
- interface CreateTestAppOptions extends Partial<CreateAppOptions> {
754
- /**
755
- * Use in-memory MongoDB for faster tests (default: true)
756
- * Requires: mongodb-memory-server
757
- *
758
- * Set to false to use a provided mongoUri instead
759
- */
760
- useInMemoryDb?: boolean;
761
- /**
762
- * MongoDB connection URI (only used if useInMemoryDb is false)
763
- */
764
- mongoUri?: string;
765
- }
766
- interface TestAppResult {
767
- /** Fastify app instance */
551
+ interface TestAppContext {
768
552
  app: FastifyInstance;
553
+ /** Unified auth provider; `undefined` when `authMode: 'none'`. */
554
+ auth: TestAuthProvider | undefined;
555
+ /** Fixture tracker for record seeding. Always attached. */
556
+ fixtures: TestFixtures;
557
+ /** Connection URI — present when `db: 'in-memory'` or `{ uri }`. */
558
+ dbUri?: string;
769
559
  /**
770
- * Cleanup function to close app and disconnect database
771
- * Call this in afterAll() or afterEach()
560
+ * One cleanup for fixtures + app + Mongoose (if connected) + in-memory DB.
561
+ * Idempotent.
772
562
  */
773
- close: () => Promise<void>;
774
- /** MongoDB connection URI (useful for connecting models) */
775
- mongoUri?: string;
563
+ close(): Promise<void>;
776
564
  }
565
+ declare function createTestApp(options?: CreateTestAppOptions): Promise<TestAppContext>;
777
566
  /**
778
- * Create a test application instance with optional in-memory MongoDB
779
- *
780
- * **Performance Boost**: Uses in-memory MongoDB by default for 10x faster tests.
781
- *
782
- * @example Basic usage with in-memory DB
783
- * ```typescript
784
- * import { createTestApp } from '@classytic/arc/testing';
785
- *
786
- * describe('API Tests', () => {
787
- * let testApp: TestAppResult;
788
- *
789
- * beforeAll(async () => {
790
- * testApp = await createTestApp({
791
- * auth: { type: 'jwt', jwt: { secret: 'test-secret' } },
792
- * });
793
- * });
794
- *
795
- * afterAll(async () => {
796
- * await testApp.close(); // Cleans up DB and disconnects
797
- * });
798
- *
799
- * test('GET /health', async () => {
800
- * const response = await testApp.app.inject({
801
- * method: 'GET',
802
- * url: '/health',
803
- * });
804
- * expect(response.statusCode).toBe(200);
805
- * });
806
- * });
807
- * ```
808
- *
809
- * @example Using external MongoDB
810
- * ```typescript
811
- * const testApp = await createTestApp({
812
- * auth: { type: 'jwt', jwt: { secret: 'test-secret' } },
813
- * useInMemoryDb: false,
814
- * mongoUri: 'mongodb://localhost:27017/test-db',
815
- * });
816
- * ```
817
- *
818
- * @example Accessing MongoDB URI for model connections
819
- * ```typescript
820
- * const testApp = await createTestApp({
821
- * auth: { type: 'jwt', jwt: { secret: 'test-secret' } },
822
- * });
823
- * await mongoose.connect(testApp.mongoUri); // Connect your models
824
- * ```
825
- */
826
- declare function createTestApp(options?: CreateTestAppOptions): Promise<TestAppResult>;
827
- /**
828
- * Create a minimal Fastify instance for unit tests
829
- *
830
- * Use when you don't need Arc's full plugin stack
831
- *
832
- * @example
833
- * const app = createMinimalTestApp();
834
- * app.get('/test', async () => ({ success: true }));
835
- *
836
- * const response = await app.inject({ method: 'GET', url: '/test' });
837
- * expect(response.json()).toEqual({ success: true });
567
+ * Minimal Fastify instance no arc plugins, no auth, no db. Use when a test
568
+ * needs bare Fastify (e.g. plugin unit tests that manually register their
569
+ * dependencies).
838
570
  */
839
571
  declare function createMinimalTestApp(options?: FastifyServerOptions): FastifyInstance;
840
- /**
841
- * Test request builder for cleaner tests
842
- *
843
- * @example
844
- * const request = new TestRequestBuilder(app)
845
- * .get('/products')
846
- * .withAuth(mockUser)
847
- * .withQuery({ page: 1, limit: 10 });
848
- *
849
- * const response = await request.send();
850
- * expect(response.statusCode).toBe(200);
851
- */
852
- declare class TestRequestBuilder {
853
- private method;
854
- private url;
855
- private body?;
856
- private query?;
857
- private headers;
858
- private app;
859
- constructor(app: FastifyInstance);
860
- get(url: string): this;
861
- post(url: string): this;
862
- put(url: string): this;
863
- patch(url: string): this;
864
- delete(url: string): this;
865
- withBody(body: Record<string, unknown>): this;
866
- withQuery(query: Record<string, string | string[]>): this;
867
- withHeader(key: string, value: string): this;
868
- withAuth(userOrHeaders: Record<string, unknown>): this;
869
- withContentType(type: string): this;
870
- send(): Promise<Fastify.LightMyRequestResponse>;
871
- }
872
- /**
873
- * Helper to create a test request builder
874
- */
875
- declare function request(app: FastifyInstance): TestRequestBuilder;
876
- /**
877
- * Test helper for authentication
878
- */
879
- declare function createTestAuth(app: FastifyInstance): {
880
- /**
881
- * Generate a JWT token for testing
882
- */
883
- generateToken(user: Record<string, unknown>): string;
884
- /**
885
- * Decode a JWT token
886
- */
887
- decodeToken(token: string): Record<string, unknown> | null;
888
- /**
889
- * Verify a JWT token
890
- */
891
- verifyToken(token: string): Promise<Record<string, unknown>>;
892
- };
893
- /**
894
- * Snapshot testing helper for API responses
895
- */
896
- declare function createSnapshotMatcher(): {
897
- /**
898
- * Match response structure (ignores dynamic values like timestamps)
899
- */
900
- matchStructure(response: unknown, expected: unknown): boolean;
901
- };
902
- /**
903
- * Bulk test data loader
904
- */
905
- declare class TestDataLoader {
906
- private data;
907
- /**
908
- * Load test data into database
909
- */
910
- load(collection: string, items: Record<string, unknown>[]): Promise<Record<string, unknown>[]>;
911
- /**
912
- * Clear all loaded test data
913
- */
914
- cleanup(): Promise<void>;
915
- }
916
572
  //#endregion
917
- export { type AuthProvider, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type CreateTestAppOptions, DatabaseSnapshot, TestFixtures as DbTestFixtures, type GenerateTestFileOptions, HttpTestHarness, type HttpTestHarnessOptions, InMemoryDatabase, type OrgResponse, type SetupBetterAuthOrgOptions, type SetupUserConfig, type StorageContractSetup, type StorageContractSetupResult, type TestAppResult, TestDataLoader, TestDatabase, type TestFixtures$1 as TestFixtures, TestHarness, type TestHarnessOptions, type TestOrgContext, TestRequestBuilder, TestSeeder, TestTransaction, type TestUserContext, createBetterAuthProvider, createBetterAuthTestHelpers, createConfigTestSuite, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSnapshotMatcher, createSpy, createTestApp, createTestAuth, createTestHarness, createTestTimer, generateTestFile, preloadResources, preloadResourcesAsync, request, runStorageContract, safeParseBody, setupBetterAuthOrg, waitFor, withTestDb };
573
+ export { type ArcAssertion, type ArcResponseLike, type AuthMode, type AuthResponse, type BetterAuthTestHelpers, type BetterAuthTestHelpersOptions, type BetterAuthTestUser, type CreateOrgInput, type CreateTestAppOptions, type DbMode, type FixtureDestroyer, type FixtureFactory, type FixtureRegistration, HttpTestHarness, type HttpTestHarnessOptions, type MockRepository, type OrgResponse, type RoleConfig, type SetupBetterAuthTestAppInput, type SetupBetterAuthTestAppResult, type SignInInput, type SignUpInput, type StorageContractSetup, type StorageContractSetupResult, type TestAppContext, type TestAuthProvider, type TestAuthSession, type TestFixtures, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, preloadResources, preloadResourcesAsync, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };