@classytic/arc 2.10.8 → 2.11.1

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-P1d6rjPy.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 +2 -2
  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-C_bgx9o4.d.mts} +712 -500
  39. package/dist/{index-BziRPS4H.d.mts → index-CvM1e09j.d.mts} +29 -10
  40. package/dist/{index-EqQN6p0W.d.mts → index-pUczGjO0.d.mts} +11 -8
  41. package/dist/index-smCAoA5W.d.mts +1179 -0
  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-CPpkyKfM.mjs} +32 -8
  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-BdA4uMBV.d.mts} +191 -28
  97. package/dist/{types-CVKBssX5.d.mts → types-Bh_gEJBi.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 +124 -39
  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,183 +1,212 @@
1
- # Arc Testing Utilities
2
-
3
- In-memory MongoDB, test app creation, mocks, data factories, and test harness.
4
-
5
- ## createTestApp()
6
-
7
- Creates an isolated Fastify instance with in-memory MongoDB:
8
-
9
- ```bash
10
- npm install -D mongodb-memory-server
11
- ```
12
-
13
- ```typescript
14
- import { createTestApp } from '@classytic/arc/testing';
15
- import type { TestAppResult } from '@classytic/arc/testing';
16
-
17
- describe('API Tests', () => {
18
- let testApp: TestAppResult;
19
-
20
- beforeAll(async () => {
21
- testApp = await createTestApp({
22
- auth: { type: 'jwt', jwt: { secret: 'test-secret-32-chars-minimum-len' } },
23
- // All security plugins disabled by default in testing preset
24
- });
25
-
26
- // Connect models to in-memory DB
27
- await mongoose.connect(testApp.mongoUri);
28
- });
29
-
30
- afterAll(async () => {
31
- await testApp.close(); // Cleans up DB + closes app
32
- });
33
-
34
- test('GET /products', async () => {
35
- const response = await testApp.app.inject({
36
- method: 'GET',
37
- url: '/products',
38
- });
39
- expect(response.statusCode).toBe(200);
40
- expect(response.json().success).toBe(true);
41
- });
42
-
43
- test('POST /products (authenticated)', async () => {
44
- const token = testApp.app.jwt.sign({ _id: 'user-1', role: ['admin'] });
45
-
46
- const response = await testApp.app.inject({
47
- method: 'POST',
48
- url: '/products',
49
- headers: { authorization: `Bearer ${token}` },
50
- payload: { name: 'Test Product', price: 99 },
51
- });
52
- expect(response.statusCode).toBe(201);
53
- });
54
- });
55
- ```
56
-
57
- ### External MongoDB
58
-
59
- ```typescript
60
- const testApp = await createTestApp({
61
- auth: { type: 'jwt', jwt: { secret: 'test-secret-32-chars-minimum-len' } },
62
- useInMemoryDb: false,
63
- mongoUri: 'mongodb://localhost:27017/test-db',
64
- });
65
- ```
66
-
67
- ## TestHarness
68
-
69
- Full lifecycle test helper — setup, fixtures, assertions, teardown:
70
-
71
- ```typescript
72
- import { TestHarness } from '@classytic/arc/testing';
73
-
74
- const harness = new TestHarness({
75
- auth: { type: 'jwt', jwt: { secret: 'test-secret-32-chars-minimum-len' } },
76
- });
77
-
78
- describe('Product API', () => {
79
- beforeAll(() => harness.setup());
80
- afterAll(() => harness.teardown());
81
- afterEach(() => harness.cleanup()); // Clear collections between tests
82
-
83
- test('full CRUD', async () => {
84
- // Create
85
- const created = await harness.inject('POST', '/products', {
86
- body: { name: 'Widget', price: 10 },
87
- auth: { _id: 'user-1', role: ['admin'] },
88
- });
89
- expect(created.statusCode).toBe(201);
90
-
91
- // Read
92
- const fetched = await harness.inject('GET', `/products/${created.json().data._id}`);
93
- expect(fetched.json().data.name).toBe('Widget');
94
-
95
- // Update
96
- const updated = await harness.inject('PATCH', `/products/${created.json().data._id}`, {
97
- body: { price: 15 },
98
- auth: { _id: 'user-1', role: ['admin'] },
99
- });
100
- expect(updated.json().data.price).toBe(15);
101
-
102
- // Delete
103
- const deleted = await harness.inject('DELETE', `/products/${created.json().data._id}`, {
104
- auth: { _id: 'user-1', role: ['admin'] },
105
- });
106
- expect(deleted.statusCode).toBe(200);
107
- });
108
- });
109
- ```
110
-
111
- ## Mock Repository
112
-
113
- ```typescript
114
- import { createMockRepository } from '@classytic/arc/testing';
115
-
116
- const mockRepo = createMockRepository({
117
- findById: jest.fn().mockResolvedValue({ _id: '123', name: 'Test' }),
118
- findAll: jest.fn().mockResolvedValue({ docs: [], total: 0 }),
119
- create: jest.fn().mockResolvedValue({ _id: '123', name: 'New' }),
120
- update: jest.fn().mockResolvedValue({ _id: '123', name: 'Updated' }),
121
- delete: jest.fn().mockResolvedValue(true),
122
- });
123
-
124
- // Use in tests
125
- const controller = new ProductController(mockRepo);
126
- ```
127
-
128
- ## Data Factory
129
-
130
- Generate test fixtures:
131
-
132
- ```typescript
133
- import { createDataFactory } from '@classytic/arc/testing';
134
-
135
- const productFactory = createDataFactory({
136
- name: (i) => `Product ${i}`,
137
- price: (i) => 100 + i * 10,
138
- isActive: () => true,
139
- category: () => 'electronics',
140
- });
141
-
142
- const product = productFactory.build(); // { name: 'Product 1', price: 110, ... }
143
- const products = productFactory.buildMany(5); // 5 products
144
- const custom = productFactory.build({ price: 0 }); // Override specific fields
145
- ```
146
-
147
- ## Database Helpers
148
-
149
- ```typescript
150
- import { withTestDb } from '@classytic/arc/testing';
151
-
152
- describe('Repository', () => {
153
- withTestDb((db) => {
154
- // db.uri MongoDB connection string
155
- // db.cleanup() — Clear all collections
156
-
157
- test('create and find', async () => {
158
- await mongoose.connect(db.uri);
159
- const product = await Product.create({ name: 'Test' });
160
- expect(product.name).toBe('Test');
161
- });
162
- });
163
- });
164
- ```
165
-
166
- ## Testing Preset
167
-
168
- When using `createTestApp()` or `createApp({ preset: 'testing' })`:
169
-
170
- - Silent logging (no noise)
171
- - No CORS restrictions
172
- - Rate limiting disabled
173
- - Minimal security overhead
174
- - In-memory MongoDB (10x faster than external)
175
- - No health monitoring
176
-
177
- ## Tips
178
-
179
- 1. **Use `app.inject()`** — No real HTTP, fastest possible
180
- 2. **Issue tokens via `app.jwt.sign()`**Don't mock auth, test the real flow
181
- 3. **Use `afterEach` cleanup** — Clear collections between tests for isolation
182
- 4. **Use data factories** Consistent, reproducible test data
183
- 5. **Test permissions** — Verify 401/403 responses with wrong/missing tokens
1
+ # Arc Testing Utilities (2.11)
2
+
3
+ Three primary entry points pick by what you're testing. Everything else composes with one of them.
4
+
5
+ | Entry point | Use when | Tests in scope |
6
+ |---|---|---|
7
+ | `createHttpTestHarness(resource, ctxFn)` | You want auto-generated CRUD + permission + validation coverage per resource | ~16 tests / resource, zero boilerplate |
8
+ | `createTestApp({ resources, authMode, db })` | Custom scenarios, end-to-end flows, integration across resources | You write assertions with `expectArc(res)` |
9
+ | `runStorageContract(setup)` | You're building an adapter and want to prove it satisfies arc's Storage contract | DB-agnostic adapter conformance |
10
+
11
+ ---
12
+
13
+ ## `createTestApp()` — turnkey Fastify + in-memory Mongo + auth + fixtures
14
+
15
+ ```typescript
16
+ import { createTestApp, expectArc } from '@classytic/arc/testing';
17
+ import type { TestAppContext } from '@classytic/arc/testing';
18
+
19
+ describe('Product API', () => {
20
+ let ctx: TestAppContext;
21
+
22
+ beforeAll(async () => {
23
+ ctx = await createTestApp({
24
+ resources: [productResource],
25
+ authMode: 'jwt', // 'jwt' | 'better-auth' | 'none'
26
+ db: 'in-memory', // default; or { uri } | false
27
+ connectMongoose: true, // optional — one-liner for Mongoose apps
28
+ });
29
+
30
+ ctx.auth.register('admin', {
31
+ user: { id: '1', roles: ['admin'] },
32
+ orgId: 'org-1',
33
+ });
34
+ });
35
+
36
+ afterAll(() => ctx.close());
37
+
38
+ it('GET /products — public', async () => {
39
+ const res = await ctx.app.inject({ method: 'GET', url: '/products' });
40
+ expectArc(res).ok().paginated();
41
+ });
42
+
43
+ it('POST /products — admin required', async () => {
44
+ const res = await ctx.app.inject({
45
+ method: 'POST',
46
+ url: '/products',
47
+ headers: ctx.auth.as('admin').headers,
48
+ payload: { name: 'Widget', price: 99 },
49
+ });
50
+ expectArc(res).ok();
51
+ });
52
+ });
53
+ ```
54
+
55
+ **`TestAppContext` shape**: `{ app, auth, fixtures, dbUri, close }`. Auth is `undefined` when `authMode: 'none'`; `dbUri` is present when `db: 'in-memory'` or `{ uri }`.
56
+
57
+ **`db` modes**:
58
+ - `'in-memory'` (default) — boots `MongoMemoryServer`, exposes `dbUri`, stops on `close()`. Needs `npm i -D mongodb-memory-server`.
59
+ - `{ uri }` — external Mongo URI; caller owns lifecycle.
60
+ - `false` no DB wiring (pure Fastify unit tests).
61
+
62
+ **`authMode: 'better-auth'`** requires the caller to also pass `auth: { type: 'better-auth', ... }`. Mismatched config fails fast at setup.
63
+
64
+ ---
65
+
66
+ ## `TestAuthProvider` — unified session primitive
67
+
68
+ One `register()` → `.as(role).headers` flow across JWT, Better Auth, and custom providers.
69
+
70
+ ```typescript
71
+ // JWT — provider signs on-the-fly via app.jwt.sign()
72
+ ctx.auth.register('admin', { user: { id: '1', roles: ['admin'] }, orgId: 'org-1' });
73
+ ctx.auth.register('user', { user: { id: '2', roles: ['user'] } });
74
+
75
+ // Better Auth / custom pre-signed tokens
76
+ ctx.auth.register('admin', { token: existingToken, orgId: 'org-1' });
77
+
78
+ // Use
79
+ const headers = ctx.auth.as('admin').headers; // { authorization, x-organization-id }
80
+ const withExtra = ctx.auth.as('admin').withExtra({ 'x-request-id': 'r-1' }).headers;
81
+ ```
82
+
83
+ Directly construct without `createTestApp`:
84
+
85
+ ```typescript
86
+ import { createJwtAuthProvider, createBetterAuthProvider } from '@classytic/arc/testing';
87
+ const auth = createJwtAuthProvider(app, { defaultOrgId: 'org-1' });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## `createHttpTestHarness(resource, ctxFn)` — auto-generated resource coverage
93
+
94
+ ~16 tests per resource (CRUD + permission + validation + error envelope) from a single factory call. Reads `defineResource()` config and probes every route.
95
+
96
+ ```typescript
97
+ import { createTestApp, createHttpTestHarness } from '@classytic/arc/testing';
98
+
99
+ describe('Product resource — full coverage', () => {
100
+ const ctx = await createTestApp({ resources: [productResource] });
101
+ ctx.auth.register('admin', { user: { id: '1', roles: ['admin'] } });
102
+
103
+ createHttpTestHarness(productResource, () => ({
104
+ app: ctx.app,
105
+ auth: ctx.auth,
106
+ adminRole: 'admin',
107
+ fixtures: { product: (i) => ({ name: `P${i}`, price: 10 + i }) },
108
+ })).runAll();
109
+ });
110
+ ```
111
+
112
+ ---
113
+
114
+ ## `expectArc(response)` fluent envelope matchers
115
+
116
+ ```typescript
117
+ expectArc(res).ok(); // 200/201, success: true
118
+ expectArc(res).forbidden(); // 403, arc error envelope
119
+ expectArc(res).notFound().hasError(/not exist/);
120
+ expectArc(res).validationError().hasData({ fields: ['email'] });
121
+ expectArc(res).paginated({ total: 10 }); // meta.pagination present
122
+ expectArc(res).hidesField('password'); // field stripped from response
123
+ expectArc(res).hasMeta('traceId');
124
+ ```
125
+
126
+ Available: `.ok`, `.failed`, `.unauthorized`, `.forbidden`, `.notFound`, `.validationError`, `.conflict`, `.hasData`, `.hidesField`, `.showsField`, `.paginated`, `.hasError`, `.hasMeta`.
127
+
128
+ ---
129
+
130
+ ## `createTestFixtures()` — DB-agnostic seeding
131
+
132
+ ```typescript
133
+ import { createTestFixtures } from '@classytic/arc/testing';
134
+
135
+ const fixtures = createTestFixtures();
136
+ fixtures.register('product', async (data) => {
137
+ const doc = await Product.create(data);
138
+ return { record: doc, destroy: () => Product.deleteOne({ _id: doc._id }) };
139
+ });
140
+
141
+ const widget = await fixtures.create('product', { name: 'Widget' });
142
+ await fixtures.clear(); // runs destroyers newest-first
143
+ ```
144
+
145
+ Destroyers bind at create time — no global cleanup registry. Works with any backend (Mongoose, sqlitekit, Prisma, in-memory).
146
+
147
+ ---
148
+
149
+ ## Better Auth orchestration — `setupBetterAuthTestApp`
150
+
151
+ Composes `createTestApp` with Better Auth sign-up + org creation. Use when you need real Better Auth tokens rather than pre-signed stubs:
152
+
153
+ ```typescript
154
+ import { setupBetterAuthTestApp, createBetterAuthTestHelpers } from '@classytic/arc/testing';
155
+
156
+ const { ctx, helpers } = await setupBetterAuthTestApp({ resources: [orderResource], auth });
157
+ const { user, token, orgId } = await helpers.signUpWithOrg({ email: 'a@x.co', name: 'A' });
158
+ ctx.auth.register('admin', { token, orgId });
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Mocks (non-Fastify unit tests)
164
+
165
+ ```typescript
166
+ import {
167
+ createMockRepository,
168
+ createDataFactory,
169
+ createMockUser,
170
+ createMockRequest,
171
+ createMockReply,
172
+ waitFor,
173
+ createSpy,
174
+ createTestTimer,
175
+ } from '@classytic/arc/testing';
176
+ ```
177
+
178
+ ---
179
+
180
+ ## `runStorageContract(setup)`adapter conformance
181
+
182
+ DB-agnostic Storage contract check. Build a setup that returns your adapter factory + a cleanup fn; arc runs the full contract suite against it.
183
+
184
+ ```typescript
185
+ import { runStorageContract } from '@classytic/arc/testing';
186
+ runStorageContract(async () => {
187
+ const storage = createMyAdapter();
188
+ return { storage, cleanup: async () => storage.close() };
189
+ });
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Tips
195
+
196
+ 1. **`app.inject()` over real HTTP** — same server, zero network.
197
+ 2. **Register auth sessions once per role** — not per test.
198
+ 3. **Use `ctx.fixtures.clear()` in `afterEach`** — destroyers handle dependency order.
199
+ 4. **Test permission denials explicitly** — `expectArc(res).forbidden()` beats status-code assertions.
200
+ 5. **Reach for `createHttpTestHarness` first** — 16 tests for one function call.
201
+
202
+ ## Migration from pre-2.11 testing APIs
203
+
204
+ | Pre-2.11 | 2.11 |
205
+ |---|---|
206
+ | `TestHarness` / `createTestHarness` | `createHttpTestHarness(resource, ctxFn)` |
207
+ | `TestAppResult` | `TestAppContext` |
208
+ | `testApp.mongoUri` | `ctx.dbUri` |
209
+ | `createJwtAuthProvider` / `createBetterAuthProvider` (as `HttpTestHarness` imports) | `ctx.auth` from `createTestApp` (or direct factory import) |
210
+ | `withTestDb()` | `createTestApp({ db: 'in-memory' })` + `ctx.dbUri` |
211
+ | `TestDatabase` / `TestSeeder` / `TestTransaction` | `createTestFixtures` + kit-level cleanup |
212
+ | `setupBetterAuthOrg` | `setupBetterAuthTestApp` + `helpers.signUpWithOrg` |
@@ -1,37 +0,0 @@
1
- //#region src/permissions/applyPermissionResult.ts
2
- /**
3
- * Normalize a permission check return value (`boolean | PermissionResult`)
4
- * into a concrete `PermissionResult`. This is the only place in Arc that
5
- * promotes booleans to results — keeps the type narrowing honest everywhere.
6
- */
7
- function normalizePermissionResult(result) {
8
- if (typeof result === "boolean") return { granted: result };
9
- return result;
10
- }
11
- /**
12
- * Apply a granted `PermissionResult` to a Fastify request — merges row-level
13
- * filters into `_policyFilters` and conditionally installs the scope.
14
- *
15
- * **Scope install rule:** only writes `scope` when the current request scope
16
- * is absent or `public`. This prevents downgrading an already-authenticated
17
- * request (e.g. Better Auth set `member`, then a permission check returns a
18
- * narrower `service` scope — the original `member` wins because it came from
19
- * a more authoritative source).
20
- *
21
- * Safe to call with a non-granted result — it simply no-ops. Callers should
22
- * still check `result.granted` and send an error response before reaching here,
23
- * but this function tolerates the misuse defensively.
24
- */
25
- function applyPermissionResult(result, request) {
26
- if (!result.granted) return;
27
- if (result.filters) request._policyFilters = {
28
- ...request._policyFilters ?? {},
29
- ...result.filters
30
- };
31
- if (result.scope) {
32
- const current = request.scope;
33
- if (!current || current.kind === "public") request.scope = result.scope;
34
- }
35
- }
36
- //#endregion
37
- export { normalizePermissionResult as n, applyPermissionResult as t };