@classytic/arc 2.25.0 → 2.30.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 +2 -0
- package/dist/{BaseController--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
- package/dist/EventTransport-C6YA8xY9.mjs +103 -0
- package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
- package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
- package/dist/audit/index.d.mts +2 -2
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +1 -1
- package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
- package/dist/cleanup/index.d.mts +313 -0
- package/dist/cleanup/index.mjs +1267 -0
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +5 -3
- package/dist/core-poalJLvp.mjs +175 -0
- package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
- package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
- package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
- package/dist/defineAggregation-ufWjUwml.mjs +47 -0
- package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
- package/dist/events/index.d.mts +28 -2
- package/dist/events/index.mjs +24 -343
- package/dist/factory/index.d.mts +4 -3
- package/dist/factory/index.mjs +3 -1
- package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
- package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/index-CCzVR-N3.d.mts +107 -0
- package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
- package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
- package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -6
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +6 -6
- 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/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
- package/dist/middleware/index.d.mts +1 -1
- package/dist/migrations/index.d.mts +0 -3
- package/dist/migrations/index.mjs +0 -3
- package/dist/module-_-M7C5ES.mjs +29 -0
- package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
- package/dist/plugins/index.d.mts +4 -4
- package/dist/plugins/index.mjs +2 -2
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
- package/dist/preloadResources-DSY5HGpS.mjs +45 -0
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +14 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
- package/dist/projection-UBDgfMwu.mjs +17 -0
- package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
- package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
- package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
- package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
- package/dist/testing/cleanupStoreContract.d.mts +10 -0
- package/dist/testing/cleanupStoreContract.mjs +375 -0
- package/dist/testing/index.d.mts +34 -5
- package/dist/testing/index.mjs +316 -211
- package/dist/testing/outboxStoreContract.d.mts +13 -0
- package/dist/testing/outboxStoreContract.mjs +247 -0
- package/dist/types/index.d.mts +3 -3
- package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
- package/dist/types-C_ZZRcD6.d.mts +450 -0
- package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
- package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
- package/dist/usage/index.d.mts +1 -1
- package/dist/usage/index.mjs +0 -0
- package/dist/utils/index.d.mts +2 -5
- package/package.json +539 -524
package/dist/testing/index.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { t as BaseController } from "../BaseController-B5DwufbB.mjs";
|
|
3
|
+
import { t as defineResource } from "../defineResource-CxWqDCAy.mjs";
|
|
4
|
+
import { t as defineAggregation } from "../defineAggregation-ufWjUwml.mjs";
|
|
5
|
+
import { n as createApp } from "../createApp-CXrKFciH.mjs";
|
|
6
|
+
import { n as getModuleExports } from "../module-_-M7C5ES.mjs";
|
|
7
|
+
import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-DSY5HGpS.mjs";
|
|
3
8
|
import { runStorageContract } from "./storageContract.mjs";
|
|
4
9
|
import Fastify from "fastify";
|
|
5
10
|
import { afterAll, describe, expect, it, vi } from "vitest";
|
|
@@ -123,6 +128,314 @@ function expectArc(response) {
|
|
|
123
128
|
return assertion;
|
|
124
129
|
}
|
|
125
130
|
//#endregion
|
|
131
|
+
//#region src/testing/mocks.ts
|
|
132
|
+
/**
|
|
133
|
+
* Create a mock repository for testing
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const mockRepo = createMockRepository<Product>({
|
|
137
|
+
* getById: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
|
|
138
|
+
* create: vi.fn().mockImplementation(data => Promise.resolve({ id: '1', ...data })),
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* await mockRepo.getById('1'); // Returns mocked product
|
|
142
|
+
*/
|
|
143
|
+
function createMockRepository(overrides = {}) {
|
|
144
|
+
return {
|
|
145
|
+
getAll: vi.fn().mockResolvedValue({
|
|
146
|
+
method: "offset",
|
|
147
|
+
data: [],
|
|
148
|
+
total: 0,
|
|
149
|
+
page: 1,
|
|
150
|
+
limit: 20,
|
|
151
|
+
pages: 0,
|
|
152
|
+
hasNext: false,
|
|
153
|
+
hasPrev: false
|
|
154
|
+
}),
|
|
155
|
+
getById: vi.fn().mockResolvedValue(null),
|
|
156
|
+
create: vi.fn().mockImplementation((data) => Promise.resolve({
|
|
157
|
+
_id: "mock-id",
|
|
158
|
+
...data
|
|
159
|
+
})),
|
|
160
|
+
update: vi.fn().mockImplementation((_id, data) => Promise.resolve({
|
|
161
|
+
_id: "mock-id",
|
|
162
|
+
...data
|
|
163
|
+
})),
|
|
164
|
+
delete: vi.fn().mockResolvedValue({
|
|
165
|
+
success: true,
|
|
166
|
+
message: "Deleted"
|
|
167
|
+
}),
|
|
168
|
+
updateMany: vi.fn().mockResolvedValue({
|
|
169
|
+
acknowledged: true,
|
|
170
|
+
matchedCount: 0,
|
|
171
|
+
modifiedCount: 0
|
|
172
|
+
}),
|
|
173
|
+
deleteMany: vi.fn().mockResolvedValue({
|
|
174
|
+
acknowledged: true,
|
|
175
|
+
deletedCount: 0
|
|
176
|
+
}),
|
|
177
|
+
claim: vi.fn().mockResolvedValue(null),
|
|
178
|
+
claimVersion: vi.fn().mockResolvedValue(null),
|
|
179
|
+
capabilities: {
|
|
180
|
+
transactions: false,
|
|
181
|
+
nestedTransactions: false,
|
|
182
|
+
upsert: false,
|
|
183
|
+
duplicateKeyError: false,
|
|
184
|
+
distinct: false,
|
|
185
|
+
aggregate: false,
|
|
186
|
+
getOrCreate: false,
|
|
187
|
+
countAndExists: false
|
|
188
|
+
},
|
|
189
|
+
getBySlug: vi.fn().mockResolvedValue(null),
|
|
190
|
+
getDeleted: vi.fn().mockResolvedValue([]),
|
|
191
|
+
restore: vi.fn().mockResolvedValue(null),
|
|
192
|
+
getTree: vi.fn().mockResolvedValue([]),
|
|
193
|
+
getChildren: vi.fn().mockResolvedValue([]),
|
|
194
|
+
...overrides
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Create a mock user for authentication testing
|
|
199
|
+
*/
|
|
200
|
+
function createMockUser(overrides = {}) {
|
|
201
|
+
return {
|
|
202
|
+
_id: "mock-user-id",
|
|
203
|
+
id: "mock-user-id",
|
|
204
|
+
email: "test@example.com",
|
|
205
|
+
roles: ["user"],
|
|
206
|
+
organizationId: null,
|
|
207
|
+
...overrides
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Create a mock Fastify request
|
|
212
|
+
*/
|
|
213
|
+
function createMockRequest(overrides = {}) {
|
|
214
|
+
return {
|
|
215
|
+
body: {},
|
|
216
|
+
params: {},
|
|
217
|
+
query: {},
|
|
218
|
+
headers: {},
|
|
219
|
+
user: createMockUser(),
|
|
220
|
+
context: {},
|
|
221
|
+
log: {
|
|
222
|
+
info: vi.fn(),
|
|
223
|
+
warn: vi.fn(),
|
|
224
|
+
error: vi.fn(),
|
|
225
|
+
debug: vi.fn()
|
|
226
|
+
},
|
|
227
|
+
...overrides
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Create a mock Fastify reply
|
|
232
|
+
*/
|
|
233
|
+
function createMockReply() {
|
|
234
|
+
return {
|
|
235
|
+
code: vi.fn().mockReturnThis(),
|
|
236
|
+
send: vi.fn().mockReturnThis(),
|
|
237
|
+
header: vi.fn().mockReturnThis(),
|
|
238
|
+
headers: vi.fn().mockReturnThis(),
|
|
239
|
+
status: vi.fn().mockReturnThis(),
|
|
240
|
+
type: vi.fn().mockReturnThis(),
|
|
241
|
+
redirect: vi.fn().mockReturnThis(),
|
|
242
|
+
callNotFound: vi.fn().mockReturnThis(),
|
|
243
|
+
sent: false
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Create a mock controller for testing
|
|
248
|
+
*/
|
|
249
|
+
function createMockController(repository) {
|
|
250
|
+
return {
|
|
251
|
+
repository,
|
|
252
|
+
list: vi.fn(),
|
|
253
|
+
get: vi.fn(),
|
|
254
|
+
create: vi.fn(),
|
|
255
|
+
update: vi.fn(),
|
|
256
|
+
delete: vi.fn()
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Create mock data factory
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* const productFactory = createDataFactory<Product>({
|
|
264
|
+
* name: () => faker.commerce.productName(),
|
|
265
|
+
* price: () => faker.number.int({ min: 10, max: 1000 }),
|
|
266
|
+
* sku: (i) => `SKU-${i}`,
|
|
267
|
+
* });
|
|
268
|
+
*
|
|
269
|
+
* const product = productFactory.build();
|
|
270
|
+
* const products = productFactory.buildMany(10);
|
|
271
|
+
*/
|
|
272
|
+
function createDataFactory(template) {
|
|
273
|
+
let counter = 0;
|
|
274
|
+
return {
|
|
275
|
+
build(overrides = {}) {
|
|
276
|
+
const index = counter++;
|
|
277
|
+
const data = {};
|
|
278
|
+
for (const [key, generator] of Object.entries(template)) data[key] = generator(index);
|
|
279
|
+
return {
|
|
280
|
+
...data,
|
|
281
|
+
...overrides
|
|
282
|
+
};
|
|
283
|
+
},
|
|
284
|
+
buildMany(count, overrides = {}) {
|
|
285
|
+
return Array.from({ length: count }, () => this.build(overrides));
|
|
286
|
+
},
|
|
287
|
+
reset() {
|
|
288
|
+
counter = 0;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Create a spy that tracks function calls
|
|
294
|
+
*
|
|
295
|
+
* Useful for testing side effects without full mocking
|
|
296
|
+
*/
|
|
297
|
+
function createSpy(_name = "spy") {
|
|
298
|
+
const calls = [];
|
|
299
|
+
const spy = vi.fn((...args) => {
|
|
300
|
+
calls.push(args);
|
|
301
|
+
});
|
|
302
|
+
spy.getCalls = () => calls;
|
|
303
|
+
spy.getLastCall = () => calls[calls.length - 1] || [];
|
|
304
|
+
return spy;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Wait for a condition to be true
|
|
308
|
+
*
|
|
309
|
+
* Useful for async testing
|
|
310
|
+
*/
|
|
311
|
+
async function waitFor(condition, options = {}) {
|
|
312
|
+
const { timeout = 5e3, interval = 100 } = options;
|
|
313
|
+
const startTime = Date.now();
|
|
314
|
+
while (Date.now() - startTime < timeout) {
|
|
315
|
+
if (await condition()) return;
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Create a test timer that can be controlled
|
|
322
|
+
*/
|
|
323
|
+
function createTestTimer() {
|
|
324
|
+
let time = Date.now();
|
|
325
|
+
return {
|
|
326
|
+
now: () => time,
|
|
327
|
+
advance: (ms) => {
|
|
328
|
+
time += ms;
|
|
329
|
+
},
|
|
330
|
+
set: (timestamp) => {
|
|
331
|
+
time = timestamp;
|
|
332
|
+
},
|
|
333
|
+
reset: () => {
|
|
334
|
+
time = Date.now();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/testing/authorizationConformance.ts
|
|
340
|
+
/**
|
|
341
|
+
* Cross-surface authorization conformance (arc 2.30).
|
|
342
|
+
*
|
|
343
|
+
* The regression wall that keeps "partial enforcement" from ever coming back:
|
|
344
|
+
* run the SAME permission through more than one arc surface and prove it enforces
|
|
345
|
+
* identically. Reviews kept finding aggregation (and MCP) drifting from CRUD —
|
|
346
|
+
* this makes that a one-call assertion for arc AND for host resources.
|
|
347
|
+
*
|
|
348
|
+
* `runAuthorizationConformance` boots a throwaway Fastify app with one resource
|
|
349
|
+
* whose `list` op AND an `aggregation` share the given permission, backed by a
|
|
350
|
+
* capturing mock repo (no DB). It returns, per surface, the HTTP status and the
|
|
351
|
+
* exact filter arc handed the repository — so the caller asserts:
|
|
352
|
+
* - deny → both surfaces fail closed with the SAME status;
|
|
353
|
+
* - allow + `policy` → the row policy reaches BOTH the list query and the
|
|
354
|
+
* aggregation `$match`, identically.
|
|
355
|
+
*
|
|
356
|
+
* Runner-agnostic: it returns data, it does not assert. Pair with your test
|
|
357
|
+
* framework's expectations (see `tests/core/authorization-conformance.test.ts`).
|
|
358
|
+
*/
|
|
359
|
+
/**
|
|
360
|
+
* Boot a one-resource app gated by `permission` on both list + an aggregation,
|
|
361
|
+
* hit both surfaces anonymously, and report what arc enforced on each.
|
|
362
|
+
*/
|
|
363
|
+
async function runAuthorizationConformance(opts) {
|
|
364
|
+
const rows = opts.rows ?? [{ _id: "1" }];
|
|
365
|
+
let listFilter;
|
|
366
|
+
let aggFilter;
|
|
367
|
+
const repo = createMockRepository({
|
|
368
|
+
getAll: (async (params = {}) => {
|
|
369
|
+
listFilter = params.filters;
|
|
370
|
+
return {
|
|
371
|
+
method: "offset",
|
|
372
|
+
data: rows,
|
|
373
|
+
total: rows.length,
|
|
374
|
+
page: 1,
|
|
375
|
+
limit: 20,
|
|
376
|
+
pages: 1,
|
|
377
|
+
hasNext: false,
|
|
378
|
+
hasPrev: false
|
|
379
|
+
};
|
|
380
|
+
}),
|
|
381
|
+
aggregate: (async (req = {}) => {
|
|
382
|
+
aggFilter = req.filter;
|
|
383
|
+
return { rows: [{ tally: rows.length }] };
|
|
384
|
+
}),
|
|
385
|
+
capabilities: {
|
|
386
|
+
count: false,
|
|
387
|
+
exists: false,
|
|
388
|
+
distinct: false,
|
|
389
|
+
aggregate: true,
|
|
390
|
+
transactions: false,
|
|
391
|
+
lookup: false,
|
|
392
|
+
softDelete: false
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
const resource = defineResource({
|
|
396
|
+
name: "conf",
|
|
397
|
+
prefix: "/confs",
|
|
398
|
+
adapter: { repository: repo },
|
|
399
|
+
controller: new BaseController(repo, {
|
|
400
|
+
resourceName: "conf",
|
|
401
|
+
tenantField: false
|
|
402
|
+
}),
|
|
403
|
+
permissions: {
|
|
404
|
+
list: opts.permission,
|
|
405
|
+
get: opts.permission,
|
|
406
|
+
create: opts.permission,
|
|
407
|
+
update: opts.permission,
|
|
408
|
+
delete: opts.permission
|
|
409
|
+
},
|
|
410
|
+
aggregations: { tally: defineAggregation({
|
|
411
|
+
measures: { tally: { op: "count" } },
|
|
412
|
+
permissions: opts.permission
|
|
413
|
+
}) }
|
|
414
|
+
});
|
|
415
|
+
const app = Fastify({ logger: false });
|
|
416
|
+
await app.register(resource.toPlugin());
|
|
417
|
+
await app.ready();
|
|
418
|
+
const list = await app.inject({
|
|
419
|
+
method: "GET",
|
|
420
|
+
url: "/confs"
|
|
421
|
+
});
|
|
422
|
+
const aggregation = await app.inject({
|
|
423
|
+
method: "GET",
|
|
424
|
+
url: "/confs/aggregations/tally"
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
list: {
|
|
428
|
+
status: list.statusCode,
|
|
429
|
+
filter: listFilter
|
|
430
|
+
},
|
|
431
|
+
aggregation: {
|
|
432
|
+
status: aggregation.statusCode,
|
|
433
|
+
filter: aggFilter
|
|
434
|
+
},
|
|
435
|
+
close: () => app.close()
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
126
439
|
//#region src/testing/authSession.ts
|
|
127
440
|
function buildHeaders(token, orgId, extra) {
|
|
128
441
|
const headers = { authorization: `Bearer ${token}` };
|
|
@@ -843,214 +1156,6 @@ function createHttpTestHarness(resource, optionsOrGetter) {
|
|
|
843
1156
|
return new HttpTestHarness(resource, optionsOrGetter);
|
|
844
1157
|
}
|
|
845
1158
|
//#endregion
|
|
846
|
-
//#region src/testing/mocks.ts
|
|
847
|
-
/**
|
|
848
|
-
* Create a mock repository for testing
|
|
849
|
-
*
|
|
850
|
-
* @example
|
|
851
|
-
* const mockRepo = createMockRepository<Product>({
|
|
852
|
-
* getById: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }),
|
|
853
|
-
* create: vi.fn().mockImplementation(data => Promise.resolve({ id: '1', ...data })),
|
|
854
|
-
* });
|
|
855
|
-
*
|
|
856
|
-
* await mockRepo.getById('1'); // Returns mocked product
|
|
857
|
-
*/
|
|
858
|
-
function createMockRepository(overrides = {}) {
|
|
859
|
-
return {
|
|
860
|
-
getAll: vi.fn().mockResolvedValue({
|
|
861
|
-
method: "offset",
|
|
862
|
-
data: [],
|
|
863
|
-
total: 0,
|
|
864
|
-
page: 1,
|
|
865
|
-
limit: 20,
|
|
866
|
-
pages: 0,
|
|
867
|
-
hasNext: false,
|
|
868
|
-
hasPrev: false
|
|
869
|
-
}),
|
|
870
|
-
getById: vi.fn().mockResolvedValue(null),
|
|
871
|
-
create: vi.fn().mockImplementation((data) => Promise.resolve({
|
|
872
|
-
_id: "mock-id",
|
|
873
|
-
...data
|
|
874
|
-
})),
|
|
875
|
-
update: vi.fn().mockImplementation((_id, data) => Promise.resolve({
|
|
876
|
-
_id: "mock-id",
|
|
877
|
-
...data
|
|
878
|
-
})),
|
|
879
|
-
delete: vi.fn().mockResolvedValue({
|
|
880
|
-
success: true,
|
|
881
|
-
message: "Deleted"
|
|
882
|
-
}),
|
|
883
|
-
updateMany: vi.fn().mockResolvedValue({
|
|
884
|
-
acknowledged: true,
|
|
885
|
-
matchedCount: 0,
|
|
886
|
-
modifiedCount: 0
|
|
887
|
-
}),
|
|
888
|
-
deleteMany: vi.fn().mockResolvedValue({
|
|
889
|
-
acknowledged: true,
|
|
890
|
-
deletedCount: 0
|
|
891
|
-
}),
|
|
892
|
-
claim: vi.fn().mockResolvedValue(null),
|
|
893
|
-
claimVersion: vi.fn().mockResolvedValue(null),
|
|
894
|
-
capabilities: {
|
|
895
|
-
transactions: false,
|
|
896
|
-
nestedTransactions: false,
|
|
897
|
-
upsert: false,
|
|
898
|
-
duplicateKeyError: false,
|
|
899
|
-
distinct: false,
|
|
900
|
-
aggregate: false,
|
|
901
|
-
getOrCreate: false,
|
|
902
|
-
countAndExists: false
|
|
903
|
-
},
|
|
904
|
-
getBySlug: vi.fn().mockResolvedValue(null),
|
|
905
|
-
getDeleted: vi.fn().mockResolvedValue([]),
|
|
906
|
-
restore: vi.fn().mockResolvedValue(null),
|
|
907
|
-
getTree: vi.fn().mockResolvedValue([]),
|
|
908
|
-
getChildren: vi.fn().mockResolvedValue([]),
|
|
909
|
-
...overrides
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Create a mock user for authentication testing
|
|
914
|
-
*/
|
|
915
|
-
function createMockUser(overrides = {}) {
|
|
916
|
-
return {
|
|
917
|
-
_id: "mock-user-id",
|
|
918
|
-
id: "mock-user-id",
|
|
919
|
-
email: "test@example.com",
|
|
920
|
-
roles: ["user"],
|
|
921
|
-
organizationId: null,
|
|
922
|
-
...overrides
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Create a mock Fastify request
|
|
927
|
-
*/
|
|
928
|
-
function createMockRequest(overrides = {}) {
|
|
929
|
-
return {
|
|
930
|
-
body: {},
|
|
931
|
-
params: {},
|
|
932
|
-
query: {},
|
|
933
|
-
headers: {},
|
|
934
|
-
user: createMockUser(),
|
|
935
|
-
context: {},
|
|
936
|
-
log: {
|
|
937
|
-
info: vi.fn(),
|
|
938
|
-
warn: vi.fn(),
|
|
939
|
-
error: vi.fn(),
|
|
940
|
-
debug: vi.fn()
|
|
941
|
-
},
|
|
942
|
-
...overrides
|
|
943
|
-
};
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Create a mock Fastify reply
|
|
947
|
-
*/
|
|
948
|
-
function createMockReply() {
|
|
949
|
-
return {
|
|
950
|
-
code: vi.fn().mockReturnThis(),
|
|
951
|
-
send: vi.fn().mockReturnThis(),
|
|
952
|
-
header: vi.fn().mockReturnThis(),
|
|
953
|
-
headers: vi.fn().mockReturnThis(),
|
|
954
|
-
status: vi.fn().mockReturnThis(),
|
|
955
|
-
type: vi.fn().mockReturnThis(),
|
|
956
|
-
redirect: vi.fn().mockReturnThis(),
|
|
957
|
-
callNotFound: vi.fn().mockReturnThis(),
|
|
958
|
-
sent: false
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* Create a mock controller for testing
|
|
963
|
-
*/
|
|
964
|
-
function createMockController(repository) {
|
|
965
|
-
return {
|
|
966
|
-
repository,
|
|
967
|
-
list: vi.fn(),
|
|
968
|
-
get: vi.fn(),
|
|
969
|
-
create: vi.fn(),
|
|
970
|
-
update: vi.fn(),
|
|
971
|
-
delete: vi.fn()
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* Create mock data factory
|
|
976
|
-
*
|
|
977
|
-
* @example
|
|
978
|
-
* const productFactory = createDataFactory<Product>({
|
|
979
|
-
* name: () => faker.commerce.productName(),
|
|
980
|
-
* price: () => faker.number.int({ min: 10, max: 1000 }),
|
|
981
|
-
* sku: (i) => `SKU-${i}`,
|
|
982
|
-
* });
|
|
983
|
-
*
|
|
984
|
-
* const product = productFactory.build();
|
|
985
|
-
* const products = productFactory.buildMany(10);
|
|
986
|
-
*/
|
|
987
|
-
function createDataFactory(template) {
|
|
988
|
-
let counter = 0;
|
|
989
|
-
return {
|
|
990
|
-
build(overrides = {}) {
|
|
991
|
-
const index = counter++;
|
|
992
|
-
const data = {};
|
|
993
|
-
for (const [key, generator] of Object.entries(template)) data[key] = generator(index);
|
|
994
|
-
return {
|
|
995
|
-
...data,
|
|
996
|
-
...overrides
|
|
997
|
-
};
|
|
998
|
-
},
|
|
999
|
-
buildMany(count, overrides = {}) {
|
|
1000
|
-
return Array.from({ length: count }, () => this.build(overrides));
|
|
1001
|
-
},
|
|
1002
|
-
reset() {
|
|
1003
|
-
counter = 0;
|
|
1004
|
-
}
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Create a spy that tracks function calls
|
|
1009
|
-
*
|
|
1010
|
-
* Useful for testing side effects without full mocking
|
|
1011
|
-
*/
|
|
1012
|
-
function createSpy(_name = "spy") {
|
|
1013
|
-
const calls = [];
|
|
1014
|
-
const spy = vi.fn((...args) => {
|
|
1015
|
-
calls.push(args);
|
|
1016
|
-
});
|
|
1017
|
-
spy.getCalls = () => calls;
|
|
1018
|
-
spy.getLastCall = () => calls[calls.length - 1] || [];
|
|
1019
|
-
return spy;
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Wait for a condition to be true
|
|
1023
|
-
*
|
|
1024
|
-
* Useful for async testing
|
|
1025
|
-
*/
|
|
1026
|
-
async function waitFor(condition, options = {}) {
|
|
1027
|
-
const { timeout = 5e3, interval = 100 } = options;
|
|
1028
|
-
const startTime = Date.now();
|
|
1029
|
-
while (Date.now() - startTime < timeout) {
|
|
1030
|
-
if (await condition()) return;
|
|
1031
|
-
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
1032
|
-
}
|
|
1033
|
-
throw new Error(`Timeout waiting for condition after ${timeout}ms`);
|
|
1034
|
-
}
|
|
1035
|
-
/**
|
|
1036
|
-
* Create a test timer that can be controlled
|
|
1037
|
-
*/
|
|
1038
|
-
function createTestTimer() {
|
|
1039
|
-
let time = Date.now();
|
|
1040
|
-
return {
|
|
1041
|
-
now: () => time,
|
|
1042
|
-
advance: (ms) => {
|
|
1043
|
-
time += ms;
|
|
1044
|
-
},
|
|
1045
|
-
set: (timestamp) => {
|
|
1046
|
-
time = timestamp;
|
|
1047
|
-
},
|
|
1048
|
-
reset: () => {
|
|
1049
|
-
time = Date.now();
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
//#endregion
|
|
1054
1159
|
//#region src/testing/testApp.ts
|
|
1055
1160
|
/**
|
|
1056
1161
|
* createTestApp — test app factory for arc
|
|
@@ -1116,7 +1221,7 @@ function pickDefaultAuth(authMode, callerAuth) {
|
|
|
1116
1221
|
};
|
|
1117
1222
|
}
|
|
1118
1223
|
async function createTestApp(options = {}) {
|
|
1119
|
-
const { createApp } = await import("../
|
|
1224
|
+
const { createApp } = await import("../createApp-CXrKFciH.mjs").then((n) => n.r);
|
|
1120
1225
|
const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
|
|
1121
1226
|
let dbHandle;
|
|
1122
1227
|
let dbUri;
|
|
@@ -1186,4 +1291,4 @@ function createMinimalTestApp(options = {}) {
|
|
|
1186
1291
|
});
|
|
1187
1292
|
}
|
|
1188
1293
|
//#endregion
|
|
1189
|
-
export { HttpTestHarness, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
|
|
1294
|
+
export { HttpTestHarness, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, setupBetterAuthTestApp, waitFor };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
2
|
+
//#region src/testing/outboxStoreContract.d.ts
|
|
3
|
+
interface OutboxStoreContractSetupResult {
|
|
4
|
+
store: OutboxStore;
|
|
5
|
+
/** Called once after the suite. */
|
|
6
|
+
teardown?: () => Promise<void>;
|
|
7
|
+
/** Empty the store between tests. Strongly recommended for isolation. */
|
|
8
|
+
reset?: () => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
type OutboxStoreContractSetup = () => Promise<OutboxStoreContractSetupResult>;
|
|
11
|
+
declare function runOutboxStoreContract(name: string, setup: OutboxStoreContractSetup): void;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { OutboxStoreContractSetup, OutboxStoreContractSetupResult, runOutboxStoreContract };
|