@fuzdev/fuz_app 0.146.1 → 0.148.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 +6 -4
- package/dist/actions/action_codegen.d.ts +15 -1
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +23 -2
- package/dist/actions/perform_action.js +1 -1
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/bootstrap_account.d.ts +2 -1
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +17 -6
- package/dist/auth/cell_action_specs.d.ts +3 -1
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +5 -5
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +2 -3
- package/dist/auth/cell_data_schema.d.ts +1 -1
- package/dist/auth/cell_data_schema.js +1 -1
- package/dist/auth/cell_field_action_specs.d.ts +4 -3
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +5 -6
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +8 -1
- package/dist/auth/cell_item_action_specs.d.ts +4 -2
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +5 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +8 -1
- package/dist/auth/keyring.d.ts +35 -38
- package/dist/auth/keyring.d.ts.map +1 -1
- package/dist/auth/keyring.js +43 -50
- package/dist/auth/password_argon2.d.ts.map +1 -1
- package/dist/auth/password_argon2.js +11 -8
- package/dist/auth/role_grant_offer_action_specs.d.ts +2 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +3 -1
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +5 -2
- package/dist/db/CLAUDE.md +18 -16
- package/dist/db/cell_queries.d.ts +3 -2
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +3 -2
- package/dist/db/fact_disk_storage.d.ts +15 -15
- package/dist/db/fact_disk_storage.d.ts.map +1 -1
- package/dist/db/fact_disk_storage.js +22 -28
- package/dist/db/fact_queries.d.ts +2 -3
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +2 -3
- package/dist/db/fact_store.d.ts +20 -18
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +30 -44
- package/dist/db/file_fact_url.d.ts +19 -4
- package/dist/db/file_fact_url.d.ts.map +1 -1
- package/dist/db/file_fact_url.js +19 -4
- package/dist/primitive_schemas.d.ts +1 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +2 -1
- package/dist/schema_meta.d.ts +20 -0
- package/dist/schema_meta.d.ts.map +1 -1
- package/dist/schema_meta.js +7 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +2 -2
- package/dist/server/serve_fact_route.d.ts +9 -5
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +15 -7
- package/dist/testing/CLAUDE.md +737 -114
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +73 -5
- package/dist/testing/adversarial_input.d.ts +0 -47
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +4 -340
- package/dist/testing/adversarial_input_cases.d.ts +64 -0
- package/dist/testing/adversarial_input_cases.d.ts.map +1 -0
- package/dist/testing/adversarial_input_cases.js +348 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +2 -2
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +24 -20
- package/dist/testing/audit_drift_guard.d.ts +27 -1
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +50 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +29 -12
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/actor_lookup.js +7 -25
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -1
- package/dist/testing/cross_backend/app_settings.js +4 -6
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts +51 -0
- package/dist/testing/cross_backend/audit_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/audit_cross_helpers.js +78 -0
- package/dist/testing/cross_backend/backend_config.d.ts +2 -1
- package/dist/testing/cross_backend/backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +6 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +26 -44
- package/dist/testing/cross_backend/body_size_smuggling.d.ts +5 -0
- package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size_smuggling.js +7 -14
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +11 -1
- package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/bootstrap_backend.js +2 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts +3 -1
- package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +55 -59
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +20 -23
- package/dist/testing/cross_backend/cargo_binary.d.ts +70 -0
- package/dist/testing/cross_backend/cargo_binary.d.ts.map +1 -0
- package/dist/testing/cross_backend/cargo_binary.js +148 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +57 -1
- package/dist/testing/cross_backend/cell_gated_create.d.ts +0 -60
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +9 -487
- package/dist/testing/cross_backend/cell_grant_role.d.ts +14 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.js +22 -12
- package/dist/testing/cross_backend/cell_moderate.d.ts +66 -0
- package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_moderate.js +511 -0
- package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_ref_admission.js +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +40 -19
- package/dist/testing/cross_backend/conformance_case.d.ts +43 -4
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +29 -1
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts +93 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_rpc_auth.js +125 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +144 -4
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +297 -65
- package/dist/testing/cross_backend/cookie_attributes.d.ts.map +1 -1
- package/dist/testing/cross_backend/cookie_attributes.js +3 -2
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +38 -5
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/create_cross_backend_global_setup.js +24 -3
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +5 -5
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.d.ts +10 -6
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_deadline.js +32 -31
- package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
- package/dist/testing/cross_backend/credential_header_robustness.js +4 -6
- package/dist/testing/cross_backend/{cell_cross_helpers.d.ts → cross_rpc.d.ts} +12 -2
- package/dist/testing/cross_backend/cross_rpc.d.ts.map +1 -0
- package/dist/testing/cross_backend/{cell_cross_helpers.js → cross_rpc.js} +31 -7
- package/dist/testing/cross_backend/default_backend_configs.d.ts +56 -61
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +97 -91
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +2 -2
- package/dist/testing/cross_backend/differential.d.ts +126 -0
- package/dist/testing/cross_backend/differential.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential.js +201 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts +150 -0
- package/dist/testing/cross_backend/differential_normalize.d.ts.map +1 -0
- package/dist/testing/cross_backend/differential_normalize.js +153 -0
- package/dist/testing/cross_backend/fact_serving.d.ts +5 -0
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +41 -7
- package/dist/testing/cross_backend/full_spine_mount.d.ts +15 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +55 -1
- package/dist/testing/cross_backend/identity_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/identity_parity.js +6 -5
- package/dist/testing/cross_backend/in_process_setup.d.ts +12 -2
- package/dist/testing/cross_backend/in_process_setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/in_process_setup.js +10 -7
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +13 -18
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/origin_conformance.js +1 -1
- package/dist/testing/cross_backend/origin_scope.d.ts +6 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin_scope.js +13 -12
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +2 -5
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +14 -24
- package/dist/testing/cross_backend/postgres_database.d.ts +85 -0
- package/dist/testing/cross_backend/postgres_database.d.ts.map +1 -0
- package/dist/testing/cross_backend/postgres_database.js +140 -0
- package/dist/testing/cross_backend/provided_context.d.ts +28 -0
- package/dist/testing/cross_backend/provided_context.d.ts.map +1 -0
- package/dist/testing/cross_backend/provided_context.js +35 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +10 -12
- package/dist/testing/cross_backend/ready.js +2 -2
- package/dist/testing/cross_backend/revocation_drivers.d.ts +4 -3
- package/dist/testing/cross_backend/revocation_drivers.d.ts.map +1 -1
- package/dist/testing/cross_backend/revocation_drivers.js +1 -1
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +3 -2
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +203 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +2 -5
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +203 -111
- package/dist/testing/cross_backend/role_grant_participation.d.ts +13 -0
- package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +32 -21
- package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_role_grammar.js +6 -9
- package/dist/testing/cross_backend/rpc_adversarial.d.ts +38 -0
- package/dist/testing/cross_backend/rpc_adversarial.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_adversarial.js +292 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts +24 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.d.ts.map +1 -0
- package/dist/testing/cross_backend/rpc_success_fixtures.js +124 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts +101 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.d.ts.map +1 -0
- package/dist/testing/cross_backend/rust_spine_consumer_surface.js +113 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +10 -0
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +27 -18
- package/dist/testing/cross_backend/session_cap.d.ts +9 -8
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +9 -4
- package/dist/testing/cross_backend/session_cookie_codec.d.ts +19 -0
- package/dist/testing/cross_backend/session_cookie_codec.d.ts.map +1 -0
- package/dist/testing/cross_backend/session_cookie_codec.js +128 -0
- package/dist/testing/cross_backend/setup.d.ts +56 -54
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +29 -29
- package/dist/testing/cross_backend/simple_request.d.ts.map +1 -1
- package/dist/testing/cross_backend/simple_request.js +4 -4
- package/dist/testing/cross_backend/spawn_backend.d.ts +59 -10
- package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -1
- package/dist/testing/cross_backend/spawn_backend.js +219 -60
- package/dist/testing/cross_backend/spine_suites.d.ts +231 -0
- package/dist/testing/cross_backend/spine_suites.d.ts.map +1 -0
- package/dist/testing/cross_backend/spine_suites.js +626 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +29 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +29 -0
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +3 -4
- package/dist/testing/cross_backend/standard.d.ts +18 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +2 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +24 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +15 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts +2 -8
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +7 -10
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +10 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +21 -12
- package/dist/testing/cross_backend/testing_server_bun.js +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +1 -1
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +9 -13
- package/dist/testing/cross_backend/transport_composition.d.ts +26 -2
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -1
- package/dist/testing/cross_backend/transport_composition.js +81 -51
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +8 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +17 -10
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +2 -5
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +8 -14
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +4 -9
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_connection_cap.js +11 -12
- package/dist/testing/cross_backend/ws_endpoint.d.ts +52 -0
- package/dist/testing/cross_backend/ws_endpoint.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_endpoint.js +54 -0
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts +2 -5
- package/dist/testing/cross_backend/ws_inbound_dispatch.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_inbound_dispatch.js +9 -15
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +2 -7
- package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_outbound_overflow.js +5 -9
- package/dist/testing/cross_backend/ws_role_gated.d.ts +4 -5
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_role_gated.js +12 -21
- package/dist/testing/cross_backend/ws_round_trip.d.ts +12 -9
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +29 -34
- package/dist/testing/db_entities.d.ts +16 -3
- package/dist/testing/db_entities.d.ts.map +1 -1
- package/dist/testing/db_entities.js +28 -5
- package/dist/testing/integration_helpers.d.ts +61 -2
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +75 -18
- package/dist/testing/round_trip.d.ts +17 -0
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +67 -24
- package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
- package/dist/testing/rpc_attack_surface.js +13 -138
- package/dist/testing/rpc_envelope_cases.d.ts +111 -0
- package/dist/testing/rpc_envelope_cases.d.ts.map +1 -0
- package/dist/testing/rpc_envelope_cases.js +240 -0
- package/dist/testing/rpc_helpers.d.ts +31 -0
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +35 -14
- package/dist/testing/rpc_round_trip.d.ts +62 -6
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +151 -39
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +22 -2
- package/dist/testing/standard.d.ts +26 -0
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +4 -1
- package/dist/testing/transports/raw_http.d.ts +21 -0
- package/dist/testing/transports/raw_http.d.ts.map +1 -1
- package/dist/testing/transports/raw_http.js +30 -0
- package/package.json +10 -8
- package/dist/testing/cross_backend/actor_search.d.ts +0 -6
- package/dist/testing/cross_backend/actor_search.d.ts.map +0 -1
- package/dist/testing/cross_backend/actor_search.js +0 -90
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +0 -1
|
@@ -14,7 +14,7 @@ import '../assert_dev_env.js';
|
|
|
14
14
|
* `output` schema from the live spec registry (RPC) or `RouteSpec` (the 6
|
|
15
15
|
* REST auth routes) — the row never carries a schema. The principal the
|
|
16
16
|
* row runs `as` resolves to a `TestFixture` accessor via
|
|
17
|
-
* `
|
|
17
|
+
* `resolve_conformance_principal` — no inline credential minting.
|
|
18
18
|
*
|
|
19
19
|
* Every response also passes an always-on **no-fingerprint** invariant
|
|
20
20
|
* (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
|
|
@@ -22,16 +22,78 @@ import '../assert_dev_env.js';
|
|
|
22
22
|
* a row may pin further header expectations via `expect.headers`. Headers are
|
|
23
23
|
* deliberately kept out of the equivalence-group `{status, body}` comparison.
|
|
24
24
|
*
|
|
25
|
+
* Given `ws`, the runner sends each case as a frame on a WebSocket instead:
|
|
26
|
+
* the principal opens an admitted socket and the frame meets the per-message
|
|
27
|
+
* gates. The frame carries the same JSON-RPC envelope the HTTP body does, so
|
|
28
|
+
* the same rows hold, read through the status the HTTP binding maps the
|
|
29
|
+
* frame's error code to; the cases a socket can't carry are left out
|
|
30
|
+
* (`is_ws_conformance_case`).
|
|
31
|
+
*
|
|
32
|
+
* A case binding `request.acting` gets `params.acting` filled with a real
|
|
33
|
+
* actor id at run time (`ConformanceActingBinding`), so rows can act as a
|
|
34
|
+
* named actor of a multi-actor keeper without knowing its id.
|
|
35
|
+
*
|
|
36
|
+
* A case's RPC method resolves from the declared `rpc_endpoints` first, then
|
|
37
|
+
* from `live_rpc_endpoints` when given — the methods a backend live-mounts
|
|
38
|
+
* off the declared surface (cell verbs, opt-in resolvers, `_testing_*`
|
|
39
|
+
* backdoors), so rows can reach them too.
|
|
40
|
+
*
|
|
25
41
|
* @module
|
|
26
42
|
*/
|
|
27
43
|
import { assert, describe, test } from 'vitest';
|
|
28
44
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
29
45
|
import { find_auth_route, rest_auth_route_suffixes } from '../integration_helpers.js';
|
|
30
|
-
import { find_rpc_action, headers_to_record,
|
|
46
|
+
import { find_rpc_action, headers_to_record, parse_rpc_call_result, rpc_call_raw, resolve_rpc_endpoints_for_setup } from '../rpc_helpers.js';
|
|
31
47
|
import { account_token_create_action_spec } from '../../auth/account_action_specs.js';
|
|
48
|
+
import { jsonrpc_error_code_to_http_status } from '../../http/jsonrpc_errors.js';
|
|
49
|
+
import { create_admitted_ws_transport } from '../transports/ws_transport.js';
|
|
50
|
+
import { is_response_for } from '../transports/ws_client.js';
|
|
32
51
|
import { SCOPED_TOKEN_ADMITTED_METHOD } from './conformance_case.js';
|
|
33
52
|
import { mint_narrowed_api_token } from './narrowed_api_token.js';
|
|
34
53
|
import { xfail_until } from './xfail.js';
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the endpoints a conformance case's RPC method is looked up in: the
|
|
56
|
+
* declared `rpc_endpoints`, then `live_rpc_endpoints`. `find_rpc_action`
|
|
57
|
+
* takes the first match, so a declared method resolves from the declared
|
|
58
|
+
* surface even when the live mount repeats it.
|
|
59
|
+
*
|
|
60
|
+
* @param options - the declared and live endpoints, and the session options
|
|
61
|
+
* @returns the endpoints in lookup order
|
|
62
|
+
*/
|
|
63
|
+
export const resolve_conformance_rpc_endpoints = (options) => [
|
|
64
|
+
...resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options),
|
|
65
|
+
...(options.live_rpc_endpoints
|
|
66
|
+
? resolve_rpc_endpoints_for_setup(options.live_rpc_endpoints, options.session_options)
|
|
67
|
+
: [])
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Principals that cannot hold a socket: each presents no usable credential
|
|
71
|
+
* (`anonymous`, the browser-discarded and malformed bearers, the expired
|
|
72
|
+
* session) or one the upgrade refuses (`scoped_token` — a narrowed token is
|
|
73
|
+
* refused `surface:ws_upgrade`), so the upgrade answers before any frame.
|
|
74
|
+
* The front-door table (`describe_transport_composition_cross_tests`) pins
|
|
75
|
+
* those upgrade refusals.
|
|
76
|
+
*/
|
|
77
|
+
const WS_UNAVAILABLE_PRINCIPALS = new Set([
|
|
78
|
+
'anonymous',
|
|
79
|
+
'bearer_browser',
|
|
80
|
+
'malformed_bearer',
|
|
81
|
+
'expired_session',
|
|
82
|
+
'scoped_token'
|
|
83
|
+
]);
|
|
84
|
+
/**
|
|
85
|
+
* Whether a case can run over a WebSocket: an RPC method (not a REST auth
|
|
86
|
+
* route), sent as `POST` (a `GET` row pins the HTTP query path), pinning no
|
|
87
|
+
* header value (a frame has no headers; an absence pin holds vacuously), as
|
|
88
|
+
* a principal that can hold a socket.
|
|
89
|
+
*
|
|
90
|
+
* @param c - the case
|
|
91
|
+
* @returns whether the WS leg runs it
|
|
92
|
+
*/
|
|
93
|
+
export const is_ws_conformance_case = (c) => !c.request.method.startsWith('/') &&
|
|
94
|
+
(c.request.verb ?? 'POST') === 'POST' &&
|
|
95
|
+
!Object.values(c.expect.headers ?? {}).some((value) => value !== null) &&
|
|
96
|
+
!WS_UNAVAILABLE_PRINCIPALS.has(c.request.as);
|
|
35
97
|
/**
|
|
36
98
|
* A deliberately malformed `X-Daemon-Token` value for the `invalid_daemon`
|
|
37
99
|
* principal. Fails the `DaemonToken` Zod schema (not 43 base64url chars), so
|
|
@@ -78,20 +140,33 @@ const mint_scoped_token = async (fixture, resolved_rpc_endpoints) => {
|
|
|
78
140
|
* `role_holder` / `wrong_role` read a seeded `extra_accounts` entry named
|
|
79
141
|
* via `options.principals` (throws a clear setup error when unconfigured);
|
|
80
142
|
* `scoped_token` mints its narrowed credential over the production path.
|
|
143
|
+
* Also used outside the table by suites that send a principal's requests
|
|
144
|
+
* themselves (`describe_rpc_adversarial_cross_tests`).
|
|
145
|
+
*
|
|
146
|
+
* @param fixture - the per-test fixture
|
|
147
|
+
* @param as - the principal
|
|
148
|
+
* @param principals - usernames for `role_holder` / `wrong_role`
|
|
149
|
+
* @param resolved_rpc_endpoints - the endpoints `scoped_token` mints through
|
|
150
|
+
* @returns the transport and headers the principal sends with
|
|
81
151
|
*/
|
|
82
|
-
const
|
|
152
|
+
export const resolve_conformance_principal = async (fixture, as, principals, resolved_rpc_endpoints = []) => {
|
|
83
153
|
switch (as) {
|
|
84
154
|
case 'keeper':
|
|
85
155
|
// Keeper carries its session cookie via `create_session_headers`
|
|
86
156
|
// (in-process the transport is stateless; cross-process the jar
|
|
87
157
|
// also holds it — the explicit header is the same value).
|
|
88
|
-
return {
|
|
158
|
+
return {
|
|
159
|
+
transport: fixture.transport,
|
|
160
|
+
headers: fixture.create_session_headers(),
|
|
161
|
+
actor_id: fixture.actor.id
|
|
162
|
+
};
|
|
89
163
|
case 'daemon':
|
|
90
164
|
// Daemon-token is a non-browser credential — empty jar + no Origin.
|
|
91
165
|
return {
|
|
92
166
|
transport: fixture.fresh_transport({ origin: null }),
|
|
93
167
|
headers: fixture.create_daemon_token_headers(),
|
|
94
|
-
suppress_default_origin: true
|
|
168
|
+
suppress_default_origin: true,
|
|
169
|
+
actor_id: fixture.actor.id
|
|
95
170
|
};
|
|
96
171
|
case 'invalid_daemon':
|
|
97
172
|
// A malformed `X-Daemon-Token` carried ALONGSIDE the keeper's session
|
|
@@ -109,7 +184,8 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
109
184
|
headers: fixture.create_session_headers({
|
|
110
185
|
[DAEMON_TOKEN_HEADER]: INVALID_DAEMON_TOKEN_VALUE
|
|
111
186
|
}),
|
|
112
|
-
suppress_default_origin: true
|
|
187
|
+
suppress_default_origin: true,
|
|
188
|
+
actor_id: fixture.actor.id
|
|
113
189
|
};
|
|
114
190
|
case 'daemon_browser':
|
|
115
191
|
// A VALID `X-Daemon-Token` carried in a browser context (default Origin
|
|
@@ -124,14 +200,16 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
124
200
|
// guard), the 403 here proves the valid token was discarded, not honored.
|
|
125
201
|
return {
|
|
126
202
|
transport: fixture.transport,
|
|
127
|
-
headers: fixture.create_session_headers(fixture.create_daemon_token_headers())
|
|
203
|
+
headers: fixture.create_session_headers(fixture.create_daemon_token_headers()),
|
|
204
|
+
actor_id: fixture.actor.id
|
|
128
205
|
};
|
|
129
206
|
case 'token':
|
|
130
207
|
// Bearer is discarded in a browser context — empty jar + no Origin.
|
|
131
208
|
return {
|
|
132
209
|
transport: fixture.fresh_transport({ origin: null }),
|
|
133
210
|
headers: fixture.create_bearer_headers(),
|
|
134
|
-
suppress_default_origin: true
|
|
211
|
+
suppress_default_origin: true,
|
|
212
|
+
actor_id: fixture.actor.id
|
|
135
213
|
};
|
|
136
214
|
case 'bearer_browser':
|
|
137
215
|
// A VALID bearer api-token in a browser context (default Origin
|
|
@@ -168,7 +246,8 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
168
246
|
return {
|
|
169
247
|
transport: fixture.fresh_transport({ origin: null }),
|
|
170
248
|
headers: { authorization: `Bearer ${token}` },
|
|
171
|
-
suppress_default_origin: true
|
|
249
|
+
suppress_default_origin: true,
|
|
250
|
+
actor_id: fixture.actor.id
|
|
172
251
|
};
|
|
173
252
|
}
|
|
174
253
|
case 'anonymous':
|
|
@@ -176,7 +255,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
176
255
|
return { transport: fixture.fresh_transport(), headers: {} };
|
|
177
256
|
case 'fresh_non_admin': {
|
|
178
257
|
const account = await fixture.create_account();
|
|
179
|
-
return {
|
|
258
|
+
return {
|
|
259
|
+
transport: fixture.fresh_transport(),
|
|
260
|
+
headers: account.create_session_headers(),
|
|
261
|
+
actor_id: account.actor.id
|
|
262
|
+
};
|
|
180
263
|
}
|
|
181
264
|
case 'expired_session': {
|
|
182
265
|
// The keeper presented via an expired server-side session — fresh
|
|
@@ -184,7 +267,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
184
267
|
// minted cookie payload is valid; the backdated `auth_session` row
|
|
185
268
|
// is what the DB-row expiry gate refuses.
|
|
186
269
|
const cookie = await fixture.mint_expired_session();
|
|
187
|
-
return {
|
|
270
|
+
return {
|
|
271
|
+
transport: fixture.fresh_transport(),
|
|
272
|
+
headers: { cookie },
|
|
273
|
+
actor_id: fixture.actor.id
|
|
274
|
+
};
|
|
188
275
|
}
|
|
189
276
|
case 'role_holder':
|
|
190
277
|
case 'wrong_role': {
|
|
@@ -198,7 +285,11 @@ const resolve_principal = async (fixture, as, principals, resolved_rpc_endpoints
|
|
|
198
285
|
throw new Error(`conformance: extra_accounts['${username}'] not seeded for principal '${as}' — ` +
|
|
199
286
|
`declare it in the suite's extra_accounts option.`);
|
|
200
287
|
}
|
|
201
|
-
return {
|
|
288
|
+
return {
|
|
289
|
+
transport: fixture.fresh_transport(),
|
|
290
|
+
headers: extra.create_session_headers(),
|
|
291
|
+
actor_id: extra.actor.id
|
|
292
|
+
};
|
|
202
293
|
}
|
|
203
294
|
}
|
|
204
295
|
};
|
|
@@ -286,45 +377,198 @@ const assert_response_headers = (headers, c) => {
|
|
|
286
377
|
};
|
|
287
378
|
const is_success_status = (status) => status >= 200 && status < 300;
|
|
288
379
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
380
|
+
* Resolve a case's principal on `fixture` and send its request, asserting
|
|
381
|
+
* nothing about the response.
|
|
382
|
+
*
|
|
383
|
+
* The send half of the table runner, shared with the cross-backend
|
|
384
|
+
* differential (`describe_differential_conformance_tests`), which sends one
|
|
385
|
+
* case to two backends and compares what came back.
|
|
386
|
+
*
|
|
387
|
+
* @param c - the case
|
|
388
|
+
* @param options - the fixture, surface and endpoints to send through
|
|
389
|
+
* @returns the resolved target and the raw response
|
|
390
|
+
* @throws Error when the method is on neither the RPC surface nor the REST
|
|
391
|
+
* auth routes
|
|
292
392
|
*/
|
|
293
|
-
const
|
|
294
|
-
const fixture =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
393
|
+
export const send_conformance_case = async (c, options) => {
|
|
394
|
+
const { fixture, surface_source, principals, resolved_rpc_endpoints, ws } = options;
|
|
395
|
+
if (ws && !is_ws_conformance_case(c)) {
|
|
396
|
+
throw new Error(`conformance: case '${c.name}' cannot run over a WebSocket (is_ws_conformance_case) — ` +
|
|
397
|
+
`filter the cases before sending them on the WS leg.`);
|
|
398
|
+
}
|
|
399
|
+
const principal = await resolve_conformance_principal(fixture, c.request.as, principals, resolved_rpc_endpoints);
|
|
400
|
+
const { transport, headers, suppress_default_origin } = principal;
|
|
401
|
+
const params = c.request.acting
|
|
402
|
+
? bind_acting(c, resolve_acting_binding(c, c.request.acting, fixture, principal))
|
|
403
|
+
: c.request.params;
|
|
404
|
+
if (c.request.method.startsWith('/')) {
|
|
405
|
+
const suffix = c.request.method;
|
|
406
|
+
if (!rest_auth_route_suffixes.includes(suffix)) {
|
|
407
|
+
throw new Error(`conformance: REST method '${c.request.method}' is not a known auth-route suffix ` +
|
|
408
|
+
`(${rest_auth_route_suffixes.join(', ')}). Use an RPC method name for RPC actions.`);
|
|
409
|
+
}
|
|
410
|
+
const verb = c.request.verb ?? 'POST';
|
|
411
|
+
const route = find_auth_route(surface_source.route_specs, suffix, verb);
|
|
412
|
+
if (!route) {
|
|
413
|
+
throw new Error(`conformance: no REST route spec for ${verb} ${suffix} on the surface.`);
|
|
414
|
+
}
|
|
415
|
+
const init = {
|
|
416
|
+
method: verb,
|
|
417
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
418
|
+
...(verb !== 'GET' && params !== undefined && { body: JSON.stringify(params) })
|
|
419
|
+
};
|
|
420
|
+
const response = await transport(route.path, init);
|
|
421
|
+
const body = await response.json().catch(() => undefined);
|
|
422
|
+
return {
|
|
423
|
+
target: { kind: 'rest', route },
|
|
424
|
+
status: response.status,
|
|
425
|
+
headers: headers_to_record(response.headers),
|
|
426
|
+
body
|
|
427
|
+
};
|
|
304
428
|
}
|
|
305
|
-
return normalized;
|
|
306
|
-
};
|
|
307
|
-
/** Dispatch + assert a case targeting an RPC method. */
|
|
308
|
-
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
309
429
|
const found = find_rpc_action(resolved_rpc_endpoints, c.request.method);
|
|
310
430
|
if (!found) {
|
|
311
431
|
throw new Error(`conformance: RPC method '${c.request.method}' not found on the surface — ` +
|
|
312
|
-
`check the method name or that the action is registered on rpc_endpoints
|
|
432
|
+
`check the method name or that the action is registered on rpc_endpoints ` +
|
|
433
|
+
`(or, for a method off the declared surface, on live_rpc_endpoints).`);
|
|
434
|
+
}
|
|
435
|
+
if (ws) {
|
|
436
|
+
return {
|
|
437
|
+
target: { kind: 'rpc', ...found },
|
|
438
|
+
...(await send_ws_frame(c.request.method, params, principal, ws))
|
|
439
|
+
};
|
|
313
440
|
}
|
|
314
|
-
const
|
|
441
|
+
const raw = await rpc_call_raw({
|
|
315
442
|
app: transport,
|
|
316
443
|
path: found.path,
|
|
317
444
|
method: c.request.method,
|
|
318
|
-
params
|
|
445
|
+
params,
|
|
319
446
|
headers,
|
|
320
447
|
...(c.request.verb && { verb: c.request.verb }),
|
|
321
448
|
...(suppress_default_origin && { suppress_default_origin: true })
|
|
322
449
|
});
|
|
323
|
-
|
|
450
|
+
return { target: { kind: 'rpc', ...found }, ...raw };
|
|
451
|
+
};
|
|
452
|
+
/**
|
|
453
|
+
* The request id the WS leg sends under — the id the HTTP leg's
|
|
454
|
+
* `rpc_call_raw` sends by default, so the two envelopes read alike.
|
|
455
|
+
*/
|
|
456
|
+
const CONFORMANCE_REQUEST_ID = 'test';
|
|
457
|
+
/**
|
|
458
|
+
* Open an admitted socket as the principal, send one request frame, and read
|
|
459
|
+
* its response frame — the WS leg of `send_conformance_case`.
|
|
460
|
+
*
|
|
461
|
+
* The upgrade carries the principal's headers (its cookie, bearer or daemon
|
|
462
|
+
* token) and the `Origin` the HTTP leg would send: none for a principal that
|
|
463
|
+
* suppresses it, the target's otherwise. It also names the principal's own
|
|
464
|
+
* actor as `?acting=`. Only the TS spine resolves an actor at the upgrade,
|
|
465
|
+
* refusing a multi-actor account's upgrade without one (400
|
|
466
|
+
* `actor_required`); the Rust spine's upgrade resolves the account and
|
|
467
|
+
* ignores `?acting=`. Per-frame dispatch resolves the actor from each frame's
|
|
468
|
+
* own `acting` on both. Because the leg always sends `?acting=`, it does not
|
|
469
|
+
* cover that upgrade-level difference — the front-door table's multi-actor
|
|
470
|
+
* row does. The socket closes afterwards; a
|
|
471
|
+
* close error is not the case's subject (a request that revokes its own
|
|
472
|
+
* credential closes the socket after its reply).
|
|
473
|
+
*/
|
|
474
|
+
const send_ws_frame = async (method, params, principal, ws) => {
|
|
475
|
+
const client = await create_admitted_ws_transport({
|
|
476
|
+
base_url: ws.base_url,
|
|
477
|
+
ws_path: principal.actor_id === undefined
|
|
478
|
+
? ws.ws_path
|
|
479
|
+
: `${ws.ws_path}?acting=${encodeURIComponent(principal.actor_id)}`,
|
|
480
|
+
cookies: [],
|
|
481
|
+
headers: principal.headers,
|
|
482
|
+
origin: principal.suppress_default_origin ? null : (ws.origin ?? ws.base_url)
|
|
483
|
+
});
|
|
484
|
+
try {
|
|
485
|
+
const frame = { jsonrpc: '2.0', id: CONFORMANCE_REQUEST_ID, method };
|
|
486
|
+
if (params !== undefined && params !== null)
|
|
487
|
+
frame.params = params;
|
|
488
|
+
await client.send(frame);
|
|
489
|
+
const body = await client.wait_for(is_response_for(CONFORMANCE_REQUEST_ID));
|
|
490
|
+
const code = body.error?.code;
|
|
491
|
+
const status = body.error === undefined
|
|
492
|
+
? 200
|
|
493
|
+
: typeof code === 'number'
|
|
494
|
+
? jsonrpc_error_code_to_http_status(code)
|
|
495
|
+
: 500;
|
|
496
|
+
return { status, headers: {}, body };
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
await client.close().catch(() => { });
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* The actor id a case's `acting` binding names on this fixture.
|
|
504
|
+
*
|
|
505
|
+
* @throws Error when the binding names an actor the fixture or principal lacks
|
|
506
|
+
*/
|
|
507
|
+
const resolve_acting_binding = (c, binding, fixture, principal) => {
|
|
508
|
+
switch (binding) {
|
|
509
|
+
case 'principal':
|
|
510
|
+
if (principal.actor_id === undefined) {
|
|
511
|
+
throw new Error(`conformance: case '${c.name}' binds acting to its principal, but '${c.request.as}' ` +
|
|
512
|
+
`presents no account to take an actor from.`);
|
|
513
|
+
}
|
|
514
|
+
return principal.actor_id;
|
|
515
|
+
case 'extra_actor': {
|
|
516
|
+
const extra = fixture.extra_actors[0];
|
|
517
|
+
if (!extra) {
|
|
518
|
+
throw new Error(`conformance: case '${c.name}' binds acting to the keeper's extra actor, but the ` +
|
|
519
|
+
`fixture seeds none — declare extra_actors at setup.`);
|
|
520
|
+
}
|
|
521
|
+
return extra.id;
|
|
522
|
+
}
|
|
523
|
+
case 'keeper_actor':
|
|
524
|
+
return fixture.actor.id;
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
/** A case's params with `acting` set to `actor_id`. */
|
|
528
|
+
const bind_acting = (c, actor_id) => {
|
|
529
|
+
const { params } = c.request;
|
|
530
|
+
if (params === undefined)
|
|
531
|
+
return { acting: actor_id };
|
|
532
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
533
|
+
throw new Error(`conformance: case '${c.name}' binds acting, so its params must be an object or omitted.`);
|
|
534
|
+
}
|
|
535
|
+
if ('acting' in params) {
|
|
536
|
+
throw new Error(`conformance: case '${c.name}' binds acting and also sets params.acting — pick one.`);
|
|
537
|
+
}
|
|
538
|
+
return { ...params, acting: actor_id };
|
|
539
|
+
};
|
|
540
|
+
/**
|
|
541
|
+
* Run one conformance case end-to-end: resolve the principal, dispatch the
|
|
542
|
+
* request, and assert the expected status / reason / fields. Returns the
|
|
543
|
+
* normalized response so the caller can feed an equivalence group.
|
|
544
|
+
*/
|
|
545
|
+
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
546
|
+
const sent = await send_conformance_case(c, {
|
|
547
|
+
fixture: await options.setup_test(),
|
|
548
|
+
surface_source: options.surface_source,
|
|
549
|
+
...(options.principals && { principals: options.principals }),
|
|
550
|
+
resolved_rpc_endpoints,
|
|
551
|
+
...(options.ws && { ws: options.ws })
|
|
552
|
+
});
|
|
553
|
+
assert_response_headers(sent.headers, c);
|
|
554
|
+
const normalized = sent.target.kind === 'rest'
|
|
555
|
+
? assert_rest_case(c, sent.target.route, sent)
|
|
556
|
+
: assert_rpc_case(c, sent.target.action, sent);
|
|
557
|
+
// Negative-space field check, uniform across REST + RPC and success + error
|
|
558
|
+
// bodies: a sensitive column (`password_hash`, `token_hash`) must not appear
|
|
559
|
+
// anywhere in what the prober sees.
|
|
560
|
+
if (c.expect.absent_fields) {
|
|
561
|
+
assert_absent_fields(normalized.body, c.expect.absent_fields, c.name);
|
|
562
|
+
}
|
|
563
|
+
return normalized;
|
|
564
|
+
};
|
|
565
|
+
/** Assert a case targeting an RPC method. */
|
|
566
|
+
const assert_rpc_case = (c, action, sent) => {
|
|
567
|
+
const res = parse_rpc_call_result(sent, c.request.method);
|
|
324
568
|
if (is_success_status(c.expect.status)) {
|
|
325
569
|
assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
|
|
326
570
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
|
|
327
|
-
const parsed =
|
|
571
|
+
const parsed = action.spec.output.safeParse(res.result);
|
|
328
572
|
assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
329
573
|
if (c.expect.fields)
|
|
330
574
|
assert_fields(res.result, c.expect.fields, c.name);
|
|
@@ -347,28 +591,10 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
347
591
|
assert_fields(res.error.data, c.expect.fields, c.name);
|
|
348
592
|
return { status: res.status, body: res.error };
|
|
349
593
|
};
|
|
350
|
-
/**
|
|
351
|
-
const
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
throw new Error(`conformance: REST method '${c.request.method}' is not a known auth-route suffix ` +
|
|
355
|
-
`(${rest_auth_route_suffixes.join(', ')}). Use an RPC method name for RPC actions.`);
|
|
356
|
-
}
|
|
357
|
-
const verb = c.request.verb ?? 'POST';
|
|
358
|
-
const route = find_auth_route(options.surface_source.route_specs, suffix, verb);
|
|
359
|
-
if (!route) {
|
|
360
|
-
throw new Error(`conformance: no REST route spec for ${verb} ${suffix} on the surface.`);
|
|
361
|
-
}
|
|
362
|
-
const init = {
|
|
363
|
-
method: verb,
|
|
364
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
365
|
-
...(verb !== 'GET' &&
|
|
366
|
-
c.request.params !== undefined && { body: JSON.stringify(c.request.params) })
|
|
367
|
-
};
|
|
368
|
-
const response = await transport(route.path, init);
|
|
369
|
-
assert_response_headers(headers_to_record(response.headers), c);
|
|
370
|
-
assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
|
|
371
|
-
const body = await response.json().catch(() => undefined);
|
|
594
|
+
/** Assert a case targeting one of the 6 REST auth routes. */
|
|
595
|
+
const assert_rest_case = (c, route, sent) => {
|
|
596
|
+
const { status, body } = sent;
|
|
597
|
+
assert.strictEqual(status, c.expect.status, `${c.name}: status`);
|
|
372
598
|
if (is_success_status(c.expect.status)) {
|
|
373
599
|
const parsed = route.output.safeParse(body);
|
|
374
600
|
assert.ok(parsed.success, `${c.name}: body does not match route output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
@@ -379,7 +605,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
379
605
|
}
|
|
380
606
|
if (c.expect.fields)
|
|
381
607
|
assert_fields(body, c.expect.fields, c.name);
|
|
382
|
-
return { status
|
|
608
|
+
return { status, body };
|
|
383
609
|
};
|
|
384
610
|
/**
|
|
385
611
|
* Register a `describe` block running every `ConformanceCase` as a
|
|
@@ -388,24 +614,30 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
388
614
|
* capabilities}` protocol.
|
|
389
615
|
*/
|
|
390
616
|
export const describe_conformance_table_tests = (options) => {
|
|
391
|
-
const resolved_rpc_endpoints =
|
|
617
|
+
const resolved_rpc_endpoints = resolve_conformance_rpc_endpoints(options);
|
|
392
618
|
// Per-group normalized responses, populated as the member case tests run
|
|
393
619
|
// and consulted by the per-group byte-identity assertions registered after
|
|
394
620
|
// the case loop — vitest runs a describe's tests in registration order, so
|
|
395
621
|
// the group assertions execute last, after every member has recorded.
|
|
396
622
|
const equivalence_groups = new Map();
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
623
|
+
const cases = options.ws ? options.cases.filter(is_ws_conformance_case) : options.cases;
|
|
624
|
+
const group_sizes = new Map();
|
|
625
|
+
for (const c of cases) {
|
|
626
|
+
const group = c.expect.equivalence_group;
|
|
627
|
+
if (group)
|
|
628
|
+
group_sizes.set(group, (group_sizes.get(group) ?? 0) + 1);
|
|
401
629
|
}
|
|
630
|
+
// a group the WS filter left a single member of has nothing to compare;
|
|
631
|
+
// on HTTP every group runs, so an undersized one still fails loud
|
|
632
|
+
const group_names = new Set([...group_sizes].filter(([, size]) => !options.ws || size >= 2).map(([group]) => group));
|
|
402
633
|
describe(options.suite_name ?? 'conformance table', () => {
|
|
403
|
-
for (const c of
|
|
634
|
+
for (const c of cases) {
|
|
404
635
|
const label = c.note ? `${c.name} — ${c.note}` : c.name;
|
|
405
636
|
const group = c.expect.equivalence_group;
|
|
637
|
+
const recorded = group !== undefined && group_names.has(group);
|
|
406
638
|
const body = async () => {
|
|
407
639
|
const normalized = await run_case(c, options, resolved_rpc_endpoints);
|
|
408
|
-
if (group) {
|
|
640
|
+
if (recorded && group) {
|
|
409
641
|
const members = equivalence_groups.get(group) ?? [];
|
|
410
642
|
members.push({ name: c.name, normalized });
|
|
411
643
|
equivalence_groups.set(group, members);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookie_attributes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cookie_attributes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cookie_attributes.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cookie_attributes.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,6DAA6D;AAC7D,MAAM,WAAW,gCAAgC;IAChD,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoED,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAmFF,CAAC"}
|
|
@@ -39,6 +39,7 @@ import '../assert_dev_env.js';
|
|
|
39
39
|
*/
|
|
40
40
|
import { describe, test, assert } from 'vitest';
|
|
41
41
|
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
42
|
+
import { SPINE_ACCOUNT_LOGIN_PATH, SPINE_ACCOUNT_LOGOUT_PATH } from './spine_surface_constants.js';
|
|
42
43
|
/**
|
|
43
44
|
* Parse a single `Set-Cookie` header value into `{name, value, attributes}`.
|
|
44
45
|
* Returns `null` for a malformed head (no `=`, empty name). Unlike the
|
|
@@ -93,8 +94,8 @@ const assert_hardened_flags = (cookie, label) => {
|
|
|
93
94
|
};
|
|
94
95
|
export const describe_cookie_attributes_cross_tests = (options) => {
|
|
95
96
|
const { setup_test, cookie_name } = options;
|
|
96
|
-
const login_path = options.login_path ??
|
|
97
|
-
const logout_path = options.logout_path ??
|
|
97
|
+
const login_path = options.login_path ?? SPINE_ACCOUNT_LOGIN_PATH;
|
|
98
|
+
const logout_path = options.logout_path ?? SPINE_ACCOUNT_LOGOUT_PATH;
|
|
98
99
|
// Fresh-keeper-per-test wipes the DB between tests, so a literal username
|
|
99
100
|
// never collides (see `setup.ts`). Each test mints it fresh.
|
|
100
101
|
const username = 'cookie_attr_user';
|
|
@@ -6,8 +6,8 @@ import '../assert_dev_env.js';
|
|
|
6
6
|
* sets its own `test.name`, and this factory derives the backend name from
|
|
7
7
|
* that project name (vitest 4 passes the `TestProject` to globalSetup), picks
|
|
8
8
|
* the matching `BackendConfig`, spawns + bootstraps it via `bootstrap_backend`,
|
|
9
|
-
* and `provide`s a serializable handle that `*.cross.test.ts` files
|
|
10
|
-
* `
|
|
9
|
+
* and `provide`s a serializable handle that `*.cross.test.ts` files read
|
|
10
|
+
* with `inject_backend_handle` (`provided_context.ts`).
|
|
11
11
|
*
|
|
12
12
|
* A consumer's `global_setup.ts` collapses to:
|
|
13
13
|
*
|
|
@@ -15,10 +15,11 @@ import '../assert_dev_env.js';
|
|
|
15
15
|
* import {create_cross_backend_global_setup} from
|
|
16
16
|
* '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts';
|
|
17
17
|
* import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
|
|
18
|
-
* import './cross_test_types.js'; // augments inject('backend_handle')
|
|
19
18
|
*
|
|
20
19
|
* export default create_cross_backend_global_setup({
|
|
21
20
|
* configs: {deno: deno_backend_config, rust: rust_backend_config},
|
|
21
|
+
* ensure_databases: true,
|
|
22
|
+
* before_spawn: {rust: () => prepare_cargo_binary(my_cargo_binary)},
|
|
22
23
|
* });
|
|
23
24
|
* ```
|
|
24
25
|
*
|
|
@@ -31,6 +32,7 @@ import '../assert_dev_env.js';
|
|
|
31
32
|
*/
|
|
32
33
|
import type { TestProject } from 'vitest/node';
|
|
33
34
|
import type { BackendConfig } from './backend_config.js';
|
|
35
|
+
import { bootstrap_backend } from './bootstrap_backend.js';
|
|
34
36
|
export interface CrossBackendGlobalSetupOptions {
|
|
35
37
|
/**
|
|
36
38
|
* Map of derived backend name → `BackendConfig` factory. The derived
|
|
@@ -45,13 +47,44 @@ export interface CrossBackendGlobalSetupOptions {
|
|
|
45
47
|
readonly derive_name?: (project_name: string) => string;
|
|
46
48
|
/**
|
|
47
49
|
* Key passed to `project.provide` (and read by `inject` in test files).
|
|
48
|
-
* Default `'backend_handle'
|
|
50
|
+
* Default `'backend_handle'`, which `provided_context.ts` declares on vitest's
|
|
51
|
+
* `ProvidedContext`; augment it yourself for another key.
|
|
49
52
|
*/
|
|
50
53
|
readonly provide_key?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Create the database the selected config's `env.DATABASE_URL` names, on
|
|
56
|
+
* the server it names, when it is missing and the URL is `postgres:`
|
|
57
|
+
* (`ensure_database_for_url` — idempotent `createdb`; a URL like
|
|
58
|
+
* `memory://` is left alone). Off by default, so a test role without
|
|
59
|
+
* `CREATEDB` is never required unless the setup asks.
|
|
60
|
+
*
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
readonly ensure_databases?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Hooks run before a backend's config is built and spawned, by backend
|
|
66
|
+
* name — the Cargo build of a Rust binary (`prepare_cargo_binary`), so it
|
|
67
|
+
* runs with its output shown and outside the spawn's startup window, and
|
|
68
|
+
* the config can spawn the built binary. Runs only for the selected
|
|
69
|
+
* backend; a key naming no config throws.
|
|
70
|
+
*/
|
|
71
|
+
readonly before_spawn?: Readonly<Record<string, () => unknown>>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What `create_cross_backend_global_setup` creates databases and spawns
|
|
75
|
+
* backends with — `ensure_database_for_url` and `bootstrap_backend` by default.
|
|
76
|
+
*/
|
|
77
|
+
export interface CrossBackendGlobalSetupDeps {
|
|
78
|
+
readonly ensure_database_for_url: (database_url: string) => unknown;
|
|
79
|
+
readonly bootstrap_backend: typeof bootstrap_backend;
|
|
51
80
|
}
|
|
52
81
|
/**
|
|
53
82
|
* Build a vitest `globalSetup` default export. Returns the
|
|
54
83
|
* `(project) => teardown` function vitest 4 expects.
|
|
84
|
+
*
|
|
85
|
+
* @param options - the configs, and what runs before the spawn
|
|
86
|
+
* @param deps - the database creator and the spawn, the shipped ones by default
|
|
87
|
+
* @throws Error when a `before_spawn` key names no config
|
|
55
88
|
*/
|
|
56
|
-
export declare const create_cross_backend_global_setup: ({ configs, derive_name, provide_key }: CrossBackendGlobalSetupOptions) => ((project: TestProject) => Promise<() => Promise<void>>);
|
|
89
|
+
export declare const create_cross_backend_global_setup: ({ configs, derive_name, provide_key, ensure_databases, before_spawn }: CrossBackendGlobalSetupOptions, deps?: CrossBackendGlobalSetupDeps) => ((project: TestProject) => Promise<() => Promise<void>>);
|
|
57
90
|
//# sourceMappingURL=create_cross_backend_global_setup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_cross_backend_global_setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/create_cross_backend_global_setup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B
|
|
1
|
+
{"version":3,"file":"create_cross_backend_global_setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/create_cross_backend_global_setup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAY3D,MAAM,WAAW,8BAA8B;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,aAAa,CAAC,CAAC,CAAC;IAChE;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;OAQG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC;CAChE;AAED;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IACpE,QAAQ,CAAC,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;CACrD;AAOD;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,GAC7C,uEAMG,8BAA8B,EACjC,OAAM,2BAAqE,KACzE,CAAC,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAiCzD,CAAC"}
|