@classytic/arc 2.10.3 → 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.
- package/README.md +1 -1
- package/dist/{BaseController-CbKKIflT.mjs → BaseController-JNV08qOT.mjs} +595 -537
- package/dist/{queryCachePlugin-BKbWjgDG.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
- package/dist/actionPermissions-C8YYU92K.mjs +22 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +15 -17
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +3 -3
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +37 -27
- package/dist/cli/commands/init.mjs +47 -34
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.d.mts +58 -0
- package/dist/context/index.mjs +2 -0
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -3
- package/dist/core-DXdSSFW-.mjs +1037 -0
- package/dist/createActionRouter-BwaSM0No.mjs +166 -0
- package/dist/{createApp-BuvPma24.mjs → createApp-DvNYEhpb.mjs} +118 -36
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-C7hgL_aI.mjs → elevation-DOFoxoDs.mjs} +1 -1
- package/dist/errorHandler-Co3lnVmJ.d.mts +114 -0
- package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
- package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-CUNjYYRY.d.mts} +1 -1
- package/dist/events/index.d.mts +4 -4
- package/dist/events/index.mjs +69 -51
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/index.mjs +38 -27
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-ChIw3776.d.mts → index-BYCqHCVu.d.mts} +4 -4
- package/dist/{index-Cl0uoKd5.d.mts → index-Cm0vUrr_.d.mts} +2100 -1688
- package/dist/{index-DStwgFUK.d.mts → index-DAushRTt.d.mts} +29 -10
- package/dist/index-DsJ1MNfC.d.mts +1179 -0
- package/dist/{index-8qw4y6ff.d.mts → index-t8pLpPFW.d.mts} +13 -10
- package/dist/index.d.mts +7 -251
- package/dist/index.mjs +8 -128
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +46 -5
- package/dist/integrations/streamline.mjs +50 -21
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +2 -154
- package/dist/integrations/websocket.mjs +292 -224
- package/dist/{keys-qcD-TVJl.mjs → keys-CARyUjiR.mjs} +2 -0
- package/dist/{loadResources-BAzJItAJ.mjs → loadResources-YNwKHvRA.mjs} +3 -1
- package/dist/logger/index.d.mts +81 -0
- package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
- package/dist/middleware/index.d.mts +109 -0
- package/dist/middleware/index.mjs +70 -0
- package/dist/multipartBody-CvTR1Un6.mjs +123 -0
- package/dist/{openapi-B5F8AddX.mjs → openapi-C0L9ar7m.mjs} +9 -7
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +1 -3
- package/dist/{permissions-Dk6mshja.mjs → permissions-B4vU9L0Q.mjs} +220 -2
- package/dist/pipe-DVoIheVC.mjs +62 -0
- package/dist/pipeline/index.d.mts +62 -0
- package/dist/pipeline/index.mjs +53 -0
- package/dist/plugins/index.d.mts +25 -5
- package/dist/plugins/index.mjs +10 -10
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +42 -24
- package/dist/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +255 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +48 -8
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-fLJVXdVn.mjs → presets-k604Lj99.mjs} +1 -1
- package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
- package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
- package/dist/{redis-DqyeggCa.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
- package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-CM8TXTix.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{requestContext-xHIKedG6.mjs → requestContext-CfRkaxwf.mjs} +1 -1
- package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools--okX6QBr.mjs} +534 -415
- package/dist/routerShared-DeESFp4a.mjs +515 -0
- package/dist/schemaIR-BlG9bY7v.mjs +137 -0
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +1 -1
- package/dist/{sse-yBCgOLGu.mjs → sse-V7aXc3bW.mjs} +1 -1
- package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-BhrzxvyQ.mjs} +4 -0
- package/dist/testing/index.d.mts +367 -711
- package/dist/testing/index.mjs +646 -1434
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/{tracing-65B51Dw3.d.mts → tracing-DokiEsuz.d.mts} +9 -4
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -3
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-Co8k3NyS.d.mts → types-CgikqKAj.d.mts} +133 -21
- package/dist/{types-Btdda02s.d.mts → types-D9NqiYIw.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -898
- package/dist/utils/index.mjs +4 -5
- package/dist/utils-D3Yxnrwr.mjs +1639 -0
- package/dist/versioning-M9lNLhO8.d.mts +117 -0
- package/dist/websocket-CyJ1VIFI.d.mts +186 -0
- package/package.json +26 -8
- package/skills/arc/SKILL.md +124 -39
- package/skills/arc/references/testing.md +212 -183
- package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
- package/dist/core-CcR01lup.mjs +0 -1411
- package/dist/createActionRouter-Bp_5c_2b.mjs +0 -249
- package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
- package/dist/errors-CCSsMpXE.d.mts +0 -140
- package/dist/fields-bxkeltzz.mjs +0 -126
- package/dist/filesUpload-t21LS-py.mjs +0 -377
- package/dist/queryParser-DBqBB6AC.mjs +0 -352
- package/dist/types-Csi3FLfq.mjs +0 -27
- package/dist/utils-B2fNOD_i.mjs +0 -929
- /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
- /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
- /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
- /package/dist/{caching-CBpK_SCM.mjs → caching-CheW3m-S.mjs} +0 -0
- /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
- /package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-BQm8ZxTK.mjs} +0 -0
- /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
- /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
- /package/dist/{interface-D218ikEo.d.mts → interface-Da0r7Lna.d.mts} +0 -0
- /package/dist/{memory-B5Amv9A1.mjs → memory-DikHSvWa.mjs} +0 -0
- /package/dist/{metrics-DuhiSEZI.mjs → metrics-Csh4nsvv.mjs} +0 -0
- /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-BneOJkpi.mjs} +0 -0
- /package/dist/{registry-B3lRFBWo.mjs → registry-D63ee7fl.mjs} +0 -0
- /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
- /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
- /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
- /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
- /package/dist/{versioning-C2U_bLY0.mjs → versioning-CGPjkqAg.mjs} +0 -0
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,477 +1,373 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as ResourceLike, r as CreateAppOptions } from "../types-
|
|
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
|
|
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/
|
|
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
|
|
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
|
-
|
|
18
|
-
body:
|
|
180
|
+
userId: string;
|
|
181
|
+
body: unknown;
|
|
19
182
|
}
|
|
20
183
|
interface OrgResponse {
|
|
21
184
|
statusCode: number;
|
|
22
185
|
orgId: string;
|
|
23
|
-
body:
|
|
186
|
+
body: unknown;
|
|
24
187
|
}
|
|
25
188
|
interface BetterAuthTestHelpers {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
org:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
82
|
-
|
|
228
|
+
addMember?: (data: {
|
|
229
|
+
app: FastifyInstance;
|
|
230
|
+
creatorToken: string;
|
|
231
|
+
orgId: string;
|
|
83
232
|
userId: string;
|
|
84
233
|
role: string;
|
|
85
|
-
}) => Promise<
|
|
86
|
-
|
|
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
|
-
*
|
|
90
|
-
*
|
|
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
|
-
|
|
93
|
-
/**
|
|
94
|
-
|
|
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
|
-
*
|
|
98
|
-
*
|
|
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):
|
|
272
|
+
declare function safeParseBody<T = unknown>(body: string | undefined): T | null;
|
|
101
273
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
332
|
+
/** Fastify app (must be ready). */
|
|
386
333
|
app: FastifyInstance;
|
|
387
|
-
/**
|
|
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
|
-
|
|
390
|
-
update?: Partial<T>;
|
|
340
|
+
valid: Partial<T>;
|
|
341
|
+
update?: Partial<T>;
|
|
391
342
|
invalid?: Partial<T>;
|
|
392
343
|
};
|
|
393
|
-
/**
|
|
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
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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
|
|
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/
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
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
|
-
|
|
521
|
+
db?: DbMode;
|
|
658
522
|
/**
|
|
659
|
-
*
|
|
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
|
-
*
|
|
662
|
-
*
|
|
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
|
-
|
|
530
|
+
connectMongoose?: boolean;
|
|
668
531
|
/**
|
|
669
|
-
*
|
|
532
|
+
* Auth mode attached to `ctx.auth` (and, for `'jwt'`, the default auth
|
|
533
|
+
* plugin on the app):
|
|
670
534
|
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* -
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
*
|
|
771
|
-
*
|
|
560
|
+
* One cleanup for fixtures + app + Mongoose (if connected) + in-memory DB.
|
|
561
|
+
* Idempotent.
|
|
772
562
|
*/
|
|
773
|
-
close
|
|
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
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
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
|
|
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 };
|