@fuzdev/fuz_app 0.63.0 → 0.65.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/dist/actions/CLAUDE.md +525 -827
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/cancel.d.ts +2 -2
- package/dist/actions/cancel.js +3 -3
- package/dist/actions/connection_closer.d.ts +65 -0
- package/dist/actions/connection_closer.d.ts.map +1 -0
- package/dist/actions/connection_closer.js +38 -0
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/register_ws_endpoint.d.ts +12 -10
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -5
- package/dist/actions/transports_ws_auth_guard.d.ts +25 -10
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -9
- package/dist/actions/ws_endpoint_spec.d.ts +119 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
- package/dist/actions/ws_endpoint_spec.js +13 -0
- package/dist/auth/CLAUDE.md +592 -1808
- package/dist/auth/account_action_specs.d.ts +1 -1
- package/dist/auth/account_actions.d.ts +13 -0
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +31 -1
- package/dist/auth/account_routes.d.ts +12 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +55 -8
- package/dist/auth/account_schema.d.ts +4 -4
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_actions.d.ts +11 -0
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +25 -0
- package/dist/auth/api_token_queries.js +1 -1
- package/dist/auth/audit_emitter.d.ts +56 -12
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +38 -12
- package/dist/auth/audit_log_ddl.d.ts +1 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +1 -1
- package/dist/auth/audit_log_schema.d.ts +5 -3
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +5 -3
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +1 -5
- package/dist/auth/bootstrap_routes.d.ts +8 -2
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +15 -11
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/keyring.d.ts +6 -6
- package/dist/auth/keyring.js +8 -8
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +4 -2
- package/dist/auth/signup_routes.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.d.ts +1 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -0
- package/dist/db/create_db.d.ts.map +1 -1
- package/dist/db/create_db.js +13 -0
- package/dist/dev/setup.d.ts +2 -2
- package/dist/dev/setup.js +3 -3
- package/dist/http/CLAUDE.md +225 -483
- package/dist/http/error_schemas.d.ts +0 -4
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +0 -4
- package/dist/http/ip_canonical.d.ts +100 -0
- package/dist/http/ip_canonical.d.ts.map +1 -0
- package/dist/http/ip_canonical.js +195 -0
- package/dist/http/origin.d.ts +14 -6
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +14 -32
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/http/proxy.d.ts +13 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +15 -23
- package/dist/http/surface.d.ts +50 -0
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +27 -1
- package/dist/primitive_schemas.d.ts +20 -4
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +25 -4
- package/dist/realtime/sse_auth_guard.d.ts +16 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +15 -3
- package/dist/runtime/mock.js +1 -1
- package/dist/server/app_backend.d.ts +66 -19
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +57 -34
- package/dist/server/app_server.d.ts +101 -10
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +105 -6
- package/dist/server/env.d.ts +7 -7
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +14 -14
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +12 -0
- package/dist/server/static.d.ts +4 -4
- package/dist/server/static.js +7 -7
- package/dist/testing/CLAUDE.md +269 -59
- package/dist/testing/admin_integration.d.ts +18 -23
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +159 -202
- package/dist/testing/adversarial_headers.d.ts +6 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -5
- package/dist/testing/app_server.d.ts +148 -60
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +143 -54
- package/dist/testing/attack_surface.d.ts +8 -7
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +12 -8
- package/dist/testing/audit_completeness.d.ts +23 -22
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +199 -158
- package/dist/testing/audit_drift_guard.d.ts +116 -0
- package/dist/testing/audit_drift_guard.d.ts.map +1 -0
- package/dist/testing/audit_drift_guard.js +134 -0
- package/dist/testing/bootstrap_success.d.ts +28 -0
- package/dist/testing/bootstrap_success.d.ts.map +1 -0
- package/dist/testing/bootstrap_success.js +144 -0
- package/dist/testing/connection_closer_helpers.d.ts +44 -0
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
- package/dist/testing/connection_closer_helpers.js +48 -0
- package/dist/testing/cross_backend/capabilities.d.ts +64 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
- package/dist/testing/cross_backend/capabilities.js +47 -0
- package/dist/testing/cross_backend/setup.d.ts +215 -0
- package/dist/testing/cross_backend/setup.d.ts.map +1 -0
- package/dist/testing/cross_backend/setup.js +101 -0
- package/dist/testing/data_exposure.d.ts +14 -15
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +127 -146
- package/dist/testing/db_entities.d.ts +11 -1
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +13 -1
- package/dist/testing/integration.d.ts +35 -21
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +231 -293
- package/dist/testing/integration_helpers.d.ts +16 -6
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +7 -7
- package/dist/testing/mock_fs.d.ts.map +1 -1
- package/dist/testing/mock_fs.js +0 -2
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +13 -4
- package/dist/testing/role_grant_helpers.d.ts +31 -0
- package/dist/testing/role_grant_helpers.d.ts.map +1 -0
- package/dist/testing/role_grant_helpers.js +46 -0
- package/dist/testing/round_trip.d.ts +21 -16
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +65 -86
- package/dist/testing/rpc_helpers.d.ts +2 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts +24 -21
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +91 -106
- package/dist/testing/schema_introspect.d.ts +106 -0
- package/dist/testing/schema_introspect.d.ts.map +1 -0
- package/dist/testing/schema_introspect.js +123 -0
- package/dist/testing/schema_parity.d.ts +144 -0
- package/dist/testing/schema_parity.d.ts.map +1 -0
- package/dist/testing/schema_parity.js +233 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +12 -6
- package/dist/testing/standard.d.ts +57 -25
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +62 -5
- package/dist/testing/stubs.d.ts +22 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +28 -21
- package/dist/testing/surface_invariants.d.ts +66 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +103 -1
- package/dist/testing/transports/surface_source.d.ts +51 -0
- package/dist/testing/transports/surface_source.d.ts.map +1 -0
- package/dist/testing/transports/surface_source.js +19 -0
- package/dist/ui/SurfaceExplorer.svelte +161 -2
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { type AuditEmitter, type CreateAuditEmitterOptions } from '../auth/audit_emitter.js';
|
|
3
|
+
import type { AuditLogInput } from '../auth/audit_log_schema.js';
|
|
4
|
+
import type { AuditFactory } from '../server/app_backend.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register per-test `beforeEach` + `afterEach` hooks that catch any audit
|
|
7
|
+
* emission with a metadata shape that fails its `audit_metadata_schemas`
|
|
8
|
+
* entry, or an `event_type` not present in the active `AuditLogConfig`.
|
|
9
|
+
*
|
|
10
|
+
* The production validation in `query_audit_log` is fail-open — it bumps
|
|
11
|
+
* process-wide counters and proceeds, so a regression that emits an
|
|
12
|
+
* undeclared metadata field or a typo'd event-type lands a row that
|
|
13
|
+
* passes downstream queries but breaks forensics. Tests that exercise
|
|
14
|
+
* audit emits should fail loudly when this happens.
|
|
15
|
+
*
|
|
16
|
+
* Call at the top of every `describe` / `describe_db` block that fires
|
|
17
|
+
* audit writes through `deps.audit.emit`. Resets counters before each
|
|
18
|
+
* test and asserts zero on completion.
|
|
19
|
+
*
|
|
20
|
+
* Pair with `await_pending_effects: true` (the default for
|
|
21
|
+
* `create_test_app`) so fire-and-forget audit writes have completed by
|
|
22
|
+
* the time the after-each check observes counter state.
|
|
23
|
+
*/
|
|
24
|
+
export declare const install_audit_drift_guard: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* Marker pushed into a shared sequence array by an emit-recording
|
|
27
|
+
* `audit_factory`. Pair with `RecordedClose` from
|
|
28
|
+
* `connection_closer_helpers.ts` to test close-vs-emit ordering at
|
|
29
|
+
* handler call sites — see `create_emit_ordering_audit_factory` below.
|
|
30
|
+
*/
|
|
31
|
+
export interface AuditEmitMarker {
|
|
32
|
+
kind: 'emit';
|
|
33
|
+
at: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Pair returned by {@link create_recording_audit_emitter} — the
|
|
37
|
+
* `AuditEmitter` to inject as `deps.audit`, plus the shared `calls`
|
|
38
|
+
* array that records every captured emission. Both fields are live —
|
|
39
|
+
* callers read `calls` after exercising the handler to assert on the
|
|
40
|
+
* audit metadata shape.
|
|
41
|
+
*/
|
|
42
|
+
export interface RecordingAuditEmitter {
|
|
43
|
+
emitter: AuditEmitter;
|
|
44
|
+
calls: Array<AuditLogInput>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
|
|
48
|
+
* `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
|
|
49
|
+
* capture audit metadata shapes in unit tests (e.g. password change failure
|
|
50
|
+
* outcome, role-grant create denial) without standing up the full PGlite +
|
|
51
|
+
* `query_audit_log` pipeline.
|
|
52
|
+
*
|
|
53
|
+
* **Capture scope — all four production fan-out shapes.**
|
|
54
|
+
* `emit_role_grant_target` mirrors `create_audit_emitter`'s lift logic in
|
|
55
|
+
* place — `actor_id` / `account_id` / `ip` are populated from `auth` + `ctx`
|
|
56
|
+
* and the `event_type` / `outcome` / `target_*_id` / `metadata` fields
|
|
57
|
+
* forward from the input envelope. Tests asserting on role-grant-shape
|
|
58
|
+
* emissions read out of the same homogeneous `calls` array.
|
|
59
|
+
* `notify` is a no-op; `on_event_chain` is an empty array.
|
|
60
|
+
*
|
|
61
|
+
* `emit` AND `emit_pool` both append to `calls` so cleanup-sweep tests
|
|
62
|
+
* (which use `emit_pool` exclusively — see `auth/cleanup.ts`) can also
|
|
63
|
+
* read assertions off the same array.
|
|
64
|
+
*
|
|
65
|
+
* Pass `calls_ref` to write into a caller-owned array (callers that
|
|
66
|
+
* declared `const events: Array<AuditLogInput> = []` and want to keep
|
|
67
|
+
* the reference). Omit to let the helper allocate a fresh array and
|
|
68
|
+
* return it on the `calls` field of the result.
|
|
69
|
+
*
|
|
70
|
+
* The returned emitter is deliberately NOT frozen — slots stay mutable
|
|
71
|
+
* so a test can override one when it needs bespoke shape (e.g. an
|
|
72
|
+
* `emit_pool` that throws on the first call). The production
|
|
73
|
+
* `create_audit_emitter` freeze invariant exists to catch the
|
|
74
|
+
* `patch_audit_emit_capture` hot-patch footgun against the
|
|
75
|
+
* closure-captured `emit`; the recording emitter has no inner closure,
|
|
76
|
+
* so the freeze isn't load-bearing here.
|
|
77
|
+
*/
|
|
78
|
+
export declare const create_recording_audit_emitter: (calls_ref?: Array<AuditLogInput>) => RecordingAuditEmitter;
|
|
79
|
+
/**
|
|
80
|
+
* Build an `audit_factory` that produces a real `create_audit_emitter`
|
|
81
|
+
* with its `emit` decorated to push a `{kind: 'emit', at: seq.value++}`
|
|
82
|
+
* marker into a shared sequence + events array. Used by the close-vs-emit
|
|
83
|
+
* ordering test to compose against a shared sequence counter (typically
|
|
84
|
+
* `create_recording_closer(seq_ref)` capturing eager-close calls).
|
|
85
|
+
*
|
|
86
|
+
* Pass the returned factory through `create_test_app({audit_factory: …})`
|
|
87
|
+
* — the test backend invokes it with its constructed `{db, log}` and
|
|
88
|
+
* lands the decorated emitter on `backend.deps.audit`. Production
|
|
89
|
+
* handlers dereference `deps.audit.emit` at call time, so the decorator
|
|
90
|
+
* sees every subsequent handler invocation. The underlying `emit` still
|
|
91
|
+
* runs — the decorator records the call, it does not suppress side
|
|
92
|
+
* effects.
|
|
93
|
+
*
|
|
94
|
+
* **Scope — both `emit` and `emit_role_grant_target`.** The decorator
|
|
95
|
+
* is captured by `emit_role_grant_target`'s closure inside
|
|
96
|
+
* `create_audit_emitter` (and re-exposed as the outer `emit` slot), so
|
|
97
|
+
* role-grant-shape emissions land in `events_ref` alongside bare `emit`
|
|
98
|
+
* calls. `emit_pool` and `notify` are not decorated — they take
|
|
99
|
+
* `AuditLogInput` / `AuditLogEvent` directly without going through
|
|
100
|
+
* `emit`, so handler-side `emit_pool` writes (today only
|
|
101
|
+
* `auth/cleanup.ts`) skip capture. Close-firing handlers all reach for
|
|
102
|
+
* `emit` or `emit_role_grant_target`, so the ordering test sees them
|
|
103
|
+
* regardless of which entry point a future refactor picks.
|
|
104
|
+
*
|
|
105
|
+
* Optionally accept `extra_options` to thread `on_audit_event` /
|
|
106
|
+
* `audit_log_config` into the inner emitter — useful when a test wants
|
|
107
|
+
* both ordering capture and a real SSE/WS guard wired into the same
|
|
108
|
+
* emitter chain.
|
|
109
|
+
*/
|
|
110
|
+
export declare const create_emit_ordering_audit_factory: <E extends {
|
|
111
|
+
kind: string;
|
|
112
|
+
at: number;
|
|
113
|
+
}>(seq_ref: {
|
|
114
|
+
value: number;
|
|
115
|
+
}, events_ref: Array<AuditEmitMarker | E>, extra_options?: Omit<CreateAuditEmitterOptions, "db" | "log" | "emit_decorator">) => AuditFactory;
|
|
116
|
+
//# sourceMappingURL=audit_drift_guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAU7B,OAAO,EAEN,KAAK,YAAY,EACjB,KAAK,yBAAyB,EAC9B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAiB5C,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACX;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBA0BF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,kCAAkC,GAAI,CAAC,SAAS;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAC,EACtF,SAAS;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,EACxB,YAAY,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,EACtC,gBAAgB,IAAI,CAAC,yBAAyB,EAAE,IAAI,GAAG,KAAK,GAAG,gBAAgB,CAAC,KAC9E,YAWF,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { assert, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { get_audit_metadata_validation_failures, get_audit_unknown_event_type_failures, reset_audit_metadata_validation_failures, reset_audit_unknown_event_type_failures, } from '../auth/audit_log_queries.js';
|
|
4
|
+
import { create_audit_emitter, } from '../auth/audit_emitter.js';
|
|
5
|
+
/**
|
|
6
|
+
* Register per-test `beforeEach` + `afterEach` hooks that catch any audit
|
|
7
|
+
* emission with a metadata shape that fails its `audit_metadata_schemas`
|
|
8
|
+
* entry, or an `event_type` not present in the active `AuditLogConfig`.
|
|
9
|
+
*
|
|
10
|
+
* The production validation in `query_audit_log` is fail-open — it bumps
|
|
11
|
+
* process-wide counters and proceeds, so a regression that emits an
|
|
12
|
+
* undeclared metadata field or a typo'd event-type lands a row that
|
|
13
|
+
* passes downstream queries but breaks forensics. Tests that exercise
|
|
14
|
+
* audit emits should fail loudly when this happens.
|
|
15
|
+
*
|
|
16
|
+
* Call at the top of every `describe` / `describe_db` block that fires
|
|
17
|
+
* audit writes through `deps.audit.emit`. Resets counters before each
|
|
18
|
+
* test and asserts zero on completion.
|
|
19
|
+
*
|
|
20
|
+
* Pair with `await_pending_effects: true` (the default for
|
|
21
|
+
* `create_test_app`) so fire-and-forget audit writes have completed by
|
|
22
|
+
* the time the after-each check observes counter state.
|
|
23
|
+
*/
|
|
24
|
+
export const install_audit_drift_guard = () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
reset_audit_metadata_validation_failures();
|
|
27
|
+
reset_audit_unknown_event_type_failures();
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
assert.strictEqual(get_audit_metadata_validation_failures(), 0, 'audit metadata failed schema validation — see audit_log_schema.audit_metadata_schemas');
|
|
31
|
+
assert.strictEqual(get_audit_unknown_event_type_failures(), 0, 'audit emitted an unknown event_type — see AUDIT_EVENT_TYPES');
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
|
|
36
|
+
* `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
|
|
37
|
+
* capture audit metadata shapes in unit tests (e.g. password change failure
|
|
38
|
+
* outcome, role-grant create denial) without standing up the full PGlite +
|
|
39
|
+
* `query_audit_log` pipeline.
|
|
40
|
+
*
|
|
41
|
+
* **Capture scope — all four production fan-out shapes.**
|
|
42
|
+
* `emit_role_grant_target` mirrors `create_audit_emitter`'s lift logic in
|
|
43
|
+
* place — `actor_id` / `account_id` / `ip` are populated from `auth` + `ctx`
|
|
44
|
+
* and the `event_type` / `outcome` / `target_*_id` / `metadata` fields
|
|
45
|
+
* forward from the input envelope. Tests asserting on role-grant-shape
|
|
46
|
+
* emissions read out of the same homogeneous `calls` array.
|
|
47
|
+
* `notify` is a no-op; `on_event_chain` is an empty array.
|
|
48
|
+
*
|
|
49
|
+
* `emit` AND `emit_pool` both append to `calls` so cleanup-sweep tests
|
|
50
|
+
* (which use `emit_pool` exclusively — see `auth/cleanup.ts`) can also
|
|
51
|
+
* read assertions off the same array.
|
|
52
|
+
*
|
|
53
|
+
* Pass `calls_ref` to write into a caller-owned array (callers that
|
|
54
|
+
* declared `const events: Array<AuditLogInput> = []` and want to keep
|
|
55
|
+
* the reference). Omit to let the helper allocate a fresh array and
|
|
56
|
+
* return it on the `calls` field of the result.
|
|
57
|
+
*
|
|
58
|
+
* The returned emitter is deliberately NOT frozen — slots stay mutable
|
|
59
|
+
* so a test can override one when it needs bespoke shape (e.g. an
|
|
60
|
+
* `emit_pool` that throws on the first call). The production
|
|
61
|
+
* `create_audit_emitter` freeze invariant exists to catch the
|
|
62
|
+
* `patch_audit_emit_capture` hot-patch footgun against the
|
|
63
|
+
* closure-captured `emit`; the recording emitter has no inner closure,
|
|
64
|
+
* so the freeze isn't load-bearing here.
|
|
65
|
+
*/
|
|
66
|
+
export const create_recording_audit_emitter = (calls_ref) => {
|
|
67
|
+
const calls = calls_ref ?? [];
|
|
68
|
+
const emitter = {
|
|
69
|
+
emit: (_ctx, input) => {
|
|
70
|
+
calls.push(input);
|
|
71
|
+
},
|
|
72
|
+
emit_role_grant_target: (ctx, auth, input) => {
|
|
73
|
+
calls.push({
|
|
74
|
+
event_type: input.event_type,
|
|
75
|
+
actor_id: auth.actor.id,
|
|
76
|
+
account_id: auth.account.id,
|
|
77
|
+
outcome: input.outcome,
|
|
78
|
+
target_account_id: input.target_account_id,
|
|
79
|
+
target_actor_id: input.target_actor_id,
|
|
80
|
+
ip: ctx.client_ip,
|
|
81
|
+
metadata: input.metadata,
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
emit_pool: (input) => {
|
|
85
|
+
calls.push(input);
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
},
|
|
88
|
+
notify: () => undefined,
|
|
89
|
+
on_event_chain: [],
|
|
90
|
+
};
|
|
91
|
+
return { emitter, calls };
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Build an `audit_factory` that produces a real `create_audit_emitter`
|
|
95
|
+
* with its `emit` decorated to push a `{kind: 'emit', at: seq.value++}`
|
|
96
|
+
* marker into a shared sequence + events array. Used by the close-vs-emit
|
|
97
|
+
* ordering test to compose against a shared sequence counter (typically
|
|
98
|
+
* `create_recording_closer(seq_ref)` capturing eager-close calls).
|
|
99
|
+
*
|
|
100
|
+
* Pass the returned factory through `create_test_app({audit_factory: …})`
|
|
101
|
+
* — the test backend invokes it with its constructed `{db, log}` and
|
|
102
|
+
* lands the decorated emitter on `backend.deps.audit`. Production
|
|
103
|
+
* handlers dereference `deps.audit.emit` at call time, so the decorator
|
|
104
|
+
* sees every subsequent handler invocation. The underlying `emit` still
|
|
105
|
+
* runs — the decorator records the call, it does not suppress side
|
|
106
|
+
* effects.
|
|
107
|
+
*
|
|
108
|
+
* **Scope — both `emit` and `emit_role_grant_target`.** The decorator
|
|
109
|
+
* is captured by `emit_role_grant_target`'s closure inside
|
|
110
|
+
* `create_audit_emitter` (and re-exposed as the outer `emit` slot), so
|
|
111
|
+
* role-grant-shape emissions land in `events_ref` alongside bare `emit`
|
|
112
|
+
* calls. `emit_pool` and `notify` are not decorated — they take
|
|
113
|
+
* `AuditLogInput` / `AuditLogEvent` directly without going through
|
|
114
|
+
* `emit`, so handler-side `emit_pool` writes (today only
|
|
115
|
+
* `auth/cleanup.ts`) skip capture. Close-firing handlers all reach for
|
|
116
|
+
* `emit` or `emit_role_grant_target`, so the ordering test sees them
|
|
117
|
+
* regardless of which entry point a future refactor picks.
|
|
118
|
+
*
|
|
119
|
+
* Optionally accept `extra_options` to thread `on_audit_event` /
|
|
120
|
+
* `audit_log_config` into the inner emitter — useful when a test wants
|
|
121
|
+
* both ordering capture and a real SSE/WS guard wired into the same
|
|
122
|
+
* emitter chain.
|
|
123
|
+
*/
|
|
124
|
+
export const create_emit_ordering_audit_factory = (seq_ref, events_ref, extra_options) => {
|
|
125
|
+
return ({ db, log }) => create_audit_emitter({
|
|
126
|
+
...extra_options,
|
|
127
|
+
db,
|
|
128
|
+
log,
|
|
129
|
+
emit_decorator: (inner) => (ctx, input) => {
|
|
130
|
+
events_ref.push({ kind: 'emit', at: seq_ref.value++ });
|
|
131
|
+
inner(ctx, input);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import type { SessionOptions } from '../auth/session_cookie.js';
|
|
3
|
+
import type { AppServerContext, BootstrapLiveOptions } from '../server/app_server.js';
|
|
4
|
+
import type { RouteSpec } from '../http/route_spec.js';
|
|
5
|
+
import type { RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
6
|
+
/** Options for `describe_bootstrap_success_tests`. */
|
|
7
|
+
export interface BootstrapSuccessTestOptions {
|
|
8
|
+
session_options: SessionOptions<string>;
|
|
9
|
+
/** Same factory the consumer's production server uses. */
|
|
10
|
+
create_route_specs: (ctx: AppServerContext) => Array<RouteSpec>;
|
|
11
|
+
/** RPC endpoints — passed through to `create_app_server` for shape parity. */
|
|
12
|
+
rpc_endpoints?: RpcEndpointsSuiteOption;
|
|
13
|
+
/**
|
|
14
|
+
* Live bootstrap config — the suite drives `POST /bootstrap` against
|
|
15
|
+
* `bootstrap.token_path`. The suite does NOT assert on `on_bootstrap`
|
|
16
|
+
* callback invocation (Hono-coupled signature is in-process only);
|
|
17
|
+
* assertions land on observable DB state.
|
|
18
|
+
*/
|
|
19
|
+
bootstrap: BootstrapLiveOptions;
|
|
20
|
+
/** Override the synthetic token text. Default deterministic. */
|
|
21
|
+
bootstrap_token?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Run the bootstrap success-path test suite against the consumer's
|
|
25
|
+
* production-shaped wiring.
|
|
26
|
+
*/
|
|
27
|
+
export declare const describe_bootstrap_success_tests: (options: BootstrapSuccessTestOptions) => void;
|
|
28
|
+
//# sourceMappingURL=bootstrap_success.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap_success.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/bootstrap_success.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAsB7B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAC,gBAAgB,EAAE,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AACpF,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAGrD,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,kBAAkB,CAAC;AAM9D,sDAAsD;AACtD,MAAM,WAAW,2BAA2B;IAC3C,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,0DAA0D;IAC1D,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE,8EAA8E;IAC9E,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC;;;;;OAKG;IACH,SAAS,EAAE,oBAAoB,CAAC;IAChC,gEAAgE;IAChE,eAAe,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,2BAA2B,KAAG,IAyIvF,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Bootstrap success-path suite for consumer projects.
|
|
4
|
+
*
|
|
5
|
+
* Exercises `POST /bootstrap` against an empty DB (no pre-keeper, lock
|
|
6
|
+
* unflipped) through the real `bootstrap_account` flow. Asserts on
|
|
7
|
+
* observable state — account exists, `bootstrap_lock.bootstrapped` is
|
|
8
|
+
* true, audit row emitted, response body shape — rather than
|
|
9
|
+
* `on_bootstrap` callback invocation, so the suite stays cross-impl
|
|
10
|
+
* friendly when Phase 3 cross-process testing wires it against a
|
|
11
|
+
* spawned Rust backend.
|
|
12
|
+
*
|
|
13
|
+
* Folded into `describe_standard_tests` with a `bootstrap.mode === 'live'`
|
|
14
|
+
* silent-skip gate; consumers wiring live bootstrap pick up success-path
|
|
15
|
+
* coverage by default.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test, assert } from 'vitest';
|
|
20
|
+
import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_INVALID_TOKEN } from '../http/error_schemas.js';
|
|
21
|
+
import { create_test_app_for_bootstrap } from './app_server.js';
|
|
22
|
+
const DEFAULT_TEST_TOKEN = 'test-bootstrap-token-value-deterministic';
|
|
23
|
+
const TEST_USERNAME = 'keeper';
|
|
24
|
+
const TEST_PASSWORD = 'test-password-with-min-12-chars';
|
|
25
|
+
/**
|
|
26
|
+
* Run the bootstrap success-path test suite against the consumer's
|
|
27
|
+
* production-shaped wiring.
|
|
28
|
+
*/
|
|
29
|
+
export const describe_bootstrap_success_tests = (options) => {
|
|
30
|
+
const token = options.bootstrap_token ?? DEFAULT_TEST_TOKEN;
|
|
31
|
+
const route_prefix = options.bootstrap.route_prefix ?? '/api/account';
|
|
32
|
+
const bootstrap_path = `${route_prefix}/bootstrap`;
|
|
33
|
+
describe('bootstrap success path', () => {
|
|
34
|
+
test('POST /bootstrap with valid token creates the keeper account and flips the lock', async () => {
|
|
35
|
+
const test_app = await create_test_app_for_bootstrap({
|
|
36
|
+
session_options: options.session_options,
|
|
37
|
+
create_route_specs: options.create_route_specs,
|
|
38
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
39
|
+
bootstrap: options.bootstrap,
|
|
40
|
+
bootstrap_token: token,
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
const response = await test_app.app.request(bootstrap_path, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: test_app.create_request_headers({ 'content-type': 'application/json' }),
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
token,
|
|
48
|
+
username: TEST_USERNAME,
|
|
49
|
+
password: TEST_PASSWORD,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
// Response shape
|
|
53
|
+
assert.strictEqual(response.status, 200);
|
|
54
|
+
const body = (await response.json());
|
|
55
|
+
assert.strictEqual(body.ok, true);
|
|
56
|
+
assert.strictEqual(body.account.username, TEST_USERNAME);
|
|
57
|
+
assert.ok(body.account.id);
|
|
58
|
+
assert.ok(body.actor.id);
|
|
59
|
+
// Observable state: account exists in DB
|
|
60
|
+
const account = await test_app.backend.deps.db.query_one('SELECT username FROM account WHERE username = $1', [TEST_USERNAME]);
|
|
61
|
+
assert.ok(account);
|
|
62
|
+
// Observable state: bootstrap_lock flipped to true
|
|
63
|
+
const lock = await test_app.backend.deps.db.query_one('SELECT bootstrapped FROM bootstrap_lock WHERE id = 1');
|
|
64
|
+
assert.ok(lock);
|
|
65
|
+
assert.strictEqual(lock.bootstrapped, true);
|
|
66
|
+
// Observable state: audit row emitted
|
|
67
|
+
const audit_row = await test_app.backend.deps.db.query_one("SELECT event_type, account_id FROM audit_log WHERE event_type = 'bootstrap' AND outcome = 'success' LIMIT 1");
|
|
68
|
+
assert.ok(audit_row);
|
|
69
|
+
assert.strictEqual(audit_row.account_id, body.account.id);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await test_app.cleanup();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
test('second POST /bootstrap returns 403 ALREADY_BOOTSTRAPPED', async () => {
|
|
76
|
+
const test_app = await create_test_app_for_bootstrap({
|
|
77
|
+
session_options: options.session_options,
|
|
78
|
+
create_route_specs: options.create_route_specs,
|
|
79
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
80
|
+
bootstrap: options.bootstrap,
|
|
81
|
+
bootstrap_token: token,
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
// First bootstrap succeeds
|
|
85
|
+
const first = await test_app.app.request(bootstrap_path, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: test_app.create_request_headers({ 'content-type': 'application/json' }),
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
token,
|
|
90
|
+
username: TEST_USERNAME,
|
|
91
|
+
password: TEST_PASSWORD,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
assert.strictEqual(first.status, 200);
|
|
95
|
+
// Second attempt blocked by lock
|
|
96
|
+
const second = await test_app.app.request(bootstrap_path, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: test_app.create_request_headers({ 'content-type': 'application/json' }),
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
token,
|
|
101
|
+
username: 'another_user',
|
|
102
|
+
password: TEST_PASSWORD,
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
assert.strictEqual(second.status, 403);
|
|
106
|
+
const body = (await second.json());
|
|
107
|
+
assert.strictEqual(body.error, ERROR_ALREADY_BOOTSTRAPPED);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await test_app.cleanup();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
test('POST /bootstrap with wrong token returns 401 INVALID_TOKEN', async () => {
|
|
114
|
+
const test_app = await create_test_app_for_bootstrap({
|
|
115
|
+
session_options: options.session_options,
|
|
116
|
+
create_route_specs: options.create_route_specs,
|
|
117
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
118
|
+
bootstrap: options.bootstrap,
|
|
119
|
+
bootstrap_token: token,
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const response = await test_app.app.request(bootstrap_path, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: test_app.create_request_headers({ 'content-type': 'application/json' }),
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
token: 'wrong-token-value-that-does-not-match',
|
|
127
|
+
username: TEST_USERNAME,
|
|
128
|
+
password: TEST_PASSWORD,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
assert.strictEqual(response.status, 401);
|
|
132
|
+
const body = (await response.json());
|
|
133
|
+
assert.strictEqual(body.error, ERROR_INVALID_TOKEN);
|
|
134
|
+
// Observable state: lock NOT flipped (transaction rolled back on auth failure)
|
|
135
|
+
const lock = await test_app.backend.deps.db.query_one('SELECT bootstrapped FROM bootstrap_lock WHERE id = 1');
|
|
136
|
+
assert.ok(lock);
|
|
137
|
+
assert.strictEqual(lock.bootstrapped, false);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await test_app.cleanup();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import type { ConnectionCloser } from '../actions/connection_closer.js';
|
|
3
|
+
/**
|
|
4
|
+
* Record of a single `ConnectionCloser` method invocation. `at` is the
|
|
5
|
+
* value of a monotonically-increasing sequence counter at the time of
|
|
6
|
+
* the call — pair with `create_emit_ordering_audit_factory` to record both
|
|
7
|
+
* close + audit emit calls into the same sequence for ordering tests.
|
|
8
|
+
*/
|
|
9
|
+
export interface RecordedClose {
|
|
10
|
+
method: 'session' | 'token' | 'account';
|
|
11
|
+
id: string;
|
|
12
|
+
at: number;
|
|
13
|
+
}
|
|
14
|
+
export interface RecordingCloser {
|
|
15
|
+
closer: ConnectionCloser;
|
|
16
|
+
calls: Array<RecordedClose>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a `ConnectionCloser` that records every call into `calls` rather
|
|
20
|
+
* than touching real transports. Each method returns 1 ("one socket
|
|
21
|
+
* closed") regardless of whether a real socket exists — handlers
|
|
22
|
+
* typically ignore the return value.
|
|
23
|
+
*
|
|
24
|
+
* Pass `seq_ref` to share the sequence counter with a sibling
|
|
25
|
+
* `create_emit_ordering_audit_factory` so tests can pin close-vs-emit
|
|
26
|
+
* ordering at the handler call site. Without `seq_ref`, the closer
|
|
27
|
+
* uses a fresh internal counter — `at: N` values within a single test
|
|
28
|
+
* are meaningful, but cannot be compared against audit emit ordering.
|
|
29
|
+
*/
|
|
30
|
+
export declare const create_recording_closer: (seq_ref?: {
|
|
31
|
+
value: number;
|
|
32
|
+
}) => RecordingCloser;
|
|
33
|
+
/**
|
|
34
|
+
* Pin `{method, id}` on a single recorded close call without baking in
|
|
35
|
+
* the `at: N` sequence number. Use at every "did the closer fire?"
|
|
36
|
+
* assertion site; the sequence number is only meaningful for dedicated
|
|
37
|
+
* ordering tests (paired with `create_emit_ordering_audit_factory`).
|
|
38
|
+
*
|
|
39
|
+
* Throws via `assert.ok` if `call` is `undefined` — index a recorded
|
|
40
|
+
* `calls` array directly (`calls[0]`) and let this helper handle the
|
|
41
|
+
* missing-element case.
|
|
42
|
+
*/
|
|
43
|
+
export declare const assert_close_call: (call: RecordedClose | undefined, method: "session" | "token" | "account", id: string) => void;
|
|
44
|
+
//# sourceMappingURL=connection_closer_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection_closer_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/connection_closer_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,KAAG,eAkBnE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,aAAa,GAAG,SAAS,EAC/B,QAAQ,SAAS,GAAG,OAAO,GAAG,SAAS,EACvC,IAAI,MAAM,KACR,IAIF,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
import { assert } from 'vitest';
|
|
3
|
+
/**
|
|
4
|
+
* Build a `ConnectionCloser` that records every call into `calls` rather
|
|
5
|
+
* than touching real transports. Each method returns 1 ("one socket
|
|
6
|
+
* closed") regardless of whether a real socket exists — handlers
|
|
7
|
+
* typically ignore the return value.
|
|
8
|
+
*
|
|
9
|
+
* Pass `seq_ref` to share the sequence counter with a sibling
|
|
10
|
+
* `create_emit_ordering_audit_factory` so tests can pin close-vs-emit
|
|
11
|
+
* ordering at the handler call site. Without `seq_ref`, the closer
|
|
12
|
+
* uses a fresh internal counter — `at: N` values within a single test
|
|
13
|
+
* are meaningful, but cannot be compared against audit emit ordering.
|
|
14
|
+
*/
|
|
15
|
+
export const create_recording_closer = (seq_ref) => {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const seq = seq_ref ?? { value: 0 };
|
|
18
|
+
const closer = {
|
|
19
|
+
close_sockets_for_session: (id) => {
|
|
20
|
+
calls.push({ method: 'session', id, at: seq.value++ });
|
|
21
|
+
return 1;
|
|
22
|
+
},
|
|
23
|
+
close_sockets_for_token: (id) => {
|
|
24
|
+
calls.push({ method: 'token', id, at: seq.value++ });
|
|
25
|
+
return 1;
|
|
26
|
+
},
|
|
27
|
+
close_sockets_for_account: (id) => {
|
|
28
|
+
calls.push({ method: 'account', id, at: seq.value++ });
|
|
29
|
+
return 1;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return { closer, calls };
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pin `{method, id}` on a single recorded close call without baking in
|
|
36
|
+
* the `at: N` sequence number. Use at every "did the closer fire?"
|
|
37
|
+
* assertion site; the sequence number is only meaningful for dedicated
|
|
38
|
+
* ordering tests (paired with `create_emit_ordering_audit_factory`).
|
|
39
|
+
*
|
|
40
|
+
* Throws via `assert.ok` if `call` is `undefined` — index a recorded
|
|
41
|
+
* `calls` array directly (`calls[0]`) and let this helper handle the
|
|
42
|
+
* missing-element case.
|
|
43
|
+
*/
|
|
44
|
+
export const assert_close_call = (call, method, id) => {
|
|
45
|
+
assert.ok(call, 'expected a recorded close call');
|
|
46
|
+
assert.strictEqual(call.method, method);
|
|
47
|
+
assert.strictEqual(call.id, id);
|
|
48
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Optional behaviors a backend may support. Each flag's TSDoc names the
|
|
4
|
+
* tests that gate on it; add a new flag here before referencing it from
|
|
5
|
+
* a suite body, and document the gating tests inline.
|
|
6
|
+
*/
|
|
7
|
+
export interface BackendCapabilities {
|
|
8
|
+
/**
|
|
9
|
+
* Bearer token auth (`Authorization: Bearer <token>`) is wired through
|
|
10
|
+
* the backend's middleware stack. Gates the bearer-token cases in
|
|
11
|
+
* `describe_standard_integration_tests` and `describe_rate_limiting_tests`.
|
|
12
|
+
*/
|
|
13
|
+
readonly bearer_auth: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Trusted-proxy XFF parsing is wired (`X-Forwarded-For` etc.). Gates
|
|
16
|
+
* the proxy-resolution cases in `describe_standard_integration_tests`
|
|
17
|
+
* and the future cross-process proxy integration suite.
|
|
18
|
+
*/
|
|
19
|
+
readonly trusted_proxy: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Per-account login rate limiting is wired. Gates the per-account
|
|
22
|
+
* rate-limit cases in `describe_rate_limiting_tests`.
|
|
23
|
+
*/
|
|
24
|
+
readonly login_rate_limit: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* WebSocket transport is reachable end-to-end. Gates the cross-process
|
|
27
|
+
* WS round-trip suite; the in-process `describe_ws_round_trip_tests`
|
|
28
|
+
* runs against `register_action_ws` directly and ignores this flag.
|
|
29
|
+
*/
|
|
30
|
+
readonly ws: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* SSE transport is reachable end-to-end. Gates the cross-process SSE
|
|
33
|
+
* close-detection cases; in-process SSE uses the
|
|
34
|
+
* `on_audit_event` hook and ignores this flag.
|
|
35
|
+
*/
|
|
36
|
+
readonly sse: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Test has direct access to backend-internal state (keyring for
|
|
39
|
+
* signing cookies, DB pool for FK-structural raw queries). Always
|
|
40
|
+
* `true` for in-process Hono via `default_in_process_setup`; always
|
|
41
|
+
* `false` cross-process. Gates the 3 keyring reads in
|
|
42
|
+
* `describe_standard_integration_tests` (expired-cookie generation)
|
|
43
|
+
* and the FK-structural raw query in `describe_audit_completeness_tests`.
|
|
44
|
+
*/
|
|
45
|
+
readonly in_process_only: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Capability declarations for the in-process Hono transport. Every flag
|
|
49
|
+
* is `true` because in-process testing exercises the full backend with
|
|
50
|
+
* no missing optional behaviors. Cross-process consumers
|
|
51
|
+
* declare each flag explicitly per backend.
|
|
52
|
+
*/
|
|
53
|
+
export declare const in_process_capabilities: BackendCapabilities;
|
|
54
|
+
/**
|
|
55
|
+
* Conditional `test()` wrapper — registers a vitest case only when
|
|
56
|
+
* `cond` is true; otherwise registers it as `.skip` so the run still
|
|
57
|
+
* surfaces the gated coverage in the report.
|
|
58
|
+
*
|
|
59
|
+
* Thin wrapper around vitest's `test.skipIf(!cond)` with the argument
|
|
60
|
+
* order flipped to match the more readable `test_if(capabilities.bearer_auth, ...)`
|
|
61
|
+
* call pattern.
|
|
62
|
+
*/
|
|
63
|
+
export declare const test_if: (cond: boolean, name: string, fn: () => void | Promise<void>) => void;
|
|
64
|
+
//# sourceMappingURL=capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAOpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Capability vocabulary for cross-backend integration testing.
|
|
4
|
+
*
|
|
5
|
+
* Backends declare which optional behaviors they support; suite bodies
|
|
6
|
+
* call `test_if(capabilities.X, ...)` to skip cases the backend doesn't
|
|
7
|
+
* implement. No `if (config.name === 'rust')` branches anywhere — name-
|
|
8
|
+
* checking is a code smell that says capability vocabulary is missing.
|
|
9
|
+
*
|
|
10
|
+
* In-process Hono via `default_in_process_setup` declares every
|
|
11
|
+
* capability `true` (see `in_process_capabilities`). Cross-process
|
|
12
|
+
* backends opt in per-flag on their `BackendConfig`.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { test } from 'vitest';
|
|
17
|
+
/**
|
|
18
|
+
* Capability declarations for the in-process Hono transport. Every flag
|
|
19
|
+
* is `true` because in-process testing exercises the full backend with
|
|
20
|
+
* no missing optional behaviors. Cross-process consumers
|
|
21
|
+
* declare each flag explicitly per backend.
|
|
22
|
+
*/
|
|
23
|
+
export const in_process_capabilities = Object.freeze({
|
|
24
|
+
bearer_auth: true,
|
|
25
|
+
trusted_proxy: true,
|
|
26
|
+
login_rate_limit: true,
|
|
27
|
+
ws: true,
|
|
28
|
+
sse: true,
|
|
29
|
+
in_process_only: true,
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Conditional `test()` wrapper — registers a vitest case only when
|
|
33
|
+
* `cond` is true; otherwise registers it as `.skip` so the run still
|
|
34
|
+
* surfaces the gated coverage in the report.
|
|
35
|
+
*
|
|
36
|
+
* Thin wrapper around vitest's `test.skipIf(!cond)` with the argument
|
|
37
|
+
* order flipped to match the more readable `test_if(capabilities.bearer_auth, ...)`
|
|
38
|
+
* call pattern.
|
|
39
|
+
*/
|
|
40
|
+
export const test_if = (cond, name, fn) => {
|
|
41
|
+
if (cond) {
|
|
42
|
+
test(name, fn);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
test.skip(name, fn);
|
|
46
|
+
}
|
|
47
|
+
};
|