@fuzdev/fuz_app 0.147.0 → 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/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/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/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/testing/CLAUDE.md +720 -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/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/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.map +1 -1
- package/dist/testing/cross_backend/fact_serving.js +27 -17
- 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.js +1 -1
- 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 +3 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +8 -4
- 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 +6 -4
- 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
|
@@ -9,38 +9,72 @@ import './assert_dev_env.js';
|
|
|
9
9
|
* fixture-producing callback.
|
|
10
10
|
*
|
|
11
11
|
* Cadence: per-describe `setup_test()` call (see `testing/round_trip.ts` module
|
|
12
|
-
* docstring).
|
|
13
|
-
* method-direction
|
|
14
|
-
*
|
|
12
|
+
* docstring). The generic POST and GET cases fire one JSON-RPC envelope per
|
|
13
|
+
* method-direction against shared principals (an authed account, an admin
|
|
14
|
+
* account, the keeper). A side-effecting method called as the authed account
|
|
15
|
+
* runs on a fresh account of its own (`round_trip_uses_fresh_account`):
|
|
16
|
+
* the generated params of a self-targeting method act on the caller — an
|
|
17
|
+
* `account_delete` with no `account_id` deletes it, an
|
|
18
|
+
* `account_session_revoke_all` ends its session — and on the shared account
|
|
19
|
+
* that would leave every later call refused as unauthenticated. A 401 to any
|
|
20
|
+
* non-public call fails (`assert_rpc_authenticated`), so a credential lost
|
|
21
|
+
* some other way goes red rather than vacuous. The `success_fixtures` case
|
|
22
|
+
* also mutates — each fixture builds a row and the call revokes, deletes, or
|
|
23
|
+
* decides it — so it runs last on the describe's fixture, and each factory
|
|
24
|
+
* seeds its own rows.
|
|
15
25
|
*
|
|
16
26
|
* @module
|
|
17
27
|
*/
|
|
18
28
|
import { describe, test, beforeAll, assert } from 'vitest';
|
|
19
29
|
import { ROLE_ADMIN } from '../auth/role_schema.js';
|
|
20
|
-
import { JSONRPC_METHOD_NOT_FOUND, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
30
|
+
import { JSONRPC_INTERNAL_ERROR, JSONRPC_METHOD_NOT_FOUND, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
31
|
+
import { JSONRPC_ERROR_CODE_TO_HTTP_STATUS } from '../http/jsonrpc_errors.js';
|
|
21
32
|
import { generate_valid_body } from './schema_generators.js';
|
|
22
|
-
import { is_public_auth } from '../http/auth_shape.js';
|
|
23
33
|
import { create_rpc_post_init, create_rpc_get_url, assert_jsonrpc_error_response, assert_jsonrpc_success_response, resolve_rpc_endpoints_for_setup, find_rpc_action, find_rpc_method } from './rpc_helpers.js';
|
|
34
|
+
import { is_unauthenticated_refusal, pick_auth_principal, round_trip_uses_fresh_account, to_principal_headers } from './integration_helpers.js';
|
|
24
35
|
/**
|
|
25
|
-
* Pick
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
36
|
+
* Pick the principal matching an RPC method's auth requirement
|
|
37
|
+
* (`pick_auth_principal`, shared with the REST round-trip), and the headers
|
|
38
|
+
* carrying its credential. Accepts any `KeeperHeaderProvider` — both `TestApp`
|
|
39
|
+
* (in-process) and `TestFixture` (cross-backend) satisfy the shape
|
|
40
|
+
* structurally.
|
|
28
41
|
*/
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
export const pick_rpc_auth = (method, keeper, authed_account, admin_account) => {
|
|
43
|
+
const principal = pick_auth_principal(method.auth);
|
|
44
|
+
return {
|
|
45
|
+
principal,
|
|
46
|
+
headers: to_principal_headers(principal, keeper, authed_account, admin_account)
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Guard against a refusal that says the call carried no valid credential
|
|
51
|
+
* when it carried one (`is_unauthenticated_refusal`). The round-trip sends
|
|
52
|
+
* every non-public method a live credential its auth admits, so such a 401
|
|
53
|
+
* means the backend failed to authenticate it — most often because an earlier
|
|
54
|
+
* call destroyed it (a self-delete, a revoke-all), which would turn every
|
|
55
|
+
* later method's coverage into a vacuous refusal. A public method may still
|
|
56
|
+
* answer 401 (a login with a wrong password), and so may a handler refusing
|
|
57
|
+
* an authenticated call with a reason of its own (`invalid_credentials`).
|
|
58
|
+
*
|
|
59
|
+
* @param method - the RPC method name
|
|
60
|
+
* @param status - the HTTP status
|
|
61
|
+
* @param body - the parsed response body (or raw text when not JSON)
|
|
62
|
+
* @param principal - the principal the call was sent as
|
|
63
|
+
*/
|
|
64
|
+
export const assert_rpc_authenticated = (method, status, body, principal) => {
|
|
65
|
+
if (principal === 'public')
|
|
66
|
+
return;
|
|
67
|
+
const parsed = JsonrpcErrorResponse.safeParse(body);
|
|
68
|
+
const data = parsed.success ? parsed.data.error.data : undefined;
|
|
69
|
+
const reason = data !== null && typeof data === 'object' ? data.reason : undefined;
|
|
70
|
+
if (!is_unauthenticated_refusal(status, reason))
|
|
71
|
+
return;
|
|
72
|
+
const detail = parsed.success
|
|
73
|
+
? `code ${parsed.data.error.code}, message ${JSON.stringify(parsed.data.error.message)}${data === undefined ? '' : `, data ${JSON.stringify(data)}`}`
|
|
74
|
+
: `body ${typeof body === 'string' ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`;
|
|
75
|
+
assert.fail(`method '${method}' answered 401 to the ${principal} principal's credential (${detail})` +
|
|
76
|
+
' — the call carried a credential its auth admits, so the backend owed an' +
|
|
77
|
+
' authenticated answer; a 401 here means that credential stopped working');
|
|
44
78
|
};
|
|
45
79
|
/**
|
|
46
80
|
* Guard against silent parity gaps: a method enumerated from the local action
|
|
@@ -62,6 +96,76 @@ const assert_method_implemented = (method, body) => {
|
|
|
62
96
|
` returned method-not-found — backend is missing this method (parity gap)`);
|
|
63
97
|
}
|
|
64
98
|
};
|
|
99
|
+
/**
|
|
100
|
+
* Read a response body as JSON, falling back to the raw text — a crashed
|
|
101
|
+
* backend may answer with a non-JSON body (an HTML or plain-text 500), and
|
|
102
|
+
* the round-trip must report that response, not a JSON parse error with no
|
|
103
|
+
* method attached.
|
|
104
|
+
*/
|
|
105
|
+
const read_rpc_body = async (res) => {
|
|
106
|
+
const text = await res.text();
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(text);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
/** The HTTP status an `internal_error` maps to, read off the shared status table. */
|
|
115
|
+
const INTERNAL_ERROR_HTTP_STATUS = JSONRPC_ERROR_CODE_TO_HTTP_STATUS[JSONRPC_INTERNAL_ERROR];
|
|
116
|
+
/**
|
|
117
|
+
* Guard against a backend that crashes on schema-valid input. A response is
|
|
118
|
+
* a crash when any of these holds:
|
|
119
|
+
*
|
|
120
|
+
* - its HTTP status is the one `internal_error` maps to (500) — what an
|
|
121
|
+
* uncaught throw and an unmapped error code both answer
|
|
122
|
+
* - its JSON-RPC code is `internal_error` (-32603)
|
|
123
|
+
* - its status is ≥500 and its body is not a well-formed JSON-RPC error
|
|
124
|
+
* envelope (an HTML or plain-text crash page)
|
|
125
|
+
*
|
|
126
|
+
* Any other ≥500 that carries a well-formed envelope with a declared code
|
|
127
|
+
* passes like a 4xx: `service_unavailable` (503), `timeout` (504), or a
|
|
128
|
+
* consumer's own code mapped in `JSONRPC_ERROR_CODE_TO_HTTP_STATUS` (a 502).
|
|
129
|
+
* Those are server conditions a handler declares on purpose — a dependency
|
|
130
|
+
* the test environment doesn't provide — while an undeclared 500 or -32603
|
|
131
|
+
* is a handler that failed where it owed an answer.
|
|
132
|
+
*
|
|
133
|
+
* @param method - the RPC method name
|
|
134
|
+
* @param status - the HTTP status
|
|
135
|
+
* @param body - the parsed response body (or raw text when not JSON)
|
|
136
|
+
* @param expected - what the call owed: a refusal (the generic nil-id loop)
|
|
137
|
+
* or a success (a declared success fixture), for the failure message
|
|
138
|
+
*/
|
|
139
|
+
const assert_not_crash = (method, status, body, expected) => {
|
|
140
|
+
const parsed = JsonrpcErrorResponse.safeParse(body);
|
|
141
|
+
const code = parsed.success ? parsed.data.error.code : null;
|
|
142
|
+
const crashed = parsed.success
|
|
143
|
+
? status === INTERNAL_ERROR_HTTP_STATUS || code === JSONRPC_INTERNAL_ERROR
|
|
144
|
+
: status >= 500;
|
|
145
|
+
if (!crashed)
|
|
146
|
+
return;
|
|
147
|
+
const message = parsed.success
|
|
148
|
+
? parsed.data.error.message
|
|
149
|
+
: typeof body === 'string'
|
|
150
|
+
? body.slice(0, 500)
|
|
151
|
+
: JSON.stringify(body).slice(0, 500);
|
|
152
|
+
assert.fail(`method '${method}' crashed on ${expected === 'success' ? 'its success fixture' : 'schema-valid params'} (status ${status}, code ${code ?? 'none'}, message ${JSON.stringify(message)})` +
|
|
153
|
+
(expected === 'success'
|
|
154
|
+
? ' — the fixture built a real row, so the backend owed a success here'
|
|
155
|
+
: ' — the backend owes a declared refusal here (a 4xx, or a declared 5xx code)'));
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Assert a non-ok response is an acceptable refusal: the method exists, the
|
|
159
|
+
* failure is not a crash (`assert_not_crash`), an authenticated call was not
|
|
160
|
+
* refused as unauthenticated (`assert_rpc_authenticated`), and the body is a
|
|
161
|
+
* well-formed JSON-RPC error envelope.
|
|
162
|
+
*/
|
|
163
|
+
const assert_rpc_refusal = (method, status, body, principal) => {
|
|
164
|
+
assert_method_implemented(method, body);
|
|
165
|
+
assert_not_crash(method, status, body, 'refusal');
|
|
166
|
+
assert_rpc_authenticated(method, status, body, principal);
|
|
167
|
+
assert_jsonrpc_error_response(body);
|
|
168
|
+
};
|
|
65
169
|
/**
|
|
66
170
|
* Run schema-driven round-trip validation for RPC endpoints.
|
|
67
171
|
*
|
|
@@ -74,10 +178,15 @@ const assert_method_implemented = (method, body) => {
|
|
|
74
178
|
*
|
|
75
179
|
* Error responses (from missing DB state, etc.) are expected and validated
|
|
76
180
|
* as well-formed JSON-RPC errors. Successful responses are validated against
|
|
77
|
-
* `action.spec.output`.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
181
|
+
* `action.spec.output`. Two error classes fail loud instead of passing as a
|
|
182
|
+
* valid envelope: `method not found` (-32601) means the backend is missing a
|
|
183
|
+
* method the local surface advertises (`assert_method_implemented`), and a
|
|
184
|
+
* crash — HTTP 500, `internal_error` (-32603), or a ≥500 with no JSON-RPC
|
|
185
|
+
* envelope — means the backend failed on schema-valid input
|
|
186
|
+
* (`assert_not_crash`). A declared ≥500 code (`service_unavailable`,
|
|
187
|
+
* `timeout`) passes as a refusal. A method the suite can't drive generically
|
|
188
|
+
* goes in `skip_methods`; one whose generated params can't reach its handler
|
|
189
|
+
* gets an `input_overrides` entry.
|
|
81
190
|
*/
|
|
82
191
|
export const describe_rpc_round_trip_tests = (options) => {
|
|
83
192
|
const skip_set = new Set(options.skip_methods);
|
|
@@ -116,18 +225,20 @@ export const describe_rpc_round_trip_tests = (options) => {
|
|
|
116
225
|
continue;
|
|
117
226
|
const override = options.input_overrides?.get(action.spec.method);
|
|
118
227
|
const params = override ?? generate_valid_body(action.spec.input) ?? null;
|
|
119
|
-
const
|
|
228
|
+
const picked = pick_rpc_auth(surface_method, fixture, authed_account, admin_account);
|
|
229
|
+
const { principal, headers } = round_trip_uses_fresh_account(picked.principal, action.spec.side_effects)
|
|
230
|
+
? pick_rpc_auth(surface_method, fixture, await fixture.create_account({ roles: [] }), admin_account)
|
|
231
|
+
: picked;
|
|
120
232
|
const init = create_rpc_post_init(action.spec.method, params);
|
|
121
233
|
Object.assign(init.headers, headers);
|
|
122
234
|
const res = await fixture.transport(ep_spec.path, init);
|
|
123
|
-
const body = await res
|
|
235
|
+
const body = await read_rpc_body(res);
|
|
124
236
|
try {
|
|
125
237
|
if (res.ok) {
|
|
126
238
|
assert_jsonrpc_success_response(body, action.spec.output);
|
|
127
239
|
}
|
|
128
240
|
else {
|
|
129
|
-
|
|
130
|
-
assert_jsonrpc_error_response(body);
|
|
241
|
+
assert_rpc_refusal(action.spec.method, res.status, body, principal);
|
|
131
242
|
}
|
|
132
243
|
}
|
|
133
244
|
catch (e) {
|
|
@@ -150,17 +261,16 @@ export const describe_rpc_round_trip_tests = (options) => {
|
|
|
150
261
|
continue;
|
|
151
262
|
const override = options.input_overrides?.get(action.spec.method);
|
|
152
263
|
const params = override ?? generate_valid_body(action.spec.input) ?? undefined;
|
|
153
|
-
const headers =
|
|
264
|
+
const { principal, headers } = pick_rpc_auth(surface_method, fixture, authed_account, admin_account);
|
|
154
265
|
const url = create_rpc_get_url(ep_spec.path, action.spec.method, params);
|
|
155
266
|
const res = await fixture.transport(url, { headers });
|
|
156
|
-
const body = await res
|
|
267
|
+
const body = await read_rpc_body(res);
|
|
157
268
|
try {
|
|
158
269
|
if (res.ok) {
|
|
159
270
|
assert_jsonrpc_success_response(body, action.spec.output);
|
|
160
271
|
}
|
|
161
272
|
else {
|
|
162
|
-
|
|
163
|
-
assert_jsonrpc_error_response(body);
|
|
273
|
+
assert_rpc_refusal(action.spec.method, res.status, body, principal);
|
|
164
274
|
}
|
|
165
275
|
}
|
|
166
276
|
catch (e) {
|
|
@@ -178,14 +288,16 @@ export const describe_rpc_round_trip_tests = (options) => {
|
|
|
178
288
|
assert.ok(located, `success_fixtures references unknown RPC method '${method}'`);
|
|
179
289
|
const surface = find_rpc_method(surface_rpc_endpoints, method);
|
|
180
290
|
assert.ok(surface, `success_fixtures method '${method}' missing from generated surface`);
|
|
181
|
-
const
|
|
182
|
-
const headers =
|
|
183
|
-
|
|
291
|
+
const fixture_case = await build(fixture);
|
|
292
|
+
const headers = fixture_case.headers ??
|
|
293
|
+
pick_rpc_auth(surface.method_spec, fixture, authed_account, admin_account).headers;
|
|
294
|
+
const init = create_rpc_post_init(method, fixture_case.params);
|
|
184
295
|
Object.assign(init.headers, headers);
|
|
185
296
|
const res = await fixture.transport(located.path, init);
|
|
186
|
-
const body = await res
|
|
297
|
+
const body = await read_rpc_body(res);
|
|
187
298
|
try {
|
|
188
299
|
assert_method_implemented(method, body);
|
|
300
|
+
assert_not_crash(method, res.status, body, 'success');
|
|
189
301
|
assert.ok(res.ok, `success fixture expected a success response, got status ${res.status}: ${JSON.stringify(body)}`);
|
|
190
302
|
assert_jsonrpc_success_response(body, located.action.spec.output);
|
|
191
303
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,WAAW,CAAC;AAInB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AAmHF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,
|
|
1
|
+
{"version":3,"file":"schema_generators.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_generators.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAKN,KAAK,YAAY,EACjB,MAAM,WAAW,CAAC;AAInB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,cAAc,CAAC,CAAC,OAAO,KAAG,MAAM,GAAG,IAkBhE,CAAC;AAmHF,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,GAAI,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,OAAO,KAAG,OA8GnF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAAI,OAAO;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,KAAG,MACL,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,gBAAgB,CAAC,CAAC,SAAS,KAAG,MAiB9E,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,cAAc,CAAC,CAAC,OAAO,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA+B5B,CAAC"}
|
|
@@ -174,8 +174,28 @@ export const generate_valid_value = (field, field_schema) => {
|
|
|
174
174
|
return '2020-01-01T00:00:00.000Z';
|
|
175
175
|
return generate_valid_string(field_schema);
|
|
176
176
|
case 'number':
|
|
177
|
-
case 'int':
|
|
178
|
-
|
|
177
|
+
case 'int': {
|
|
178
|
+
// 1, unless the schema's bounds rule it out (`.negative()`, `.min(10)`)
|
|
179
|
+
for (const candidate of [1, -1, 0]) {
|
|
180
|
+
if (field_schema.safeParse(candidate).success)
|
|
181
|
+
return candidate;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const json = z.toJSONSchema(field_schema);
|
|
185
|
+
if (typeof json.minimum === 'number')
|
|
186
|
+
return Math.ceil(json.minimum);
|
|
187
|
+
if (typeof json.exclusiveMinimum === 'number')
|
|
188
|
+
return Math.floor(json.exclusiveMinimum) + 1;
|
|
189
|
+
if (typeof json.maximum === 'number')
|
|
190
|
+
return Math.floor(json.maximum);
|
|
191
|
+
if (typeof json.exclusiveMaximum === 'number')
|
|
192
|
+
return Math.ceil(json.exclusiveMaximum) - 1;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// no bounds to read
|
|
196
|
+
}
|
|
197
|
+
return 1; // fall through — generate_valid_body will report the failure
|
|
198
|
+
}
|
|
179
199
|
case 'boolean':
|
|
180
200
|
return true;
|
|
181
201
|
case 'array': {
|
|
@@ -21,6 +21,7 @@ import type { RoleSchemaResult } from '../auth/role_schema.js';
|
|
|
21
21
|
import type { BootstrapServerOptions } from '../server/app_server.js';
|
|
22
22
|
import type { AppServerContext } from '../server/app_server_context.js';
|
|
23
23
|
import type { RouteSpec } from '../http/route_spec.js';
|
|
24
|
+
import { type RpcSuccessFixtures } from './rpc_round_trip.js';
|
|
24
25
|
import type { RpcEndpointsSuiteOption } from './rpc_helpers.js';
|
|
25
26
|
import type { BackendCapabilities } from './cross_backend/capabilities.js';
|
|
26
27
|
import type { SetupTest } from './cross_backend/setup.js';
|
|
@@ -85,6 +86,31 @@ export interface StandardTestOptions {
|
|
|
85
86
|
error_coverage_min?: number;
|
|
86
87
|
/** Override the bootstrap-success suite's synthetic token. */
|
|
87
88
|
bootstrap_token?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Forwarded to `describe_rpc_round_trip_tests` as `success_fixtures`
|
|
91
|
+
* (method name → async params factory) — drives a populated **success**
|
|
92
|
+
* body for methods that act on an existing row, validated against the
|
|
93
|
+
* method's `output` schema. `create_spine_rpc_success_fixtures`
|
|
94
|
+
* (`cross_backend/rpc_success_fixtures.ts`) builds the map for the
|
|
95
|
+
* standard spine methods. See `RpcRoundTripTestOptions.success_fixtures`.
|
|
96
|
+
*/
|
|
97
|
+
rpc_success_fixtures?: RpcSuccessFixtures;
|
|
98
|
+
/**
|
|
99
|
+
* Forwarded to `describe_rpc_round_trip_tests` as `skip_methods` — the
|
|
100
|
+
* consumer escape hatch for a method the generic round-trip can't drive at
|
|
101
|
+
* all (a handler whose test wiring stubs out the deps it reaches, or reads
|
|
102
|
+
* tables the suite's database doesn't migrate). Each entry hides the
|
|
103
|
+
* method from both the crash check and the envelope check, so name the
|
|
104
|
+
* reason beside it.
|
|
105
|
+
*/
|
|
106
|
+
rpc_skip_methods?: Array<string>;
|
|
107
|
+
/**
|
|
108
|
+
* Forwarded to `describe_rpc_round_trip_tests` as `input_overrides`
|
|
109
|
+
* (method name → params) — for a method whose generated params can't
|
|
110
|
+
* reach its handler cleanly. The response is held to the same rules as a
|
|
111
|
+
* generated one.
|
|
112
|
+
*/
|
|
113
|
+
rpc_input_overrides?: Map<string, Record<string, unknown>>;
|
|
88
114
|
}
|
|
89
115
|
/**
|
|
90
116
|
* Run the full standard test bundle — integration, admin (when `roles`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/standard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"standard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/standard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI7F,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IACnC,2CAA2C;IAC3C,UAAU,EAAE,SAAS,CAAC;IACtB;;;OAGG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B;;;;OAIG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC,uFAAuF;IACvF,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,kBAAkB,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC;IAChE;;;;;OAKG;IACH,aAAa,EAAE,uBAAuB,CAAC;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB;;;;OAIG;IACH,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,sEAAsE;IACtE,yBAAyB,CAAC,EAAE,eAAe,CAAC;IAC5C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;IAC1C;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC3D;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,SAAS,mBAAmB,KAAG,IAyDtE,CAAC"}
|
package/dist/testing/standard.js
CHANGED
|
@@ -30,7 +30,10 @@ export const describe_standard_tests = (options) => {
|
|
|
30
30
|
setup_test: options.setup_test,
|
|
31
31
|
surface_source: options.surface_source,
|
|
32
32
|
session_options: options.session_options,
|
|
33
|
-
rpc_endpoints: options.rpc_endpoints
|
|
33
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
34
|
+
skip_methods: options.rpc_skip_methods,
|
|
35
|
+
input_overrides: options.rpc_input_overrides,
|
|
36
|
+
success_fixtures: options.rpc_success_fixtures
|
|
34
37
|
});
|
|
35
38
|
describe_data_exposure_tests({
|
|
36
39
|
setup_test: options.setup_test,
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* The read window a raw exchange gets by default in the cross suites. The
|
|
4
|
+
* server holds every byte it will get when the wait starts, so a backend that
|
|
5
|
+
* answers on what it has answers at once, and one that waits for more runs the
|
|
6
|
+
* whole window.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RAW_HTTP_READ_TIMEOUT_MS = 2000;
|
|
2
9
|
/** What a raw exchange read back, and how it ended. */
|
|
3
10
|
export interface RawHttpExchange {
|
|
4
11
|
/** Every byte the server sent, `latin1`-decoded. */
|
|
@@ -53,6 +60,20 @@ export interface RawHttpResponse {
|
|
|
53
60
|
* @param request_bytes - the bytes to send, `latin1`-encoded
|
|
54
61
|
*/
|
|
55
62
|
export declare const raw_http_exchange: (base_url: string, request_bytes: string, options: RawHttpExchangeOptions) => Promise<RawHttpExchange>;
|
|
63
|
+
/**
|
|
64
|
+
* Render request headers as raw HTTP/1.1 header lines, each ending in CRLF.
|
|
65
|
+
*
|
|
66
|
+
* @param headers - header names to values, sent in iteration order
|
|
67
|
+
* @returns the lines, concatenated
|
|
68
|
+
*/
|
|
69
|
+
export declare const raw_http_header_lines: (headers: Record<string, string>) => string;
|
|
70
|
+
/**
|
|
71
|
+
* Parse a response body as JSON.
|
|
72
|
+
*
|
|
73
|
+
* @param body - the body text
|
|
74
|
+
* @returns the parsed value, or `undefined` when the body is not JSON
|
|
75
|
+
*/
|
|
76
|
+
export declare const raw_http_parse_json: (body: string) => unknown;
|
|
56
77
|
/**
|
|
57
78
|
* Count HTTP response status lines in a raw byte stream. Deliberately
|
|
58
79
|
* **unanchored** (no `^`/`m`): a second pipelined response is concatenated
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"raw_http.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/raw_http.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CACtC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,SAAS,sBAAsB,KAC7B,OAAO,CAAC,eAAe,CAwBvB,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,MACN,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAG5D,CAAC;AAyBF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,KAAG,eAAe,GAAG,IAyBvE,CAAC"}
|
|
1
|
+
{"version":3,"file":"raw_http.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/transports/raw_http.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuB9B;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC/B,oDAAoD;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,uDAAuD;IACvD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;CACtC;AAED,oCAAoC;AACpC,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,iBAAiB,GAC7B,UAAU,MAAM,EAChB,eAAe,MAAM,EACrB,SAAS,sBAAsB,KAC7B,OAAO,CAAC,eAAe,CAwBvB,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAG7D,CAAC;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,OAMlD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,GAAI,KAAK,MAAM,KAAG,MACN,CAAC;AAElD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAG5D,CAAC;AAyBF;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,KAAG,eAAe,GAAG,IAyBvE,CAAC"}
|
|
@@ -18,6 +18,13 @@ import '../assert_dev_env.js';
|
|
|
18
18
|
* @module
|
|
19
19
|
*/
|
|
20
20
|
import { connect } from 'node:net';
|
|
21
|
+
/**
|
|
22
|
+
* The read window a raw exchange gets by default in the cross suites. The
|
|
23
|
+
* server holds every byte it will get when the wait starts, so a backend that
|
|
24
|
+
* answers on what it has answers at once, and one that waits for more runs the
|
|
25
|
+
* whole window.
|
|
26
|
+
*/
|
|
27
|
+
export const RAW_HTTP_READ_TIMEOUT_MS = 2000;
|
|
21
28
|
/**
|
|
22
29
|
* Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
|
|
23
30
|
* what the server sends back until it closes the connection, a whole response
|
|
@@ -59,6 +66,29 @@ export const raw_http_exchange = (base_url, request_bytes, options) => new Promi
|
|
|
59
66
|
socket.on('error', () => { }); // EPIPE/ECONNRESET on a mid-write close is expected
|
|
60
67
|
socket.on('close', () => finish(true));
|
|
61
68
|
});
|
|
69
|
+
/**
|
|
70
|
+
* Render request headers as raw HTTP/1.1 header lines, each ending in CRLF.
|
|
71
|
+
*
|
|
72
|
+
* @param headers - header names to values, sent in iteration order
|
|
73
|
+
* @returns the lines, concatenated
|
|
74
|
+
*/
|
|
75
|
+
export const raw_http_header_lines = (headers) => Object.entries(headers)
|
|
76
|
+
.map(([name, value]) => `${name}: ${value}\r\n`)
|
|
77
|
+
.join('');
|
|
78
|
+
/**
|
|
79
|
+
* Parse a response body as JSON.
|
|
80
|
+
*
|
|
81
|
+
* @param body - the body text
|
|
82
|
+
* @returns the parsed value, or `undefined` when the body is not JSON
|
|
83
|
+
*/
|
|
84
|
+
export const raw_http_parse_json = (body) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(body);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
62
92
|
/**
|
|
63
93
|
* Count HTTP response status lines in a raw byte stream. Deliberately
|
|
64
94
|
* **unanchored** (no `^`/`m`): a second pipelined response is concatenated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.148.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
"build": "gro build",
|
|
15
15
|
"check": "gro check",
|
|
16
16
|
"test": "gro test",
|
|
17
|
+
"test:cross": "node --conditions=development src/test/cross_backend/run_cross.ts",
|
|
17
18
|
"test:cross:ts": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_ts_node --project cross_backend_ts_deno --project cross_backend_ts_bun",
|
|
18
19
|
"test:cross:rust-spine-stub": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_rust_spine_stub",
|
|
19
20
|
"test:cross:parity": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_parity",
|
|
20
21
|
"test:cross:security": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_security",
|
|
22
|
+
"test:cross:ts-differential": "FUZ_TEST_CROSS_BACKEND=1 vitest run --project cross_backend_ts_differential",
|
|
21
23
|
"benchmark:cross-impl": "gro run src/benchmarks/cross_impl.bench.ts",
|
|
22
24
|
"preview": "vite preview",
|
|
23
25
|
"deploy": "gro deploy"
|
|
@@ -33,7 +35,7 @@
|
|
|
33
35
|
"peerDependencies": {
|
|
34
36
|
"@electric-sql/pglite": ">=0.4",
|
|
35
37
|
"@fuzdev/blake3-wasm": ">=0.2.0",
|
|
36
|
-
"@fuzdev/fuz_css": ">=0.
|
|
38
|
+
"@fuzdev/fuz_css": ">=0.71.1",
|
|
37
39
|
"@fuzdev/fuz_schema": "^0.5.0",
|
|
38
40
|
"@fuzdev/fuz_util": ">=0.78.0",
|
|
39
41
|
"@fuzdev/pglet-wasm": ">=0.1.2",
|
|
@@ -92,11 +94,11 @@
|
|
|
92
94
|
"@electric-sql/pglite": "^0.5.8",
|
|
93
95
|
"@fuzdev/blake3-wasm": "^0.2.0",
|
|
94
96
|
"@fuzdev/fuz_code": "^0.54.0",
|
|
95
|
-
"@fuzdev/fuz_css": "^0.71.
|
|
97
|
+
"@fuzdev/fuz_css": "^0.71.1",
|
|
96
98
|
"@fuzdev/fuz_schema": "^0.5.0",
|
|
97
99
|
"@fuzdev/fuz_ui": "^0.220.0",
|
|
98
100
|
"@fuzdev/fuz_util": "^0.78.0",
|
|
99
|
-
"@fuzdev/gro": "^0.
|
|
101
|
+
"@fuzdev/gro": "^0.222.0",
|
|
100
102
|
"@fuzdev/mdz": "^0.6.0",
|
|
101
103
|
"@fuzdev/pglet-wasm": "^0.1.2",
|
|
102
104
|
"@hono/bun": "^1.0.1",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import '../assert_dev_env.js';
|
|
2
|
-
import type { RpcPathCrossSuiteOptions } from './setup.js';
|
|
3
|
-
/** Options for the actor-search parity suite (the standard RPC-dispatched shape). */
|
|
4
|
-
export type ActorSearchCrossTestOptions = RpcPathCrossSuiteOptions;
|
|
5
|
-
export declare const describe_actor_search_cross_tests: (options: ActorSearchCrossTestOptions) => void;
|
|
6
|
-
//# sourceMappingURL=actor_search.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"actor_search.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_search.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAyDxF,CAAC"}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import '../assert_dev_env.js';
|
|
2
|
-
/**
|
|
3
|
-
* Cross-backend parity suite for `actor_search`.
|
|
4
|
-
*
|
|
5
|
-
* `actor_search` is an opt-in case-insensitive prefix search over
|
|
6
|
-
* `actor.name` (`{query, scope_ids?, limit?} → {actors: [{id, username,
|
|
7
|
-
* display_name?}]}`), not folded into the standard bundle. Like
|
|
8
|
-
* `actor_lookup` / cells, it's live-mounted on the spine RPC path but kept
|
|
9
|
-
* off the declared surface, so this dedicated suite is its validator. The
|
|
10
|
-
* security property under test is the **empty-`scope_ids` admin gate**:
|
|
11
|
-
*
|
|
12
|
-
* - **anonymous → 401** — the account-grain auth gate refuses an
|
|
13
|
-
* unauthenticated caller before the handler runs.
|
|
14
|
-
* - **non-admin + no `scope_ids` → 400** `actor_search_scope_required` — an
|
|
15
|
-
* unbounded global search is admin-only; a non-admin must scope the query.
|
|
16
|
-
* This is the core security assertion, exercised against each impl's real
|
|
17
|
-
* auth resolution.
|
|
18
|
-
* - **non-admin + `scope_ids` → 200** — passing a scope bypasses the admin
|
|
19
|
-
* requirement (results are filtered to actors holding active role_grants on
|
|
20
|
-
* those scopes); an unheld scope simply yields an empty result, not a
|
|
21
|
-
* rejection — proving the gate keys on `scope_ids` presence, not identity.
|
|
22
|
-
* - **admin + no `scope_ids` → 200** — the admin path reaches the unbounded
|
|
23
|
-
* search.
|
|
24
|
-
*
|
|
25
|
-
* Cites `security.md` §Authorization (the `actor_search` scope gate).
|
|
26
|
-
*
|
|
27
|
-
* Runs both legs via the shared `{setup_test}` protocol: in-process
|
|
28
|
-
* (`auth/actor_search_parity.db.test.ts`) + cross-process
|
|
29
|
-
* (`cross_backend/actor_search.cross.test.ts`, TS spine binaries + Rust
|
|
30
|
-
* `testing_spine_stub`). Mounted on every spine, so the suite is ungated.
|
|
31
|
-
*
|
|
32
|
-
* @module
|
|
33
|
-
*/
|
|
34
|
-
import { describe, test, assert } from 'vitest';
|
|
35
|
-
import { actor_search_action_spec, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED } from '../../auth/actor_search_action_specs.js';
|
|
36
|
-
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
37
|
-
/** Nil UUID — an unheld scope id (no actor holds a role_grant on it). */
|
|
38
|
-
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
39
|
-
/** Build the JSON-RPC envelope body for an `actor_search` call. */
|
|
40
|
-
const search_envelope = (params, id) => JSON.stringify({ jsonrpc: '2.0', method: actor_search_action_spec.method, params, id });
|
|
41
|
-
export const describe_actor_search_cross_tests = (options) => {
|
|
42
|
-
const { setup_test } = options;
|
|
43
|
-
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
44
|
-
describe('actor_search parity', () => {
|
|
45
|
-
test('anonymous → 401 (account-grain auth gate)', async () => {
|
|
46
|
-
const fixture = await setup_test();
|
|
47
|
-
const res = await fixture.fresh_transport()(rpc_path, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: { 'content-type': 'application/json' },
|
|
50
|
-
body: search_envelope({ query: 'a' }, 'anon-search')
|
|
51
|
-
});
|
|
52
|
-
assert.strictEqual(res.status, 401, 'unauthenticated actor_search must be refused');
|
|
53
|
-
});
|
|
54
|
-
test('non-admin + no scope_ids → 400 actor_search_scope_required', async () => {
|
|
55
|
-
const fixture = await setup_test();
|
|
56
|
-
const account = await fixture.create_account();
|
|
57
|
-
const res = await fixture.fresh_transport()(rpc_path, {
|
|
58
|
-
method: 'POST',
|
|
59
|
-
headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
|
|
60
|
-
body: search_envelope({ query: 'a' }, 'nonadmin-noscope')
|
|
61
|
-
});
|
|
62
|
-
assert.strictEqual(res.status, 400, 'non-admin unbounded search must be rejected');
|
|
63
|
-
const body = (await res.json());
|
|
64
|
-
assert.strictEqual(body.error?.data?.reason, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, 'rejection carries the scope-required reason');
|
|
65
|
-
});
|
|
66
|
-
test('non-admin + scope_ids → 200 (scope bypasses admin gate)', async () => {
|
|
67
|
-
const fixture = await setup_test();
|
|
68
|
-
const account = await fixture.create_account();
|
|
69
|
-
const res = await fixture.fresh_transport()(rpc_path, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
|
|
72
|
-
body: search_envelope({ query: 'a', scope_ids: [NIL_UUID] }, 'nonadmin-scope')
|
|
73
|
-
});
|
|
74
|
-
assert.strictEqual(res.status, 200, 'non-admin with a scope filter is allowed');
|
|
75
|
-
const body = (await res.json());
|
|
76
|
-
assert(Array.isArray(body.result?.actors), 'response carries an actors array');
|
|
77
|
-
});
|
|
78
|
-
test('admin + no scope_ids → 200 (admin reaches unbounded search)', async () => {
|
|
79
|
-
const fixture = await setup_test();
|
|
80
|
-
const res = await fixture.transport(rpc_path, {
|
|
81
|
-
method: 'POST',
|
|
82
|
-
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
83
|
-
body: search_envelope({ query: 'a' }, 'admin-noscope')
|
|
84
|
-
});
|
|
85
|
-
assert.strictEqual(res.status, 200, 'admin unbounded search is allowed');
|
|
86
|
-
const body = (await res.json());
|
|
87
|
-
assert(Array.isArray(body.result?.actors), 'response carries an actors array');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cell_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgB9B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAEvE,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAC1B,WAAW,cAAc,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,QAAQ,OAAO,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,SAAS,CAMnB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,KAAG,OAG/B,CAAC;AAEd;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,CAUrE,CAAC"}
|