@classytic/arc 2.30.0 → 2.32.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/LICENSE +1 -1
- package/README.md +7 -0
- package/TRADEMARK.md +22 -0
- package/dist/{BaseController-B5DwufbB.mjs → BaseController-Dk34dxhI.mjs} +21 -19
- package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
- package/dist/{EventTransport-C6YA8xY9.mjs → EventTransport-BV10D0oG.mjs} +1 -0
- package/dist/{HookSystem-WkIougo6.mjs → HookSystem-vimw5D7Z.mjs} +2 -1
- package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
- package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-BVr7PgA0.mjs} +18 -15
- package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-Dti7ebyv.mjs} +1 -0
- package/dist/audit/index.d.mts +3 -2
- package/dist/audit/index.mjs +2 -1
- package/dist/auth/audit.d.mts +1 -0
- package/dist/auth/audit.mjs +1 -0
- package/dist/auth/index.d.mts +5 -4
- package/dist/auth/index.mjs +11 -9
- package/dist/auth/redis-session.d.mts +2 -1
- package/dist/auth/redis-session.mjs +1 -0
- package/dist/{applyPermissionResult-Cf6zUCvb.mjs → authorizationDecision-__T3EJDA.mjs} +19 -10
- package/dist/{buildHandler-BYS3Vkfz.mjs → buildHandler-Dn7rzUJo.mjs} +6 -4
- package/dist/cache/index.d.mts +4 -3
- package/dist/cache/index.mjs +4 -3
- package/dist/{caching-_NJeDd1_.mjs → caching-DFJqHyFs.mjs} +2 -1
- package/dist/cleanup/index.d.mts +39 -16
- package/dist/cleanup/index.mjs +50 -38
- package/dist/cli/commands/describe.d.mts +2 -1
- package/dist/cli/commands/describe.mjs +4 -5
- package/dist/cli/commands/docs.d.mts +1 -0
- package/dist/cli/commands/docs.mjs +3 -2
- package/dist/cli/commands/doctor.d.mts +1 -0
- package/dist/cli/commands/doctor.mjs +1 -0
- package/dist/cli/commands/generate.d.mts +2 -1
- package/dist/cli/commands/generate.mjs +2 -1
- package/dist/cli/commands/init.d.mts +2 -1
- package/dist/cli/commands/init.mjs +2 -1
- package/dist/cli/commands/introspect.d.mts +1 -0
- package/dist/cli/commands/introspect.mjs +2 -1
- package/dist/cli/index.d.mts +3 -2
- package/dist/cli/index.mjs +3 -2
- package/dist/{constants-DNre0wRg.mjs → constants-J99LGTJI.mjs} +1 -0
- package/dist/context/index.d.mts +1 -0
- package/dist/context/index.mjs +3 -2
- package/dist/core/index.d.mts +3 -2
- package/dist/core/index.mjs +7 -6
- package/dist/{core-poalJLvp.mjs → core-BwwmNSp7.mjs} +3 -2
- package/dist/{createActionRouter-BxuywVRB.mjs → createActionRouter-DkJZWo0m.mjs} +14 -10
- package/dist/{createAggregationRouter-BiY-lGdY.mjs → createAggregationRouter-B1exOVD8.mjs} +10 -7
- package/dist/{createApp-CXrKFciH.mjs → createApp-Bv0m-Q6_.mjs} +625 -229
- package/dist/{defineAggregation-ufWjUwml.mjs → defineAggregation-gX4No1gt.mjs} +1 -0
- package/dist/{defineResource-CxWqDCAy.mjs → defineResource-XFPPUu2c.mjs} +84 -50
- package/dist/discovery/index.d.mts +1 -0
- package/dist/discovery/index.mjs +1 -0
- package/dist/docs/index.d.mts +3 -2
- package/dist/docs/index.mjs +3 -2
- package/dist/{elevation-C9s5hFP_.d.mts → elevation-BXCfVcNG.d.mts} +1 -0
- package/dist/{elevation-CIz1GjGm.mjs → elevation-DNXXxgjl.mjs} +4 -3
- package/dist/encryption/index.d.mts +2 -1
- package/dist/encryption/index.mjs +5 -3
- package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-B2AZUJUj.mjs} +3 -2
- package/dist/{errorHandler-C6yv4ZTW.d.mts → errorHandler-CpYUJmPA.d.mts} +1 -0
- package/dist/{errors-DHcHVAgk.d.mts → errors-BGdVwP25.d.mts} +1 -0
- package/dist/{errors-DkO8Nk6-.mjs → errors-Bh1KK5ps.mjs} +1 -0
- package/dist/{eventPlugin-D2NNR4py.mjs → eventPlugin-Dk_TyXeE.mjs} +8 -6
- package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
- package/dist/events/index.d.mts +112 -44
- package/dist/events/index.mjs +177 -141
- package/dist/events/transports/redis-stream-entry.d.mts +2 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -9
- package/dist/events/transports/redis.d.mts +2 -1
- package/dist/events/transports/redis.mjs +1 -0
- package/dist/{externalPaths-BE6wFpYv.d.mts → externalPaths-CmGOxOtD.d.mts} +1 -0
- package/dist/factory/index.d.mts +9 -5
- package/dist/factory/index.mjs +131 -7
- package/dist/fastify-augmentation-BppG5bMT.d.mts +179 -0
- package/dist/{fields-CQ_7KQWw.mjs → fields-DEi65pGj.mjs} +6 -10
- package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
- package/dist/{generate-D0wZupBg.mjs → generate-CRSG0jTv.mjs} +2 -1
- package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
- package/dist/{headers-BvlQSDMB.mjs → headers-BXjQYbeC.mjs} +2 -1
- package/dist/{health-CB_m4S32.mjs → health-5XnTa2uo.mjs} +1 -0
- package/dist/hooks/index.d.mts +2 -1
- package/dist/hooks/index.mjs +2 -1
- package/dist/idempotency/index.d.mts +3 -2
- package/dist/idempotency/index.mjs +8 -5
- package/dist/idempotency/redis.d.mts +2 -1
- package/dist/idempotency/redis.mjs +1 -0
- package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
- package/dist/{index-Q5gzEIWY.d.mts → index-CpzxqePp.d.mts} +107 -32
- package/dist/{index-Cx7bro4R.d.mts → index-DXlHwvuE.d.mts} +5 -4
- package/dist/index-ZWhLTp_s.d.mts +290 -0
- package/dist/{index-Ch16aAah.d.mts → index-zq6VrjaB.d.mts} +122 -15
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +11 -10
- package/dist/{init-tlRSt667.mjs → init-BbfP8aky.mjs} +25 -27
- package/dist/{init-BPEu8cs-.d.mts → init-Cscs4lab.d.mts} +1 -0
- package/dist/integrations/event-gateway.d.mts +3 -2
- package/dist/integrations/event-gateway.mjs +2 -1
- package/dist/integrations/index.d.mts +4 -3
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs/index.d.mts +2 -1
- package/dist/integrations/jobs/index.mjs +109 -33
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +2 -1
- package/dist/integrations/mcp/testing.d.mts +3 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +1 -0
- package/dist/integrations/streamline.mjs +3 -2
- package/dist/integrations/webhooks.d.mts +1 -0
- package/dist/integrations/webhooks.mjs +1 -0
- package/dist/integrations/websocket-pushref-redis.d.mts +2 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -0
- package/dist/integrations/websocket-redis.d.mts +2 -1
- package/dist/integrations/websocket-redis.mjs +1 -0
- package/dist/integrations/websocket.d.mts +3 -2
- package/dist/integrations/websocket.mjs +38 -34
- package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
- package/dist/{interface-n5hpKg1d.mjs → interface-BP4IJCBO.mjs} +1 -0
- package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
- package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-DDSR2HFl.mjs} +2 -1
- package/dist/{keys-Cd316u_y.mjs → keys-B-HBTGXl.mjs} +1 -0
- package/dist/{loadResources-DpU8pfjV.mjs → loadResources-DisEAmkm.mjs} +4 -2
- package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-D1MJp13f.mjs} +2 -1
- package/dist/logger/index.d.mts +1 -0
- package/dist/logger/index.mjs +1 -0
- package/dist/{mcpPlugin-nRzm5g2s.mjs → mcpPlugin-CpfVpKHE.mjs} +38 -38
- package/dist/{memory-CmnzXE5Z.mjs → memory-DsuWnj4T.mjs} +5 -4
- package/dist/{metrics-fdxI5mn_.mjs → metrics-DdQL8syd.mjs} +2 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.mjs +3 -2
- package/dist/migrations/index.d.mts +1 -0
- package/dist/migrations/index.mjs +3 -2
- package/dist/module-Br81am6m.mjs +78 -0
- package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-BnkBLN7v.mjs} +3 -2
- package/dist/{openapi-B8Vkbxbn.mjs → openapi-DIRzvxC0.mjs} +6 -5
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +6 -5
- package/dist/{permissions-Bfu_8y1U.mjs → permissions-Dl_m0NCe.mjs} +308 -42
- package/dist/{pipe-BOLmGIxa.mjs → pipe-ELHCP3zj.mjs} +3 -4
- package/dist/pipeline/index.d.mts +2 -1
- package/dist/pipeline/index.mjs +2 -1
- package/dist/{pipeline-BJnpkY9i.mjs → pipeline-CPA-jx0M.mjs} +17 -14
- package/dist/plugins/index.d.mts +8 -7
- package/dist/plugins/index.mjs +16 -15
- package/dist/plugins/response-cache.d.mts +1 -0
- package/dist/plugins/response-cache.mjs +2 -1
- package/dist/plugins/tracing-entry.d.mts +2 -1
- package/dist/plugins/tracing-entry.mjs +2 -1
- package/dist/{pluralize-BhrgwZb9.mjs → pluralize-C2ZkKbsn.mjs} +1 -0
- package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-CDs87fTw.mjs} +1 -0
- package/dist/{preloadResources-DCfa2egz.d.mts → preloadResources-T1hmlTEZ.d.mts} +117 -12
- package/dist/presets/filesUpload.d.mts +6 -5
- package/dist/presets/filesUpload.mjs +10 -12
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +2 -1
- package/dist/presets/multiTenant.d.mts +2 -1
- package/dist/presets/multiTenant.mjs +3 -2
- package/dist/presets/search.d.mts +3 -2
- package/dist/presets/search.mjs +2 -1
- package/dist/{presets-CJOEXLuk.mjs → presets-Dntd_pUO.mjs} +6 -4
- package/dist/{projection-UBDgfMwu.mjs → projection-CjHhM5W1.mjs} +2 -1
- package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
- package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
- package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-Dhps-Ldt.mjs} +6 -5
- package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-C7mmAGs3.mjs} +7 -3
- package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-CccEFBfk.mjs} +1 -0
- package/dist/{realtime-B9nmIY5V.mjs → realtime-DVvVZZFz.mjs} +20 -9
- package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
- package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
- package/dist/{registerRoute-JkxIEBJq.mjs → registerRoute-B_QrjYZS.mjs} +10 -7
- package/dist/registry/index.d.mts +3 -2
- package/dist/registry/index.mjs +3 -2
- package/dist/{registry-BYXUUg21.mjs → registry-DqUmzA9W.mjs} +2 -1
- package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-BpP9wwj7.mjs} +1 -0
- package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DS-P0b32.mjs} +2 -1
- package/dist/{repositoryFilter-Dz8k3S4P.mjs → repositoryFilter-DotlxG3L.mjs} +1 -0
- package/dist/{requestContext-MrlJwECf.mjs → requestContext-B6vQF-sy.mjs} +1 -0
- package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-BhflRrBs.mjs} +1 -0
- package/dist/{schedules-CjOd2MG9.mjs → schedules-C2Q3uv2d.mjs} +3 -2
- package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-Crjvczi_.mjs} +70 -17
- package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-CnyKK_1j.mjs} +2 -1
- package/dist/schemas/index.d.mts +1 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/scim/index.d.mts +1 -0
- package/dist/scim/index.mjs +2 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +5 -4
- package/dist/{scope-DfaPntpo.mjs → scope-afz_Ieek.mjs} +4 -3
- package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
- package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-C4KiMWUa.mjs} +1 -0
- package/dist/{sse-BQ1w0dfl.mjs → sse-B9P4Y-UI.mjs} +7 -5
- package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B3zb_d1h.mjs} +2 -1
- package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
- package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-DmEMHlPQ.mjs} +1 -0
- package/dist/{streaming-BQcW6_Kq.mjs → streaming-DPZdQmDz.mjs} +1 -0
- package/dist/{subscribe-helpers-DfzsivM6.mjs → subscribe-helpers-sp5xTDVA.mjs} +2 -1
- package/dist/sync/changelog.d.mts +1 -0
- package/dist/sync/changelog.mjs +1 -0
- package/dist/testing/cleanupStoreContract.d.mts +2 -1
- package/dist/testing/cleanupStoreContract.mjs +10 -5
- package/dist/testing/index.d.mts +166 -12
- package/dist/testing/index.mjs +315 -43
- package/dist/testing/outboxStoreContract.d.mts +12 -2
- package/dist/testing/outboxStoreContract.mjs +110 -20
- package/dist/testing/storageContract.d.mts +2 -1
- package/dist/testing/storageContract.mjs +11 -5
- package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
- package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-BvLKvOIL.mjs} +1 -0
- package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B_5-hKjJ.mjs} +1 -0
- package/dist/types/index.d.mts +6 -5
- package/dist/types/index.mjs +1 -0
- package/dist/types/storage.d.mts +2 -1
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-ChvvVNP8.d.mts → types-B7XeLEgK.d.mts} +34 -2
- package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
- package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
- package/dist/{types-D_n8L6VY.d.mts → types-CT5vu6M0.d.mts} +132 -17
- package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
- package/dist/{types-Bmmn-fO0.d.mts → types-CwcUQ610.d.mts} +2 -6
- package/dist/{types-CSyCxl2V.mjs → types-DXYX7dIR.mjs} +4 -3
- package/dist/usage/index.d.mts +3 -2
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-BNsOXzOd.mjs} +1 -0
- package/dist/utils/index.d.mts +16 -7
- package/dist/utils/index.mjs +10 -9
- package/dist/{utils-taUVD4p3.mjs → utils-BtOha_Sp.mjs} +3 -2
- package/dist/{validate-Bk44F2yT.mjs → validate-PMCPR9El.mjs} +2 -1
- package/dist/{versioning-gKWyryT3.mjs → versioning-CKv2jz_U.mjs} +4 -3
- package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
- package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
- package/package.json +557 -539
- package/skills/arc/SKILL.md +22 -8
- package/skills/arc/references/api-reference.md +4 -0
- package/skills/arc/references/auth.md +3 -0
- package/skills/arc/references/integrations.md +33 -0
- package/skills/arc/references/mcp.md +6 -7
- package/skills/arc/references/production.md +24 -25
- package/skills/arc-code-review/references/anti-patterns.md +3 -3
- package/dist/fastify-augmentation-0WhPnUH7.d.mts +0 -76
- package/dist/index-CCzVR-N3.d.mts +0 -107
- package/dist/module-_-M7C5ES.mjs +0 -29
package/dist/testing/index.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as CRUD_OPERATIONS } from "../constants-J99LGTJI.mjs";
|
|
3
|
+
import { g as getUserId } from "../types-DXYX7dIR.mjs";
|
|
4
|
+
import { t as BaseController } from "../BaseController-Dk34dxhI.mjs";
|
|
5
|
+
import { t as defineResource } from "../defineResource-XFPPUu2c.mjs";
|
|
6
|
+
import { t as defineAggregation } from "../defineAggregation-gX4No1gt.mjs";
|
|
7
|
+
import { E as requireOrgRole } from "../permissions-Dl_m0NCe.mjs";
|
|
8
|
+
import { n as createApp } from "../createApp-Bv0m-Q6_.mjs";
|
|
9
|
+
import { n as getModuleExports } from "../module-Br81am6m.mjs";
|
|
10
|
+
import { n as preloadResourcesAsync, t as preloadResources } from "../preloadResources-CDs87fTw.mjs";
|
|
8
11
|
import { runStorageContract } from "./storageContract.mjs";
|
|
9
12
|
import Fastify from "fastify";
|
|
10
13
|
import { afterAll, describe, expect, it, vi } from "vitest";
|
|
@@ -61,7 +64,8 @@ function expectArc(response) {
|
|
|
61
64
|
failed(status) {
|
|
62
65
|
if (status !== void 0) expect(response.statusCode).toBe(status);
|
|
63
66
|
else expect(response.statusCode).toBeGreaterThanOrEqual(400);
|
|
64
|
-
|
|
67
|
+
const body = getBody();
|
|
68
|
+
expect(body.code, "expected ErrorContract.code on failed response").toBeDefined();
|
|
65
69
|
return assertion;
|
|
66
70
|
},
|
|
67
71
|
unauthorized() {
|
|
@@ -81,7 +85,8 @@ function expectArc(response) {
|
|
|
81
85
|
},
|
|
82
86
|
hasData() {
|
|
83
87
|
const body = getBody();
|
|
84
|
-
|
|
88
|
+
const hasContent = Object.keys(body).length > 0;
|
|
89
|
+
expect(hasContent, "expected response body to be defined").toBe(true);
|
|
85
90
|
return assertion;
|
|
86
91
|
},
|
|
87
92
|
hasStatus(status) {
|
|
@@ -89,11 +94,13 @@ function expectArc(response) {
|
|
|
89
94
|
return assertion;
|
|
90
95
|
},
|
|
91
96
|
hidesField(field) {
|
|
92
|
-
|
|
97
|
+
const body = getBody();
|
|
98
|
+
expect(body, `expected field '${field}' to be hidden from response`).not.toHaveProperty(field);
|
|
93
99
|
return assertion;
|
|
94
100
|
},
|
|
95
101
|
showsField(field) {
|
|
96
|
-
|
|
102
|
+
const body = getBody();
|
|
103
|
+
expect(body, `expected field '${field}' on response`).toHaveProperty(field);
|
|
97
104
|
return assertion;
|
|
98
105
|
},
|
|
99
106
|
paginated(expected) {
|
|
@@ -304,18 +311,30 @@ function createSpy(_name = "spy") {
|
|
|
304
311
|
return spy;
|
|
305
312
|
}
|
|
306
313
|
/**
|
|
307
|
-
* Wait
|
|
314
|
+
* Wait until `condition` holds — the standard way to assert on asynchronous
|
|
315
|
+
* effects in arc's suites.
|
|
308
316
|
*
|
|
309
|
-
*
|
|
317
|
+
* **Prefer this over `await sleep(n)` + assert.** A fixed delay encodes a guess
|
|
318
|
+
* about scheduling: too short and it flakes the moment the pool is busy, too
|
|
319
|
+
* long and every run pays for the worst case. Polling returns as soon as the
|
|
320
|
+
* effect lands, so the fast path stays fast and a loaded machine simply waits —
|
|
321
|
+
* and the failure mode becomes "this never happened", which is the thing worth
|
|
322
|
+
* asserting.
|
|
323
|
+
*
|
|
324
|
+
* Supply `label`: without it a timeout reports only its own duration, which is
|
|
325
|
+
* the least useful sentence a flaky test can print.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* await waitFor(() => runs >= 3, { label: "3 scheduler ticks" });
|
|
310
329
|
*/
|
|
311
330
|
async function waitFor(condition, options = {}) {
|
|
312
|
-
const { timeout = 5e3, interval =
|
|
331
|
+
const { timeout = 5e3, interval = 25, label } = options;
|
|
313
332
|
const startTime = Date.now();
|
|
314
333
|
while (Date.now() - startTime < timeout) {
|
|
315
334
|
if (await condition()) return;
|
|
316
335
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
317
336
|
}
|
|
318
|
-
throw new Error(`
|
|
337
|
+
throw new Error(label ? `Timed out after ${timeout}ms waiting for: ${label}` : `Timed out after ${timeout}ms waiting for a condition (pass "label" to name it)`);
|
|
319
338
|
}
|
|
320
339
|
/**
|
|
321
340
|
* Create a test timer that can be controlled
|
|
@@ -363,6 +382,7 @@ function createTestTimer() {
|
|
|
363
382
|
async function runAuthorizationConformance(opts) {
|
|
364
383
|
const rows = opts.rows ?? [{ _id: "1" }];
|
|
365
384
|
let listFilter;
|
|
385
|
+
let getFilter;
|
|
366
386
|
let aggFilter;
|
|
367
387
|
const repo = createMockRepository({
|
|
368
388
|
getAll: (async (params = {}) => {
|
|
@@ -378,6 +398,25 @@ async function runAuthorizationConformance(opts) {
|
|
|
378
398
|
hasPrev: false
|
|
379
399
|
};
|
|
380
400
|
}),
|
|
401
|
+
getOne: (async (filter) => {
|
|
402
|
+
getFilter = filter;
|
|
403
|
+
return rows[0] ?? null;
|
|
404
|
+
}),
|
|
405
|
+
/**
|
|
406
|
+
* The single-record read has TWO paths, and the harness must be able to observe both.
|
|
407
|
+
*
|
|
408
|
+
* `fetchDetailed` only builds a compound filter and calls `getOne` when there is something to
|
|
409
|
+
* compound — a row policy, a tenant field, or a non-default id field. A permission that simply
|
|
410
|
+
* ALLOWS (no policy) leaves `{ _id }` alone and takes the plain `getById` path instead. The base
|
|
411
|
+
* mock stubs that as `null`, so before this stub the `get` surface reported **404 on every
|
|
412
|
+
* policy-free allow** while `list` and `aggregation` reported 200.
|
|
413
|
+
*
|
|
414
|
+
* That reads as partial enforcement and it is not: it is the harness failing to answer. A host
|
|
415
|
+
* asserting `get.status === 200` for its own allow-gate — the obvious falsification case to pair
|
|
416
|
+
* with a deny-parity suite — was told its gate broke the single-record surface. Found from
|
|
417
|
+
* be-prod, whose conformance suite does exactly that.
|
|
418
|
+
*/
|
|
419
|
+
getById: (async () => rows[0] ?? null),
|
|
381
420
|
aggregate: (async (req = {}) => {
|
|
382
421
|
aggFilter = req.filter;
|
|
383
422
|
return { rows: [{ tally: rows.length }] };
|
|
@@ -419,6 +458,10 @@ async function runAuthorizationConformance(opts) {
|
|
|
419
458
|
method: "GET",
|
|
420
459
|
url: "/confs"
|
|
421
460
|
});
|
|
461
|
+
const get = await app.inject({
|
|
462
|
+
method: "GET",
|
|
463
|
+
url: "/confs/1"
|
|
464
|
+
});
|
|
422
465
|
const aggregation = await app.inject({
|
|
423
466
|
method: "GET",
|
|
424
467
|
url: "/confs/aggregations/tally"
|
|
@@ -428,6 +471,10 @@ async function runAuthorizationConformance(opts) {
|
|
|
428
471
|
status: list.statusCode,
|
|
429
472
|
filter: listFilter
|
|
430
473
|
},
|
|
474
|
+
get: {
|
|
475
|
+
status: get.statusCode,
|
|
476
|
+
filter: getFilter
|
|
477
|
+
},
|
|
431
478
|
aggregation: {
|
|
432
479
|
status: aggregation.statusCode,
|
|
433
480
|
filter: aggFilter
|
|
@@ -775,6 +822,127 @@ async function setupBetterAuthTestApp(input) {
|
|
|
775
822
|
};
|
|
776
823
|
}
|
|
777
824
|
//#endregion
|
|
825
|
+
//#region src/testing/testActor.ts
|
|
826
|
+
/**
|
|
827
|
+
* Test-only actor installation — how a package's HTTP tests say "run this
|
|
828
|
+
* request as a manager in org X".
|
|
829
|
+
*
|
|
830
|
+
* Identity in arc comes from `request.scope` and only from `request.scope`;
|
|
831
|
+
* headers like `x-role` are not identity. That rule left tests with no seam —
|
|
832
|
+
* `bootModuleApp` boots with `auth: false`, so every injected request arrived as
|
|
833
|
+
* `PUBLIC_SCOPE` and any role-gated route answered 401 with no supported way to
|
|
834
|
+
* be somebody.
|
|
835
|
+
*
|
|
836
|
+
* The `x-test-actor` header is a TRANSPORT for the test's intent, not a trusted
|
|
837
|
+
* claim. The authenticator that reads it is installed by `bootModuleApp` alone,
|
|
838
|
+
* behind the `@classytic/arc/testing` entrypoint; `createApp` never registers
|
|
839
|
+
* it, so no production build turns a forged header into identity. It is
|
|
840
|
+
* per-request rather than boot-only because the interesting authorization tests
|
|
841
|
+
* are the ones where a cashier is refused what a manager is allowed.
|
|
842
|
+
*
|
|
843
|
+
* Gate the routes under test with arc's own primitives — `requireRoles`,
|
|
844
|
+
* `requireOrgMembership`, `allOf(...)`. A test-specific gate would be a second
|
|
845
|
+
* implementation of authorization whose semantics could drift from the one
|
|
846
|
+
* actually shipping.
|
|
847
|
+
*/
|
|
848
|
+
/** The header the testing harness uses to carry a request's actor. */
|
|
849
|
+
const TEST_ACTOR_HEADER = "x-test-actor";
|
|
850
|
+
/**
|
|
851
|
+
* Build the headers that make an injected request act as `actor`.
|
|
852
|
+
*
|
|
853
|
+
* A string is shorthand for a single role. `null` means "stay public", so a test
|
|
854
|
+
* can prove an endpoint is reachable anonymously.
|
|
855
|
+
*
|
|
856
|
+
* @example
|
|
857
|
+
* ```ts
|
|
858
|
+
* t.app.inject({ method: 'GET', url: '/pos/shifts/current',
|
|
859
|
+
* headers: testActorHeaders('manager', ORG) });
|
|
860
|
+
* ```
|
|
861
|
+
*/
|
|
862
|
+
function testActorHeaders(actor, organizationId) {
|
|
863
|
+
if (actor === null) return {};
|
|
864
|
+
const org = organizationId !== void 0 ? { organizationId } : {};
|
|
865
|
+
const spec = typeof actor === "string" ? {
|
|
866
|
+
roles: [actor],
|
|
867
|
+
...org
|
|
868
|
+
} : {
|
|
869
|
+
...actor,
|
|
870
|
+
...org
|
|
871
|
+
};
|
|
872
|
+
return { [TEST_ACTOR_HEADER]: JSON.stringify(spec) };
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Turn a spec into a `RequestScope`.
|
|
876
|
+
*
|
|
877
|
+
* `orgRoles` defaults to `roles`: a test that says "manager" means it at both
|
|
878
|
+
* levels, and making the caller repeat itself only produces tests that pass a
|
|
879
|
+
* global gate and mysteriously fail an org one.
|
|
880
|
+
*/
|
|
881
|
+
function scopeFromTestActor(spec) {
|
|
882
|
+
const roles = spec.roles ?? [];
|
|
883
|
+
const userId = spec.userId ?? `test-${roles.join("-") || "anon"}`;
|
|
884
|
+
if (spec.elevated !== void 0 && spec.elevated !== false) return {
|
|
885
|
+
kind: "elevated",
|
|
886
|
+
userId,
|
|
887
|
+
elevatedBy: spec.elevated === true ? "test" : spec.elevated,
|
|
888
|
+
...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {},
|
|
889
|
+
...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
|
|
890
|
+
};
|
|
891
|
+
if (spec.organizationId !== void 0) return {
|
|
892
|
+
kind: "member",
|
|
893
|
+
userId,
|
|
894
|
+
userRoles: roles,
|
|
895
|
+
organizationId: spec.organizationId,
|
|
896
|
+
orgRoles: spec.orgRoles ?? roles,
|
|
897
|
+
...spec.teamId !== void 0 ? { teamId: spec.teamId } : {},
|
|
898
|
+
...spec.context !== void 0 ? { context: Object.freeze({ ...spec.context }) } : {}
|
|
899
|
+
};
|
|
900
|
+
return {
|
|
901
|
+
kind: "authenticated",
|
|
902
|
+
userId,
|
|
903
|
+
userRoles: roles
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Build the `auth` option that makes the test actor arc's authenticator.
|
|
908
|
+
*
|
|
909
|
+
* It must be an authenticator, not a hook. Fastify encapsulates plugins and
|
|
910
|
+
* modules register DURING `createApp`, so a hook added to the root instance
|
|
911
|
+
* afterwards never runs for their routes — every request still resolves as
|
|
912
|
+
* public and every role gate still answers 401, which reads exactly like a
|
|
913
|
+
* broken authorization rule in the code under test. Registering as
|
|
914
|
+
* `auth: { type: 'authenticator' }` puts the scope where arc's own auth would,
|
|
915
|
+
* at the phase arc expects, so tenant filtering and permission checks behave
|
|
916
|
+
* identically to production.
|
|
917
|
+
*
|
|
918
|
+
* `defaultActor` applies to requests carrying no header. A malformed header
|
|
919
|
+
* REJECTS rather than falling back to public — a test that meant to be an admin
|
|
920
|
+
* and got quietly demoted is the most expensive way to fail.
|
|
921
|
+
*/
|
|
922
|
+
function testActorAuth(defaultActor) {
|
|
923
|
+
const authenticate = async (request) => {
|
|
924
|
+
const req = request;
|
|
925
|
+
const raw = req.headers[TEST_ACTOR_HEADER];
|
|
926
|
+
let spec = defaultActor;
|
|
927
|
+
if (typeof raw === "string") try {
|
|
928
|
+
spec = JSON.parse(raw);
|
|
929
|
+
} catch {
|
|
930
|
+
throw new Error(`[arc-testing] malformed ${TEST_ACTOR_HEADER} header: ${raw}. Build it with testActorHeaders() rather than by hand.`);
|
|
931
|
+
}
|
|
932
|
+
if (spec === void 0) return;
|
|
933
|
+
req.scope = scopeFromTestActor(spec);
|
|
934
|
+
req.user = {
|
|
935
|
+
id: getUserId(req.scope) ?? "test-anon",
|
|
936
|
+
...spec.roles !== void 0 ? { role: spec.roles } : {},
|
|
937
|
+
...spec.organizationId !== void 0 ? { organizationId: spec.organizationId } : {}
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
return {
|
|
941
|
+
type: "authenticator",
|
|
942
|
+
authenticate
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
//#endregion
|
|
778
946
|
//#region src/testing/bootModuleApp.ts
|
|
779
947
|
/**
|
|
780
948
|
* The built-in DB factory: in-memory MongoDB via lazy devDep imports.
|
|
@@ -807,7 +975,6 @@ async function bootModuleApp(modules, options = {}) {
|
|
|
807
975
|
const db = await (options.database ?? mongoMemoryDatabase)({ replset: options.replset ?? false });
|
|
808
976
|
const ctx = {
|
|
809
977
|
uri: db.uri,
|
|
810
|
-
mongoUri: db.uri,
|
|
811
978
|
connection: db.connection
|
|
812
979
|
};
|
|
813
980
|
const resolved = typeof modules === "function" ? await modules(ctx) : modules;
|
|
@@ -816,7 +983,17 @@ async function bootModuleApp(modules, options = {}) {
|
|
|
816
983
|
try {
|
|
817
984
|
app = await createApp({
|
|
818
985
|
preset: "testing",
|
|
819
|
-
|
|
986
|
+
/**
|
|
987
|
+
* The test actor is arc's AUTHENTICATOR rather than a bolted-on hook, so the scope
|
|
988
|
+
* lands at the phase the rest of the pipeline expects — tenant filtering and
|
|
989
|
+
* permission checks then behave exactly as they do in production.
|
|
990
|
+
*
|
|
991
|
+
* This replaces the old `auth: false`. The authenticator is inert for a request
|
|
992
|
+
* carrying no `x-test-actor` header and no configured default, so a suite that
|
|
993
|
+
* never mentions an actor behaves as it did before. `options.app` still wins, so a
|
|
994
|
+
* suite exercising real auth can override it.
|
|
995
|
+
*/
|
|
996
|
+
auth: testActorAuth(options.actor),
|
|
820
997
|
sensible: false,
|
|
821
998
|
...options.app,
|
|
822
999
|
modules: list
|
|
@@ -830,7 +1007,6 @@ async function bootModuleApp(modules, options = {}) {
|
|
|
830
1007
|
return {
|
|
831
1008
|
app,
|
|
832
1009
|
uri: db.uri,
|
|
833
|
-
mongoUri: db.uri,
|
|
834
1010
|
connection: db.connection,
|
|
835
1011
|
exports: (name) => getModuleExports(app, name),
|
|
836
1012
|
close: async () => {
|
|
@@ -1009,12 +1185,13 @@ var HttpTestHarness = class {
|
|
|
1009
1185
|
it(`${verb} /:id with non-existent ID should return 404`, async () => {
|
|
1010
1186
|
const { app, fixtures } = this.getOptions();
|
|
1011
1187
|
const payload = fixtures.update ?? fixtures.valid;
|
|
1012
|
-
|
|
1188
|
+
const res = await app.inject({
|
|
1013
1189
|
method: verb,
|
|
1014
1190
|
url: `${this.getBaseUrl()}/000000000000000000000000`,
|
|
1015
1191
|
headers: this.adminHeaders(),
|
|
1016
1192
|
payload
|
|
1017
|
-
})
|
|
1193
|
+
});
|
|
1194
|
+
expect(res.statusCode).toBe(404);
|
|
1018
1195
|
});
|
|
1019
1196
|
}
|
|
1020
1197
|
if (enabledRoutes.has("delete")) {
|
|
@@ -1031,24 +1208,29 @@ var HttpTestHarness = class {
|
|
|
1031
1208
|
deleteId = JSON.parse(createRes.body)._id;
|
|
1032
1209
|
}
|
|
1033
1210
|
if (!deleteId) return;
|
|
1034
|
-
|
|
1211
|
+
const res = await app.inject({
|
|
1035
1212
|
method: "DELETE",
|
|
1036
1213
|
url: `${this.getBaseUrl()}/${deleteId}`,
|
|
1037
1214
|
headers: this.adminHeaders()
|
|
1038
|
-
})
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1215
|
+
});
|
|
1216
|
+
expect(res.statusCode).toBe(200);
|
|
1217
|
+
if (enabledRoutes.has("get")) {
|
|
1218
|
+
const getRes = await app.inject({
|
|
1219
|
+
method: "GET",
|
|
1220
|
+
url: `${this.getBaseUrl()}/${deleteId}`,
|
|
1221
|
+
headers: this.adminHeaders()
|
|
1222
|
+
});
|
|
1223
|
+
expect(getRes.statusCode).toBe(404);
|
|
1224
|
+
}
|
|
1044
1225
|
});
|
|
1045
1226
|
it("DELETE /:id with non-existent ID should return 404", async () => {
|
|
1046
1227
|
const { app } = this.getOptions();
|
|
1047
|
-
|
|
1228
|
+
const res = await app.inject({
|
|
1048
1229
|
method: "DELETE",
|
|
1049
1230
|
url: `${this.getBaseUrl()}/000000000000000000000000`,
|
|
1050
1231
|
headers: this.adminHeaders()
|
|
1051
|
-
})
|
|
1232
|
+
});
|
|
1233
|
+
expect(res.statusCode).toBe(404);
|
|
1052
1234
|
});
|
|
1053
1235
|
}
|
|
1054
1236
|
});
|
|
@@ -1067,49 +1249,55 @@ var HttpTestHarness = class {
|
|
|
1067
1249
|
describe(`${resource.displayName} HTTP Permissions`, () => {
|
|
1068
1250
|
if (enabledRoutes.has("list") && protectedOps.list) it("GET list without auth should return 401", async () => {
|
|
1069
1251
|
const { app } = this.getOptions();
|
|
1070
|
-
|
|
1252
|
+
const res = await app.inject({
|
|
1071
1253
|
method: "GET",
|
|
1072
1254
|
url: this.getBaseUrl()
|
|
1073
|
-
})
|
|
1255
|
+
});
|
|
1256
|
+
expect(res.statusCode).toBe(401);
|
|
1074
1257
|
});
|
|
1075
1258
|
if (enabledRoutes.has("get") && protectedOps.get) it("GET /:id without auth should return 401", async () => {
|
|
1076
1259
|
const { app } = this.getOptions();
|
|
1077
|
-
|
|
1260
|
+
const res = await app.inject({
|
|
1078
1261
|
method: "GET",
|
|
1079
1262
|
url: `${this.getBaseUrl()}/000000000000000000000000`
|
|
1080
|
-
})
|
|
1263
|
+
});
|
|
1264
|
+
expect(res.statusCode).toBe(401);
|
|
1081
1265
|
});
|
|
1082
1266
|
if (enabledRoutes.has("create") && protectedOps.create) it("POST without auth should return 401", async () => {
|
|
1083
1267
|
const { app, fixtures } = this.getOptions();
|
|
1084
|
-
|
|
1268
|
+
const res = await app.inject({
|
|
1085
1269
|
method: "POST",
|
|
1086
1270
|
url: this.getBaseUrl(),
|
|
1087
1271
|
payload: fixtures.valid
|
|
1088
|
-
})
|
|
1272
|
+
});
|
|
1273
|
+
expect(res.statusCode).toBe(401);
|
|
1089
1274
|
});
|
|
1090
1275
|
if (enabledRoutes.has("update") && protectedOps.update) for (const verb of updateMethods) it(`${verb} without auth should return 401`, async () => {
|
|
1091
1276
|
const { app, fixtures } = this.getOptions();
|
|
1092
1277
|
const payload = fixtures.update ?? fixtures.valid;
|
|
1093
|
-
|
|
1278
|
+
const res = await app.inject({
|
|
1094
1279
|
method: verb,
|
|
1095
1280
|
url: `${this.getBaseUrl()}/000000000000000000000000`,
|
|
1096
1281
|
payload
|
|
1097
|
-
})
|
|
1282
|
+
});
|
|
1283
|
+
expect(res.statusCode).toBe(401);
|
|
1098
1284
|
});
|
|
1099
1285
|
if (enabledRoutes.has("delete") && protectedOps.delete) it("DELETE without auth should return 401", async () => {
|
|
1100
1286
|
const { app } = this.getOptions();
|
|
1101
|
-
|
|
1287
|
+
const res = await app.inject({
|
|
1102
1288
|
method: "DELETE",
|
|
1103
1289
|
url: `${this.getBaseUrl()}/000000000000000000000000`
|
|
1104
|
-
})
|
|
1290
|
+
});
|
|
1291
|
+
expect(res.statusCode).toBe(401);
|
|
1105
1292
|
});
|
|
1106
1293
|
if (enabledRoutes.has("list")) it("admin should access list endpoint", async () => {
|
|
1107
1294
|
const { app } = this.getOptions();
|
|
1108
|
-
|
|
1295
|
+
const res = await app.inject({
|
|
1109
1296
|
method: "GET",
|
|
1110
1297
|
url: this.getBaseUrl(),
|
|
1111
1298
|
headers: this.adminHeaders()
|
|
1112
|
-
})
|
|
1299
|
+
});
|
|
1300
|
+
expect(res.statusCode).toBeLessThan(400);
|
|
1113
1301
|
});
|
|
1114
1302
|
if (enabledRoutes.has("create")) it("admin should access create endpoint", async () => {
|
|
1115
1303
|
const { app, fixtures } = this.getOptions();
|
|
@@ -1156,6 +1344,90 @@ function createHttpTestHarness(resource, optionsOrGetter) {
|
|
|
1156
1344
|
return new HttpTestHarness(resource, optionsOrGetter);
|
|
1157
1345
|
}
|
|
1158
1346
|
//#endregion
|
|
1347
|
+
//#region src/testing/moduleSetup.ts
|
|
1348
|
+
function createTestModuleSetup() {
|
|
1349
|
+
const deferred = [];
|
|
1350
|
+
return {
|
|
1351
|
+
context: { defer(disposer) {
|
|
1352
|
+
if (typeof disposer !== "function") throw new TypeError(`[arc/testing] defer() expected a function, received ${typeof disposer}.`);
|
|
1353
|
+
deferred.push(disposer);
|
|
1354
|
+
} },
|
|
1355
|
+
deferred,
|
|
1356
|
+
async dispose() {
|
|
1357
|
+
const errors = [];
|
|
1358
|
+
for (let i = deferred.length - 1; i >= 0; i--) {
|
|
1359
|
+
const dispose = deferred[i];
|
|
1360
|
+
if (!dispose) continue;
|
|
1361
|
+
try {
|
|
1362
|
+
await dispose();
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
errors.push(err);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
deferred.length = 0;
|
|
1368
|
+
if (errors.length === 1) throw errors[0];
|
|
1369
|
+
if (errors.length > 1) throw new AggregateError(errors, `[arc/testing] ${errors.length} disposers threw`);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
//#endregion
|
|
1374
|
+
//#region src/testing/scopeRoleGate.ts
|
|
1375
|
+
/**
|
|
1376
|
+
* `scopeRoleGate` — the ONE role gate for package tests.
|
|
1377
|
+
*
|
|
1378
|
+
* ## Why this is in the framework
|
|
1379
|
+
*
|
|
1380
|
+
* Nine `@classytic/arc-*` packages had their own copy of this helper, and all nine were
|
|
1381
|
+
* wrong in the same two ways:
|
|
1382
|
+
*
|
|
1383
|
+
* 1. They read the role from an `x-role` HEADER and INSTALLED the request scope from
|
|
1384
|
+
* inside the check — so identity came from the caller's own claim, and it arrived too
|
|
1385
|
+
* late for arc's tenant-filtering pipeline to see it.
|
|
1386
|
+
* 2. They returned the pre-2.30 `{ granted: boolean }` shape, which is neither a boolean
|
|
1387
|
+
* nor an `AuthorizationDecision`. It passed through `normalizeToDecision` untouched,
|
|
1388
|
+
* arrived with `effect: undefined`, and was read as DENY — the gate said yes and arc
|
|
1389
|
+
* refused anyway. Nothing type-errored, because the gate is cast at the call site.
|
|
1390
|
+
*
|
|
1391
|
+
* Two of the nine had already been fixed by hand, each with a docstring describing this
|
|
1392
|
+
* exact discovery. The knowledge reached neither of the other seven. That is the whole
|
|
1393
|
+
* case for a shared seam: a copy does not fail, it diverges.
|
|
1394
|
+
*
|
|
1395
|
+
* A test role gate is generic — "does the verified scope hold one of these roles" has no
|
|
1396
|
+
* domain in it — so it belongs here, beside the contract it depends on.
|
|
1397
|
+
*
|
|
1398
|
+
* Identity itself comes from `testActorHeaders()` + `bootModuleApp`'s authenticator
|
|
1399
|
+
* (see `testActor.ts`), which puts the scope where production puts it. This stays a pure
|
|
1400
|
+
* predicate over what arc verified.
|
|
1401
|
+
*
|
|
1402
|
+
* Kept in its own module rather than beside `testActor.ts` so the two can be edited
|
|
1403
|
+
* independently.
|
|
1404
|
+
*/
|
|
1405
|
+
/**
|
|
1406
|
+
* Grants iff the caller is an org member holding one of `allowed` as an ORG role.
|
|
1407
|
+
*
|
|
1408
|
+
* This is `requireOrgRole` under a name package tests reach for — deliberately a
|
|
1409
|
+
* delegation, not a reimplementation. It previously read `scope.userRoles` while
|
|
1410
|
+
* requiring an organization, so it disagreed with production for any actor whose
|
|
1411
|
+
* global and org roles differ: given
|
|
1412
|
+
* `{ roles: ["employee"], orgRoles: ["manager"], organizationId: "org-1" }`,
|
|
1413
|
+
* `requireOrgRole("manager")` allowed and this denied. The tests never caught it
|
|
1414
|
+
* because `scopeFromTestActor` defaults `orgRoles` to `roles`, making the two
|
|
1415
|
+
* dimensions identical for the common actor.
|
|
1416
|
+
*
|
|
1417
|
+
* Delegating also inherits the semantics a hand-rolled gate kept missing:
|
|
1418
|
+
* elevated scopes pass, and a service (API-key) identity is refused with a
|
|
1419
|
+
* message naming `requireServiceScope` instead of silently failing a role test.
|
|
1420
|
+
*
|
|
1421
|
+
* **Pair it with an org-bearing actor.** `testActorHeaders('manager', ORG)`
|
|
1422
|
+
* yields a `member` scope and passes; `testActorHeaders('manager')` yields an
|
|
1423
|
+
* `authenticated` scope with no tenant, which is refused — arc's generated CRUD
|
|
1424
|
+
* filters by tenant, so a gate that waves a tenant-less request through is how a
|
|
1425
|
+
* green test reads another org's rows.
|
|
1426
|
+
*/
|
|
1427
|
+
function scopeRoleGate(...allowed) {
|
|
1428
|
+
return requireOrgRole(...allowed);
|
|
1429
|
+
}
|
|
1430
|
+
//#endregion
|
|
1159
1431
|
//#region src/testing/testApp.ts
|
|
1160
1432
|
/**
|
|
1161
1433
|
* createTestApp — test app factory for arc
|
|
@@ -1221,7 +1493,7 @@ function pickDefaultAuth(authMode, callerAuth) {
|
|
|
1221
1493
|
};
|
|
1222
1494
|
}
|
|
1223
1495
|
async function createTestApp(options = {}) {
|
|
1224
|
-
const { createApp } = await import("../createApp-
|
|
1496
|
+
const { createApp } = await import("../createApp-Bv0m-Q6_.mjs").then((n) => n.r);
|
|
1225
1497
|
const { resources = [], db = "in-memory", connectMongoose = false, authMode = "jwt", defaultOrgId, plugins, auth: callerAuth, ...appOptions } = options;
|
|
1226
1498
|
let dbHandle;
|
|
1227
1499
|
let dbUri;
|
|
@@ -1291,4 +1563,4 @@ function createMinimalTestApp(options = {}) {
|
|
|
1291
1563
|
});
|
|
1292
1564
|
}
|
|
1293
1565
|
//#endregion
|
|
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 };
|
|
1566
|
+
export { HttpTestHarness, TEST_ACTOR_HEADER, bootModuleApp, createBetterAuthProvider, createBetterAuthTestHelpers, createCustomAuthProvider, createDataFactory, createHttpTestHarness, createJwtAuthProvider, createMinimalTestApp, createMockController, createMockReply, createMockRepository, createMockRequest, createMockUser, createSpy, createTestApp, createTestFixtures, createTestModuleSetup, createTestTimer, expectArc, mongoMemoryDatabase, preloadResources, preloadResourcesAsync, runAuthorizationConformance, runStorageContract, safeParseBody, scopeFromTestActor, scopeRoleGate, setupBetterAuthTestApp, testActorAuth, testActorHeaders, waitFor };
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
|
|
1
2
|
import { OutboxStore } from "@classytic/primitives/outbox";
|
|
2
3
|
//#region src/testing/outboxStoreContract.d.ts
|
|
3
4
|
interface OutboxStoreContractSetupResult {
|
|
4
5
|
store: OutboxStore;
|
|
5
6
|
/** Called once after the suite. */
|
|
6
7
|
teardown?: () => Promise<void>;
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Empty the store. REQUIRED, and run before every test.
|
|
10
|
+
*
|
|
11
|
+
* Not a recommendation: several assertions are absolute rather than relative —
|
|
12
|
+
* "exactly zero pending", "exactly two", "`oldestPendingAgeMs()` is null on an
|
|
13
|
+
* empty queue". Without a reset those read whatever the previous test left, so the
|
|
14
|
+
* suite passes on ORDER rather than on behaviour, and inserting one unrelated case
|
|
15
|
+
* turns a conforming store red. A store that cannot empty itself cannot be
|
|
16
|
+
* conformance-tested.
|
|
17
|
+
*/
|
|
18
|
+
reset: () => Promise<void>;
|
|
9
19
|
}
|
|
10
20
|
type OutboxStoreContractSetup = () => Promise<OutboxStoreContractSetupResult>;
|
|
11
21
|
declare function runOutboxStoreContract(name: string, setup: OutboxStoreContractSetup): void;
|