@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
|
@@ -17,11 +17,12 @@ import '../assert_dev_env.js';
|
|
|
17
17
|
* @module
|
|
18
18
|
*/
|
|
19
19
|
import { describe, assert } from 'vitest';
|
|
20
|
-
import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput,
|
|
20
|
+
import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput, ERROR_CANNOT_DELETE_KEEPER } from '../../auth/admin_action_specs.js';
|
|
21
21
|
import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED, ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
|
|
22
22
|
import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
|
|
23
|
+
import { assert_audit_metadata_fields, list_audit_events } from './audit_cross_helpers.js';
|
|
23
24
|
import { test_if } from './capabilities.js';
|
|
24
|
-
import { cross_rpc_call, error_reason, expect_output } from './
|
|
25
|
+
import { cross_rpc_call, error_reason, expect_output } from './cross_rpc.js';
|
|
25
26
|
import { assert_iso8601_seconds } from './wire_shapes.js';
|
|
26
27
|
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
27
28
|
export const describe_account_lifecycle_cross_tests = (options) => {
|
|
@@ -38,6 +39,15 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
38
39
|
assert.strictEqual(deleted.deleted, true);
|
|
39
40
|
const undeleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers), AccountUndeleteOutput);
|
|
40
41
|
assert.strictEqual(undeleted.undeleted, true);
|
|
42
|
+
// each verb audits the victim's identity snapshot, attributed to
|
|
43
|
+
// the admin (`list_audit_events` parses the metadata)
|
|
44
|
+
for (const event_type of ['account_delete', 'account_undelete']) {
|
|
45
|
+
const events = await list_audit_events(fixture, { event_type }, rpc_path);
|
|
46
|
+
const row = events.find((e) => e.outcome === 'success' && e.target_account_id === victim.account.id);
|
|
47
|
+
assert.ok(row, `${event_type} writes a success row targeting the victim`);
|
|
48
|
+
assert.strictEqual(row.account_id, fixture.account.id, event_type);
|
|
49
|
+
assert.strictEqual(row.metadata?.username, victim.account.username, `${event_type} snapshots the username`);
|
|
50
|
+
}
|
|
41
51
|
});
|
|
42
52
|
test_if(capabilities.account_lifecycle, 'purge (keeper, confirmed)', async () => {
|
|
43
53
|
const fixture = await setup_test();
|
|
@@ -46,6 +56,11 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
46
56
|
// Purge is keeper-gated: daemon-token credential, not a session.
|
|
47
57
|
const purged = expect_output(await cross_rpc_call(t, rpc_path, 'account_purge', { account_id: victim.account.id, confirm: true }, fixture.create_daemon_token_headers()), AccountPurgeOutput);
|
|
48
58
|
assert.strictEqual(purged.purged, true);
|
|
59
|
+
// the purge row outlives the account it names, carrying its identity
|
|
60
|
+
const events = await list_audit_events(fixture, { event_type: 'account_purge' }, rpc_path);
|
|
61
|
+
const row = events.find((e) => e.outcome === 'success' && e.target_account_id === victim.account.id);
|
|
62
|
+
assert.ok(row, 'account_purge writes a success row targeting the victim');
|
|
63
|
+
assert.strictEqual(row.metadata?.username, victim.account.username, 'account_purge snapshots the username');
|
|
49
64
|
});
|
|
50
65
|
test_if(capabilities.account_lifecycle, 'keeper guard: delete + purge refuse the keeper account', async () => {
|
|
51
66
|
const fixture = await setup_test();
|
|
@@ -119,16 +134,11 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
119
134
|
// and emits a forensic `outcome: failure` audit row.
|
|
120
135
|
const del = await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: fixture.account.id }, fixture.create_session_headers());
|
|
121
136
|
assert.strictEqual(error_reason(del), ERROR_CANNOT_DELETE_KEEPER);
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
// The failure row is now authoritative on both spines. `_testing_reset`
|
|
128
|
-
// wiped audit_log at setup, so the refused delete is the only
|
|
129
|
-
// account_delete event.
|
|
130
|
-
const listed = expect_output(await cross_rpc_call(t, rpc_path, 'audit_log_list', { event_type: 'account_delete' }, fixture.create_session_headers()), AuditLogListOutput);
|
|
131
|
-
const failure = listed.events.find((e) => e.outcome === 'failure' &&
|
|
137
|
+
// Read behind the drain barrier, so the failure row is authoritative
|
|
138
|
+
// on both spines. `_testing_reset` wiped audit_log at setup, so the
|
|
139
|
+
// refused delete is the only account_delete event.
|
|
140
|
+
const events = await list_audit_events(fixture, { event_type: 'account_delete' }, rpc_path);
|
|
141
|
+
const failure = events.find((e) => e.outcome === 'failure' &&
|
|
132
142
|
e.metadata?.reason === ERROR_CANNOT_DELETE_KEEPER);
|
|
133
143
|
assert.ok(failure, 'keeper-removal guard must emit an account_delete outcome=failure audit row with reason cannot_delete_keeper');
|
|
134
144
|
// Byte-shape gate on the audit row's timestamp — `z.string()` in
|
|
@@ -151,6 +161,13 @@ export const describe_account_lifecycle_cross_tests = (options) => {
|
|
|
151
161
|
const again = await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers);
|
|
152
162
|
assert.strictEqual(again.ok, false, 'double-undelete must not silently succeed');
|
|
153
163
|
assert.strictEqual(error_reason(again), ERROR_ACCOUNT_NOT_FOUND);
|
|
164
|
+
// the refused undelete is audited as a failure naming the probed id
|
|
165
|
+
const events = await list_audit_events(fixture, { event_type: 'account_undelete', outcome: 'failure' }, rpc_path);
|
|
166
|
+
assert.strictEqual(events.length, 1, 'one account_undelete failure row');
|
|
167
|
+
assert_audit_metadata_fields(fixture, events[0].metadata, {
|
|
168
|
+
reason: ERROR_ACCOUNT_NOT_FOUND,
|
|
169
|
+
attempted_account_id: victim.account.id
|
|
170
|
+
});
|
|
154
171
|
});
|
|
155
172
|
// The last-admin guard (`ERROR_CANNOT_DELETE_LAST_ADMIN`) is **not**
|
|
156
173
|
// cross-process-testable against this fixture: the per-test keeper
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor_lookup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_lookup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,
|
|
1
|
+
{"version":3,"file":"actor_lookup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_lookup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AASnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8BxF,CAAC"}
|
|
@@ -6,22 +6,22 @@ import '../assert_dev_env.js';
|
|
|
6
6
|
* (`{ids} → {actors: [{id, username, display_name?}]}`), not folded into the
|
|
7
7
|
* standard bundle. It's live-mounted on the spine RPC path but kept off the
|
|
8
8
|
* declared surface (`create_spine_surface_spec`) — like cells / ws / sse — so
|
|
9
|
-
* the standard cross suite's generic round-trip never drives it
|
|
10
|
-
*
|
|
9
|
+
* the standard cross suite's generic round-trip never drives it.
|
|
10
|
+
*
|
|
11
|
+
* Its single-request refusals are conformance-table rows on a table given the
|
|
12
|
+
* live mount (`live_rpc_endpoints`): the anonymous 401 a generated auth row,
|
|
13
|
+
* the empty-`ids` 400 a hand-written one. This suite holds what a row can't
|
|
14
|
+
* say, because it names the fixture's own actor:
|
|
11
15
|
*
|
|
12
|
-
* - **anonymous → 401** — the account-grain auth gate refuses an
|
|
13
|
-
* unauthenticated caller before the handler runs.
|
|
14
16
|
* - **keeper resolves own actor → 200** — the populated round trip: the
|
|
15
17
|
* returned row carries the keeper's `id` + `username`, and **no**
|
|
16
18
|
* `account_id` / `email` / timestamp / role field (the wire shape's
|
|
17
19
|
* deliberate info-leak posture). This is the assertion that exercises the
|
|
18
20
|
* Rust row→JSON mapping against the TS canonical shape.
|
|
19
|
-
* - **empty `ids` → 400** — the `min(1)` input bound is enforced on both
|
|
20
|
-
* spines (TS Zod, Rust `parse_ids`).
|
|
21
21
|
*
|
|
22
22
|
* Runs both legs via the shared `{setup_test}` protocol: the in-process leg
|
|
23
23
|
* (`auth/actor_lookup_parity.db.test.ts`, plain `gro test`) and the
|
|
24
|
-
* cross-process leg (`cross_backend/
|
|
24
|
+
* cross-process leg (`cross_backend/spine_suites.cross.test.ts`, the TS spine
|
|
25
25
|
* binaries + Rust `testing_spine_stub` over real HTTP). `actor_lookup` is
|
|
26
26
|
* mounted on every spine, so the suite is ungated.
|
|
27
27
|
*
|
|
@@ -38,15 +38,6 @@ export const describe_actor_lookup_cross_tests = (options) => {
|
|
|
38
38
|
const { setup_test } = options;
|
|
39
39
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
40
40
|
describe('actor_lookup parity', () => {
|
|
41
|
-
test('anonymous → 401 (account-grain auth gate)', async () => {
|
|
42
|
-
const fixture = await setup_test();
|
|
43
|
-
const res = await fixture.fresh_transport()(rpc_path, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { 'content-type': 'application/json' },
|
|
46
|
-
body: lookup_envelope([fixture.actor.id], 'anon-lookup')
|
|
47
|
-
});
|
|
48
|
-
assert.strictEqual(res.status, 401, 'unauthenticated actor_lookup must be refused');
|
|
49
|
-
});
|
|
50
41
|
test('keeper resolves own actor → 200 with id + username, no control fields', async () => {
|
|
51
42
|
const fixture = await setup_test();
|
|
52
43
|
const res = await fixture.transport(rpc_path, {
|
|
@@ -67,14 +58,5 @@ export const describe_actor_lookup_cross_tests = (options) => {
|
|
|
67
58
|
assert(!(key in row), `actor_lookup row must not leak '${key}'`);
|
|
68
59
|
}
|
|
69
60
|
});
|
|
70
|
-
test('empty ids → 400 (min(1) input bound)', async () => {
|
|
71
|
-
const fixture = await setup_test();
|
|
72
|
-
const res = await fixture.transport(rpc_path, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
75
|
-
body: lookup_envelope([], 'empty-lookup')
|
|
76
|
-
});
|
|
77
|
-
assert.strictEqual(res.status, 400, 'empty ids must fail input validation');
|
|
78
|
-
});
|
|
79
61
|
});
|
|
80
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app_settings.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/app_settings.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"app_settings.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/app_settings.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,qFAAqF;AACrF,MAAM,MAAM,2BAA2B,GAAG,wBAAwB,CAAC;AAcnE,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8DxF,CAAC"}
|
|
@@ -22,7 +22,7 @@ import '../assert_dev_env.js';
|
|
|
22
22
|
*
|
|
23
23
|
* Cites `security.md` §Signup. Runs both legs via the shared `{setup_test}`
|
|
24
24
|
* protocol: in-process (`auth/app_settings_parity.db.test.ts`) +
|
|
25
|
-
* cross-process (`cross_backend/
|
|
25
|
+
* cross-process (`cross_backend/spine_suites.cross.test.ts`, TS spine
|
|
26
26
|
* binaries + Rust `testing_spine_stub`). Mounted on every spine, so the
|
|
27
27
|
* suite is ungated.
|
|
28
28
|
*
|
|
@@ -31,9 +31,7 @@ import '../assert_dev_env.js';
|
|
|
31
31
|
import { describe, test, assert } from 'vitest';
|
|
32
32
|
import { app_settings_update_action_spec } from '../../auth/admin_action_specs.js';
|
|
33
33
|
import { ERROR_NO_MATCHING_INVITE } from '../../http/error_schemas.js';
|
|
34
|
-
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
35
|
-
/** REST signup path on the spine surface (`/api/account` prefix + `/signup`). */
|
|
36
|
-
const SIGNUP_PATH = '/api/account/signup';
|
|
34
|
+
import { SPINE_ACCOUNT_SIGNUP_PATH, SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
37
35
|
/** A password that satisfies the creation-strength schema (min 12). */
|
|
38
36
|
const SIGNUP_PASSWORD = 'securepassword123';
|
|
39
37
|
/** Build the JSON-RPC envelope for an `app_settings_update` call. */
|
|
@@ -55,7 +53,7 @@ export const describe_app_settings_cross_tests = (options) => {
|
|
|
55
53
|
body: update_envelope(true, 'enable-open-signup')
|
|
56
54
|
});
|
|
57
55
|
assert.strictEqual(enable.status, 200, 'admin app_settings_update must succeed');
|
|
58
|
-
const res = await fixture.fresh_transport()(
|
|
56
|
+
const res = await fixture.fresh_transport()(SPINE_ACCOUNT_SIGNUP_PATH, {
|
|
59
57
|
method: 'POST',
|
|
60
58
|
headers: { 'content-type': 'application/json' },
|
|
61
59
|
body: JSON.stringify({ username: 'open_signup_user', password: SIGNUP_PASSWORD })
|
|
@@ -80,7 +78,7 @@ export const describe_app_settings_cross_tests = (options) => {
|
|
|
80
78
|
body: update_envelope(false, 'disable-open-signup')
|
|
81
79
|
});
|
|
82
80
|
assert.strictEqual(disable.status, 200, 'admin app_settings_update (disable) must succeed');
|
|
83
|
-
const res = await fixture.fresh_transport()(
|
|
81
|
+
const res = await fixture.fresh_transport()(SPINE_ACCOUNT_SIGNUP_PATH, {
|
|
84
82
|
method: 'POST',
|
|
85
83
|
headers: { 'content-type': 'application/json' },
|
|
86
84
|
body: JSON.stringify({ username: 'closed_signup_user', password: SIGNUP_PASSWORD })
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { AuditLogEventWithUsernamesJson } from '../../auth/audit_log_schema.js';
|
|
3
|
+
import type { FetchTransport } from '../transports/fetch_transport.js';
|
|
4
|
+
/** The fixture members an audit read uses — every `TestFixture` has them. */
|
|
5
|
+
export interface AuditReadFixture {
|
|
6
|
+
readonly fresh_transport: (options?: {
|
|
7
|
+
readonly origin?: string | null;
|
|
8
|
+
}) => FetchTransport;
|
|
9
|
+
/** The keeper's session headers — the keeper holds the admin role the audit read needs. */
|
|
10
|
+
readonly create_session_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
11
|
+
readonly create_daemon_token_headers: (extra?: Record<string, string>) => Record<string, string>;
|
|
12
|
+
/** Refuse undeclared metadata keys — see `TestFixture.reject_unknown_audit_metadata_keys`. */
|
|
13
|
+
readonly reject_unknown_audit_metadata_keys?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Await the backend's in-flight audit writes via `_testing_drain_effects`, so
|
|
17
|
+
* a following `audit_log_list` sees every row emitted before the call. The
|
|
18
|
+
* backend must mount the action — every spine test binary does; in-process
|
|
19
|
+
* suites mount `create_testing_drain_effects_action`.
|
|
20
|
+
*
|
|
21
|
+
* @param fixture - the per-test fixture, whose daemon token authorizes the drain
|
|
22
|
+
* @param rpc_path - the RPC endpoint the drain action is mounted on
|
|
23
|
+
*/
|
|
24
|
+
export declare const cross_drain_effects: (fixture: AuditReadFixture, rpc_path?: string) => Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Assert an audit row's `metadata` carries `expected` — each field compared
|
|
27
|
+
* on its own, so a consumer's added key passes — and none of `absent`. When
|
|
28
|
+
* the fixture's setup closed the world (`reject_unknown_audit_metadata_keys`),
|
|
29
|
+
* the whole object must equal `expected`, so an undeclared key fails too.
|
|
30
|
+
*
|
|
31
|
+
* @param fixture - the per-test fixture, whose flag picks the comparison
|
|
32
|
+
* @param metadata - the row's `metadata`
|
|
33
|
+
* @param expected - the declared fields and their values
|
|
34
|
+
* @param absent - keys the row must not carry, e.g. an optional field left unset
|
|
35
|
+
*/
|
|
36
|
+
export declare const assert_audit_metadata_fields: (fixture: Pick<AuditReadFixture, "reject_unknown_audit_metadata_keys">, metadata: Record<string, unknown> | null, expected: Record<string, unknown>, absent?: ReadonlyArray<string>) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Drain the backend's audit writes (`cross_drain_effects`), then list audit rows as
|
|
39
|
+
* the keeper, parse them against `AuditLogListOutput`, and parse each row's
|
|
40
|
+
* `metadata` against its event type's schema (`assert_audit_event_metadata`),
|
|
41
|
+
* with unknown keys refused when `fixture.reject_unknown_audit_metadata_keys`
|
|
42
|
+
* is set. The wire schema leaves `metadata` a loose record, so without this a
|
|
43
|
+
* spine's metadata drift reaches no assertion.
|
|
44
|
+
*
|
|
45
|
+
* @param fixture - the per-test fixture; the list runs on the keeper's session
|
|
46
|
+
* @param params - `audit_log_list` params, e.g. `{event_type: 'login', outcome: 'failure'}`
|
|
47
|
+
* @param rpc_path - the RPC endpoint both actions are mounted on
|
|
48
|
+
* @returns the listed events
|
|
49
|
+
*/
|
|
50
|
+
export declare const list_audit_events: (fixture: AuditReadFixture, params: Record<string, unknown>, rpc_path?: string) => Promise<Array<AuditLogEventWithUsernamesJson>>;
|
|
51
|
+
//# sourceMappingURL=audit_cross_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/audit_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsB9B,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AAErF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAIvE,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,KAAK,cAAc,CAAC;IAC5F,2FAA2F;IAC3F,QAAQ,CAAC,sBAAsB,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5F,QAAQ,CAAC,2BAA2B,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjG,8FAA8F;IAC9F,QAAQ,CAAC,kCAAkC,CAAC,EAAE,OAAO,CAAC;CACtD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC/B,SAAS,gBAAgB,EACzB,WAAU,MAAuB,KAC/B,OAAO,CAAC,IAAI,CASd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,IAAI,CAAC,gBAAgB,EAAE,oCAAoC,CAAC,EACrE,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACxC,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,SAAQ,aAAa,CAAC,MAAM,CAAM,KAChC,IASF,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,gBAAgB,EACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAU,MAAuB,KAC/B,OAAO,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAe/C,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Audit-row reads for the cross-backend suites, behind the
|
|
4
|
+
* `_testing_drain_effects` barrier.
|
|
5
|
+
*
|
|
6
|
+
* Both spines write audit rows off the request path — a WS mutation is
|
|
7
|
+
* answered before its effects flush, and the Rust spine's emits are
|
|
8
|
+
* fire-and-forget — so a suite that reads `audit_log_list` right after the
|
|
9
|
+
* call it audits can miss the row, and an assertion that a row is absent then
|
|
10
|
+
* passes vacuously. `list_audit_events` drains before it lists, so no reader
|
|
11
|
+
* can skip the barrier, and holds every row it lists to its event type's
|
|
12
|
+
* metadata schema (`assert_audit_event_metadata`), so every reader checks the
|
|
13
|
+
* metadata of the rows it reads, on every spine — closed to undeclared keys
|
|
14
|
+
* when the fixture's setup asked for it.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { assert } from 'vitest';
|
|
19
|
+
import { AuditLogListOutput } from '../../auth/admin_action_specs.js';
|
|
20
|
+
import { assert_audit_event_metadata } from '../audit_drift_guard.js';
|
|
21
|
+
import { cross_rpc_call, expect_output } from './cross_rpc.js';
|
|
22
|
+
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
23
|
+
/**
|
|
24
|
+
* Await the backend's in-flight audit writes via `_testing_drain_effects`, so
|
|
25
|
+
* a following `audit_log_list` sees every row emitted before the call. The
|
|
26
|
+
* backend must mount the action — every spine test binary does; in-process
|
|
27
|
+
* suites mount `create_testing_drain_effects_action`.
|
|
28
|
+
*
|
|
29
|
+
* @param fixture - the per-test fixture, whose daemon token authorizes the drain
|
|
30
|
+
* @param rpc_path - the RPC endpoint the drain action is mounted on
|
|
31
|
+
*/
|
|
32
|
+
export const cross_drain_effects = async (fixture, rpc_path = SPINE_RPC_PATH) => {
|
|
33
|
+
const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
|
|
34
|
+
assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Assert an audit row's `metadata` carries `expected` — each field compared
|
|
38
|
+
* on its own, so a consumer's added key passes — and none of `absent`. When
|
|
39
|
+
* the fixture's setup closed the world (`reject_unknown_audit_metadata_keys`),
|
|
40
|
+
* the whole object must equal `expected`, so an undeclared key fails too.
|
|
41
|
+
*
|
|
42
|
+
* @param fixture - the per-test fixture, whose flag picks the comparison
|
|
43
|
+
* @param metadata - the row's `metadata`
|
|
44
|
+
* @param expected - the declared fields and their values
|
|
45
|
+
* @param absent - keys the row must not carry, e.g. an optional field left unset
|
|
46
|
+
*/
|
|
47
|
+
export const assert_audit_metadata_fields = (fixture, metadata, expected, absent = []) => {
|
|
48
|
+
assert.ok(metadata, 'the audit row carries metadata');
|
|
49
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
50
|
+
assert.deepStrictEqual(metadata[key], value, `metadata.${key}`);
|
|
51
|
+
}
|
|
52
|
+
for (const key of absent)
|
|
53
|
+
assert.notProperty(metadata, key, `metadata.${key} is absent`);
|
|
54
|
+
if (fixture.reject_unknown_audit_metadata_keys) {
|
|
55
|
+
assert.deepStrictEqual(metadata, expected, 'metadata carries only the declared fields');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Drain the backend's audit writes (`cross_drain_effects`), then list audit rows as
|
|
60
|
+
* the keeper, parse them against `AuditLogListOutput`, and parse each row's
|
|
61
|
+
* `metadata` against its event type's schema (`assert_audit_event_metadata`),
|
|
62
|
+
* with unknown keys refused when `fixture.reject_unknown_audit_metadata_keys`
|
|
63
|
+
* is set. The wire schema leaves `metadata` a loose record, so without this a
|
|
64
|
+
* spine's metadata drift reaches no assertion.
|
|
65
|
+
*
|
|
66
|
+
* @param fixture - the per-test fixture; the list runs on the keeper's session
|
|
67
|
+
* @param params - `audit_log_list` params, e.g. `{event_type: 'login', outcome: 'failure'}`
|
|
68
|
+
* @param rpc_path - the RPC endpoint both actions are mounted on
|
|
69
|
+
* @returns the listed events
|
|
70
|
+
*/
|
|
71
|
+
export const list_audit_events = async (fixture, params, rpc_path = SPINE_RPC_PATH) => {
|
|
72
|
+
await cross_drain_effects(fixture, rpc_path);
|
|
73
|
+
const { events } = expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'audit_log_list', params, fixture.create_session_headers()), AuditLogListOutput);
|
|
74
|
+
const reject_unknown_keys = fixture.reject_unknown_audit_metadata_keys ?? false;
|
|
75
|
+
for (const event of events)
|
|
76
|
+
assert_audit_event_metadata(event, { reject_unknown_keys });
|
|
77
|
+
return events;
|
|
78
|
+
};
|
|
@@ -44,7 +44,8 @@ export interface BackendBootstrapConfig {
|
|
|
44
44
|
* (env: `*_DAEMON_TOKEN_PATH`). `spawn_backend` reads this file once
|
|
45
45
|
* after the health probe succeeds and threads the token onto
|
|
46
46
|
* `BackendHandle.daemon_token` for `_testing_reset` calls plus any
|
|
47
|
-
* other admin/keeper-gated cross-process tests.
|
|
47
|
+
* other admin/keeper-gated cross-process tests. `spawn_backend` deletes
|
|
48
|
+
* the file before each spawn, so a token a previous run left is never read.
|
|
48
49
|
*/
|
|
49
50
|
readonly daemon_token_path: string;
|
|
50
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACtC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B
|
|
1
|
+
{"version":3,"file":"backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACtC,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;CACnC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,aAAa;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,6DAA6D;IAC7D,QAAQ,CAAC,SAAS,EAAE,sBAAsB,CAAC;IAC3C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;CAC3C"}
|
|
@@ -20,6 +20,12 @@ export interface BodySizeCrossTestOptions extends RpcPathCrossSuiteOptions {
|
|
|
20
20
|
* time.
|
|
21
21
|
*/
|
|
22
22
|
readonly base_url: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* The backend's request-body cap on `rpc_path`, in bytes — the boundary the
|
|
25
|
+
* cases probe. Default `SPINE_BODY_LIMIT_DEFAULT_BYTES`; pass the backend's
|
|
26
|
+
* value when it caps its RPC path elsewhere.
|
|
27
|
+
*/
|
|
28
|
+
readonly rpc_max_body_bytes?: number;
|
|
23
29
|
}
|
|
24
30
|
export declare const describe_body_size_cross_tests: (options: BodySizeCrossTestOptions) => void;
|
|
25
31
|
//# sourceMappingURL=body_size.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAiG9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AA8FD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IAkIlF,CAAC"}
|
|
@@ -4,7 +4,8 @@ import '../assert_dev_env.js';
|
|
|
4
4
|
*
|
|
5
5
|
* `create_app_server` (TS) and the Rust spine both cap the request body at a
|
|
6
6
|
* 1 MiB default (`DEFAULT_MAX_BODY_SIZE` / `fuz_http`'s
|
|
7
|
-
* `DEFAULT_BODY_LIMIT_BYTES
|
|
7
|
+
* `DEFAULT_BODY_LIMIT_BYTES`; a backend capping its RPC path elsewhere passes
|
|
8
|
+
* `rpc_max_body_bytes`) and reject oversized payloads with `413` and the
|
|
8
9
|
* canonical flat REST body `{error: 'payload_too_large'}`. A declared
|
|
9
10
|
* `Content-Length` over the cap is refused on the header, *before* auth,
|
|
10
11
|
* origin, or dispatch run (middleware step 4). Each impl unit-tests this in
|
|
@@ -55,7 +56,7 @@ import '../assert_dev_env.js';
|
|
|
55
56
|
*
|
|
56
57
|
* - a **declared** over-cap length is sent as the request head alone —
|
|
57
58
|
* `Content-Length` one over the cap and not one body byte. The 413 must
|
|
58
|
-
* arrive within `
|
|
59
|
+
* arrive within `RAW_HTTP_READ_TIMEOUT_MS`, which also pins that the refusal is
|
|
59
60
|
* made on the header: a backend that reads the body before refusing, or has
|
|
60
61
|
* no cap, waits for bytes that never come and fails the case.
|
|
61
62
|
* - the **chunked** body is sent up to one byte past the cap and never
|
|
@@ -72,7 +73,7 @@ import '../assert_dev_env.js';
|
|
|
72
73
|
* imperative suite, not a `conformance_table` row. Runs both legs via the
|
|
73
74
|
* shared `setup_test` protocol: the in-process leg
|
|
74
75
|
* (`auth/body_size_parity.db.test.ts`, plain `gro test`) and the cross-process
|
|
75
|
-
* leg (`cross_backend/
|
|
76
|
+
* leg (`cross_backend/spine_suites.cross.test.ts`, the TS spine binaries + Rust
|
|
76
77
|
* `testing_spine_stub` over real HTTP). The body-size limit is on every spine,
|
|
77
78
|
* so the suite is ungated.
|
|
78
79
|
*
|
|
@@ -83,16 +84,8 @@ import '../assert_dev_env.js';
|
|
|
83
84
|
import { describe, test, assert } from 'vitest';
|
|
84
85
|
import { account_verify_action_spec } from '../../auth/account_action_specs.js';
|
|
85
86
|
import { ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
|
|
86
|
-
import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
|
|
87
|
-
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
88
|
-
/**
|
|
89
|
-
* The shared 1 MiB default both impls ship (`DEFAULT_MAX_BODY_SIZE` in TS,
|
|
90
|
-
* `DEFAULT_BODY_LIMIT_BYTES` in `fuz_http`). Kept as a local cross-impl
|
|
91
|
-
* contract value rather than imported from either impl — the boundary it
|
|
92
|
-
* pins is the shared default, and a change to either is a deliberate
|
|
93
|
-
* cross-impl decision that would revisit this suite regardless.
|
|
94
|
-
*/
|
|
95
|
-
const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
|
|
87
|
+
import { raw_http_exchange, raw_http_header_lines, raw_http_parse_json, raw_http_parse_response, RAW_HTTP_READ_TIMEOUT_MS } from '../transports/raw_http.js';
|
|
88
|
+
import { SPINE_BODY_LIMIT_DEFAULT_BYTES, SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
96
89
|
/**
|
|
97
90
|
* Build an `account_verify` JSON-RPC envelope whose serialized length is
|
|
98
91
|
* *exactly* `target_bytes`, by growing a `params.pad` string to fill. Every
|
|
@@ -101,6 +94,10 @@ const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
|
|
|
101
94
|
* middleware measures. `account_verify` takes no params, but at/over the cap
|
|
102
95
|
* the size check fires before input validation, so the padded shape is never
|
|
103
96
|
* reached on the rejection path.
|
|
97
|
+
*
|
|
98
|
+
* The envelope with an empty pad is already 70-odd bytes (more with a longer
|
|
99
|
+
* `id`), so a smaller target overshoots: the body comes out at that floor, not
|
|
100
|
+
* at `target_bytes`.
|
|
104
101
|
*/
|
|
105
102
|
const sized_envelope = (id, target_bytes) => {
|
|
106
103
|
const method = account_verify_action_spec.method;
|
|
@@ -131,28 +128,10 @@ const streamed_body = (total_bytes) => {
|
|
|
131
128
|
}
|
|
132
129
|
});
|
|
133
130
|
};
|
|
134
|
-
/**
|
|
135
|
-
* How long a raw over-cap exchange waits for the refusal. The server has every
|
|
136
|
-
* byte it will get when the wait starts, so a backend that refuses on what it
|
|
137
|
-
* has answers at once and one that waits for more runs the whole window.
|
|
138
|
-
*/
|
|
139
|
-
const RAW_READ_TIMEOUT_MS = 2000;
|
|
140
131
|
const to_sized_response = async (res) => ({
|
|
141
132
|
status: res.status,
|
|
142
133
|
body: await res.json().catch(() => undefined)
|
|
143
134
|
});
|
|
144
|
-
const parse_json = (text) => {
|
|
145
|
-
try {
|
|
146
|
-
return JSON.parse(text);
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
/** Render request headers as raw header lines. */
|
|
153
|
-
const to_header_lines = (headers) => Object.entries(headers)
|
|
154
|
-
.map(([name, value]) => `${name}: ${value}\r\n`)
|
|
155
|
-
.join('');
|
|
156
135
|
/**
|
|
157
136
|
* Write `request_bytes` to the backend's socket and read the one response it
|
|
158
137
|
* must answer with — without being sent anything more.
|
|
@@ -161,17 +140,17 @@ const to_header_lines = (headers) => Object.entries(headers)
|
|
|
161
140
|
*/
|
|
162
141
|
const exchange_withholding = async (base_url, request_bytes, withheld) => {
|
|
163
142
|
const { received, closed_by_server } = await raw_http_exchange(base_url, request_bytes, {
|
|
164
|
-
read_timeout_ms:
|
|
143
|
+
read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS,
|
|
165
144
|
until: 'response'
|
|
166
145
|
});
|
|
167
146
|
const response = raw_http_parse_response(received);
|
|
168
147
|
assert.ok(response, `expected a whole response with ${withheld} withheld; ` +
|
|
169
148
|
(closed_by_server
|
|
170
149
|
? `the server closed the connection first`
|
|
171
|
-
: `none arrived within ${
|
|
150
|
+
: `none arrived within ${RAW_HTTP_READ_TIMEOUT_MS}ms — the backend is waiting for bytes ` +
|
|
172
151
|
`it should refuse without`) +
|
|
173
152
|
`. Received: ${received ? received.slice(0, 120) : 'nothing'}`);
|
|
174
|
-
return { status: response.status, body:
|
|
153
|
+
return { status: response.status, body: raw_http_parse_json(response.body) };
|
|
175
154
|
};
|
|
176
155
|
/** Assert a response is the canonical over-cap refusal. */
|
|
177
156
|
const assert_payload_too_large = (res, message) => {
|
|
@@ -181,6 +160,7 @@ const assert_payload_too_large = (res, message) => {
|
|
|
181
160
|
export const describe_body_size_cross_tests = (options) => {
|
|
182
161
|
const { setup_test, base_url } = options;
|
|
183
162
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
163
|
+
const cap = options.rpc_max_body_bytes ?? SPINE_BODY_LIMIT_DEFAULT_BYTES;
|
|
184
164
|
/** POST `body` to the RPC path as the keeper. */
|
|
185
165
|
const post = (fixture, body) => fixture.transport(rpc_path, {
|
|
186
166
|
method: 'POST',
|
|
@@ -194,7 +174,7 @@ export const describe_body_size_cross_tests = (options) => {
|
|
|
194
174
|
*/
|
|
195
175
|
const post_over_limit_declared = async (fixture, id) => {
|
|
196
176
|
const headers = fixture.create_session_headers();
|
|
197
|
-
const content_length =
|
|
177
|
+
const content_length = cap + 1;
|
|
198
178
|
if (base_url === null) {
|
|
199
179
|
return to_sized_response(await fixture.transport(rpc_path, {
|
|
200
180
|
method: 'POST',
|
|
@@ -204,7 +184,7 @@ export const describe_body_size_cross_tests = (options) => {
|
|
|
204
184
|
}
|
|
205
185
|
const url = new URL(base_url);
|
|
206
186
|
return exchange_withholding(base_url, `POST ${rpc_path} HTTP/1.1\r\n` +
|
|
207
|
-
|
|
187
|
+
raw_http_header_lines({
|
|
208
188
|
host: url.host,
|
|
209
189
|
origin: url.origin,
|
|
210
190
|
...headers,
|
|
@@ -223,18 +203,19 @@ export const describe_body_size_cross_tests = (options) => {
|
|
|
223
203
|
return to_sized_response(await fixture.transport(rpc_path, {
|
|
224
204
|
method: 'POST',
|
|
225
205
|
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
226
|
-
body: streamed_body(
|
|
206
|
+
body: streamed_body(cap + 1024),
|
|
227
207
|
// a streamed request body needs half-duplex `fetch`
|
|
228
208
|
duplex: 'half'
|
|
229
209
|
}));
|
|
230
210
|
}
|
|
231
211
|
const url = new URL(base_url);
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
const
|
|
212
|
+
// chunks up to exactly the cap, then a one-byte chunk's size line and its
|
|
213
|
+
// byte — and neither that chunk's closing CRLF nor the terminating chunk,
|
|
214
|
+
// so the last byte sent is the first one past the cap
|
|
215
|
+
const to_chunk = (size) => `${size.toString(16)}\r\n${'x'.repeat(size)}\r\n`;
|
|
216
|
+
const remainder = cap % STREAM_CHUNK_BYTES;
|
|
236
217
|
return exchange_withholding(base_url, `POST ${rpc_path} HTTP/1.1\r\n` +
|
|
237
|
-
|
|
218
|
+
raw_http_header_lines({
|
|
238
219
|
host: url.host,
|
|
239
220
|
origin: url.origin,
|
|
240
221
|
...fixture.create_session_headers(),
|
|
@@ -242,7 +223,8 @@ export const describe_body_size_cross_tests = (options) => {
|
|
|
242
223
|
'transfer-encoding': 'chunked'
|
|
243
224
|
}) +
|
|
244
225
|
`\r\n` +
|
|
245
|
-
|
|
226
|
+
to_chunk(STREAM_CHUNK_BYTES).repeat(Math.floor(cap / STREAM_CHUNK_BYTES)) +
|
|
227
|
+
(remainder === 0 ? '' : to_chunk(remainder)) +
|
|
246
228
|
`1\r\nx`, `the chunked body's end`);
|
|
247
229
|
};
|
|
248
230
|
describe('body-size limit parity', () => {
|
|
@@ -259,7 +241,7 @@ export const describe_body_size_cross_tests = (options) => {
|
|
|
259
241
|
// Exactly at the cap is one byte under the `>` threshold, so it clears
|
|
260
242
|
// the size gate and reaches the dispatcher. Whatever the dispatcher
|
|
261
243
|
// then returns, it must not be the 413 — that's the lower boundary.
|
|
262
|
-
const res = await post(fixture, sized_envelope('at-limit',
|
|
244
|
+
const res = await post(fixture, sized_envelope('at-limit', cap));
|
|
263
245
|
assert.notStrictEqual(res.status, 413, 'a body exactly at the cap must not be size-rejected');
|
|
264
246
|
});
|
|
265
247
|
test('under-limit POST (small) → 200', async () => {
|
|
@@ -5,6 +5,11 @@ export interface BodySizeSmugglingCrossTestOptions {
|
|
|
5
5
|
readonly base_url: string;
|
|
6
6
|
/** RPC endpoint path to target. Default `/api/rpc`. */
|
|
7
7
|
readonly rpc_path?: string;
|
|
8
|
+
/**
|
|
9
|
+
* The backend's request-body cap on `rpc_path`, in bytes; the oversized
|
|
10
|
+
* `POST` declares a length past it. Default `SPINE_BODY_LIMIT_DEFAULT_BYTES`.
|
|
11
|
+
*/
|
|
12
|
+
readonly rpc_max_body_bytes?: number;
|
|
8
13
|
}
|
|
9
14
|
export declare const describe_body_size_smuggling_cross_tests: (options: BodySizeSmugglingCrossTestOptions) => void;
|
|
10
15
|
//# sourceMappingURL=body_size_smuggling.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuE9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA4GF,CAAC"}
|
|
@@ -59,15 +59,8 @@ import '../assert_dev_env.js';
|
|
|
59
59
|
* @module
|
|
60
60
|
*/
|
|
61
61
|
import { describe, test, assert } from 'vitest';
|
|
62
|
-
import { raw_http_count_responses, raw_http_exchange } from '../transports/raw_http.js';
|
|
63
|
-
import { SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
64
|
-
/** The shared 1 MiB cap (see `body_size.ts` for why it's a local constant). */
|
|
65
|
-
const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
|
|
66
|
-
/**
|
|
67
|
-
* How long each raw exchange waits for the server to finish answering. An
|
|
68
|
-
* exchange the backend does not end by closing runs the whole window.
|
|
69
|
-
*/
|
|
70
|
-
const RAW_READ_TIMEOUT_MS = 2000;
|
|
62
|
+
import { raw_http_count_responses, raw_http_exchange, RAW_HTTP_READ_TIMEOUT_MS } from '../transports/raw_http.js';
|
|
63
|
+
import { SPINE_BODY_LIMIT_DEFAULT_BYTES, SPINE_RPC_PATH } from './spine_surface_constants.js';
|
|
71
64
|
export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
72
65
|
const { base_url } = options;
|
|
73
66
|
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
@@ -81,7 +74,7 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
|
81
74
|
const two_requests = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n` +
|
|
82
75
|
`GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`;
|
|
83
76
|
const { received: response } = await raw_http_exchange(base_url, two_requests, {
|
|
84
|
-
read_timeout_ms:
|
|
77
|
+
read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS
|
|
85
78
|
});
|
|
86
79
|
const n = raw_http_count_responses(response);
|
|
87
80
|
assert.ok(n >= 2, `expected ≥2 responses on one keep-alive connection (got ${n}); without ` +
|
|
@@ -89,7 +82,7 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
|
89
82
|
`assertion below is not a real signal. Raw head: ${response.slice(0, 120)}`);
|
|
90
83
|
});
|
|
91
84
|
test('oversized POST + pipelined GET → no request smuggled', async () => {
|
|
92
|
-
const oversized_len =
|
|
85
|
+
const oversized_len = (options.rpc_max_body_bytes ?? SPINE_BODY_LIMIT_DEFAULT_BYTES) + 1024;
|
|
93
86
|
const pipelined_get = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n`;
|
|
94
87
|
// One write: an over-cap POST (rejected on Content-Length) immediately
|
|
95
88
|
// followed by a GET.
|
|
@@ -101,7 +94,7 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
|
101
94
|
'x'.repeat(oversized_len) +
|
|
102
95
|
pipelined_get;
|
|
103
96
|
const { received: response } = await raw_http_exchange(base_url, payload, {
|
|
104
|
-
read_timeout_ms:
|
|
97
|
+
read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS
|
|
105
98
|
});
|
|
106
99
|
const n = raw_http_count_responses(response);
|
|
107
100
|
// The reject closes the connection, so the pipelined GET is never
|
|
@@ -140,7 +133,7 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
|
140
133
|
chunked_envelope +
|
|
141
134
|
`GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n`;
|
|
142
135
|
const { received, closed_by_server } = await raw_http_exchange(base_url, payload, {
|
|
143
|
-
read_timeout_ms:
|
|
136
|
+
read_timeout_ms: RAW_HTTP_READ_TIMEOUT_MS
|
|
144
137
|
});
|
|
145
138
|
const n = raw_http_count_responses(received);
|
|
146
139
|
// Refused with 400 or answered — the status is the implementation's.
|
|
@@ -151,7 +144,7 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
|
|
|
151
144
|
`Raw head: ${received.slice(0, 120)}`);
|
|
152
145
|
assert.ok(closed_by_server, `expected the server to close the connection after a request carrying both ` +
|
|
153
146
|
`Content-Length and Transfer-Encoding; it was still open after ` +
|
|
154
|
-
`${
|
|
147
|
+
`${RAW_HTTP_READ_TIMEOUT_MS}ms. Raw head: ${received.slice(0, 120)}`);
|
|
155
148
|
});
|
|
156
149
|
}
|
|
157
150
|
});
|