@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
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Schema-walk generation of adversarial inputs — payloads that must fail
|
|
4
|
+
* validation.
|
|
5
|
+
*
|
|
6
|
+
* Focused cases: one representative wrong-type value per field, one format
|
|
7
|
+
* violation per constrained field, one null and one missing case per required
|
|
8
|
+
* field, plus whole-body structural attacks (non-object body, extra unknown
|
|
9
|
+
* keys). Hono-free, so the cross-process suites reuse the generators the
|
|
10
|
+
* in-process `describe_adversarial_input` and `describe_rpc_attack_surface_tests`
|
|
11
|
+
* run.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { zod_unwrap_to_object, zod_extract_fields } from '../zod.js';
|
|
17
|
+
import { is_null_schema } from '../http/schema_helpers.js';
|
|
18
|
+
import { ERROR_INVALID_REQUEST_BODY, ERROR_INVALID_JSON_BODY } from '../http/error_schemas.js';
|
|
19
|
+
import { detect_format, generate_valid_value } from './schema_generators.js';
|
|
20
|
+
// --- Payload generation ---
|
|
21
|
+
/** One wrong-type value per base type — one representative is sufficient. */
|
|
22
|
+
const wrong_type_for = (base_type) => {
|
|
23
|
+
switch (base_type) {
|
|
24
|
+
case 'string':
|
|
25
|
+
case 'uuid':
|
|
26
|
+
case 'email':
|
|
27
|
+
return { label: 'number instead of string', value: 42 };
|
|
28
|
+
case 'number':
|
|
29
|
+
case 'int':
|
|
30
|
+
return { label: 'string instead of number', value: 'not_a_number' };
|
|
31
|
+
case 'boolean':
|
|
32
|
+
return { label: 'number instead of boolean', value: 42 };
|
|
33
|
+
case 'array':
|
|
34
|
+
return { label: 'string instead of array', value: 'not_an_array' };
|
|
35
|
+
case 'object':
|
|
36
|
+
return { label: 'string instead of object', value: 'not_an_object' };
|
|
37
|
+
case 'enum':
|
|
38
|
+
return { label: 'number instead of enum', value: 42 };
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** Format violation payloads — one per constraint type. */
|
|
44
|
+
const format_violation = (format) => {
|
|
45
|
+
switch (format) {
|
|
46
|
+
case 'uuid':
|
|
47
|
+
return { label: 'malformed uuid', value: 'not-a-uuid' };
|
|
48
|
+
case 'email':
|
|
49
|
+
return { label: 'malformed email', value: 'not-an-email' };
|
|
50
|
+
case 'date-time':
|
|
51
|
+
return { label: 'malformed datetime', value: 'not-a-date' };
|
|
52
|
+
case 'pattern':
|
|
53
|
+
return { label: 'pattern violation', value: "'; DROP TABLE --" };
|
|
54
|
+
default:
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
// --- Input test case generation ---
|
|
59
|
+
/**
|
|
60
|
+
* Generate adversarial test cases for a route's input schema.
|
|
61
|
+
*
|
|
62
|
+
* Produces focused, non-redundant cases:
|
|
63
|
+
* - Whole-body: send array instead of object, extra unknown key
|
|
64
|
+
* - Missing required fields (without defaults)
|
|
65
|
+
* - One wrong-type value per field
|
|
66
|
+
* - Null for required non-nullable fields
|
|
67
|
+
* - One format violation per constrained field
|
|
68
|
+
*
|
|
69
|
+
* @throws Error if the seed body built by `generate_valid_value` fails
|
|
70
|
+
* `input_schema.safeParse` — surfaces broken generation logic with the
|
|
71
|
+
* Zod issues path rather than producing nonsense adversarial cases.
|
|
72
|
+
*/
|
|
73
|
+
export const generate_input_test_cases = (input_schema) => {
|
|
74
|
+
if (is_null_schema(input_schema))
|
|
75
|
+
return [];
|
|
76
|
+
const object_schema = zod_unwrap_to_object(input_schema);
|
|
77
|
+
if (!object_schema)
|
|
78
|
+
return [];
|
|
79
|
+
const fields = zod_extract_fields(object_schema);
|
|
80
|
+
const cases = [];
|
|
81
|
+
// build a base object with valid-ish values — must pass validation itself
|
|
82
|
+
// skip optional fields without defaults to avoid generating invalid nested values
|
|
83
|
+
const base = {};
|
|
84
|
+
for (const field of fields) {
|
|
85
|
+
if (!field.required && !field.has_default)
|
|
86
|
+
continue;
|
|
87
|
+
base[field.name] = generate_valid_value(field, object_schema.shape[field.name]);
|
|
88
|
+
}
|
|
89
|
+
let base_result = input_schema.safeParse(base);
|
|
90
|
+
if (!base_result.success) {
|
|
91
|
+
// Fallback for schemas with a top-level `.refine()` that requires at
|
|
92
|
+
// least one of N optional fields. Fill optional fields until the base
|
|
93
|
+
// satisfies validation; surfaces a clear error if no combination works.
|
|
94
|
+
for (const field of fields) {
|
|
95
|
+
if (field.required || field.has_default || field.name in base)
|
|
96
|
+
continue;
|
|
97
|
+
base[field.name] = generate_valid_value(field, object_schema.shape[field.name]);
|
|
98
|
+
base_result = input_schema.safeParse(base);
|
|
99
|
+
if (base_result.success)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!base_result.success) {
|
|
104
|
+
throw new Error(`adversarial_input: generated base object fails validation for schema — ` +
|
|
105
|
+
`fix generate_valid_value for: ${base_result.error.issues
|
|
106
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
107
|
+
.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
// whole-body structural: send array instead of object
|
|
110
|
+
cases.push({
|
|
111
|
+
label: 'non-object body (array)',
|
|
112
|
+
body: [1, 2, 3],
|
|
113
|
+
expected_error: ERROR_INVALID_JSON_BODY
|
|
114
|
+
});
|
|
115
|
+
// whole-body structural: extra unknown key (enforces strictObject)
|
|
116
|
+
// only emit if the schema rejects unknown keys (i.e. uses z.strictObject)
|
|
117
|
+
const extra_key_result = input_schema.safeParse({ ...base, __adversarial_extra: 'rejected' });
|
|
118
|
+
if (!extra_key_result.success) {
|
|
119
|
+
cases.push({
|
|
120
|
+
label: 'extra unknown key',
|
|
121
|
+
body: { ...base, __adversarial_extra: 'should_be_rejected' },
|
|
122
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const field of fields) {
|
|
126
|
+
const field_schema = object_schema.shape[field.name];
|
|
127
|
+
// missing required field (skip fields with defaults — Zod fills them)
|
|
128
|
+
if (field.required && !field.has_default) {
|
|
129
|
+
const without = {};
|
|
130
|
+
for (const [k, v] of Object.entries(base)) {
|
|
131
|
+
if (k !== field.name)
|
|
132
|
+
without[k] = v;
|
|
133
|
+
}
|
|
134
|
+
cases.push({
|
|
135
|
+
label: `missing: ${field.name}`,
|
|
136
|
+
body: without,
|
|
137
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// one wrong-type value
|
|
141
|
+
const wrong = wrong_type_for(field.base_type);
|
|
142
|
+
if (wrong) {
|
|
143
|
+
cases.push({
|
|
144
|
+
label: `wrong type: ${field.name} (${wrong.label})`,
|
|
145
|
+
body: { ...base, [field.name]: wrong.value },
|
|
146
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// null for required non-nullable fields
|
|
150
|
+
if (field.required && !field.nullable && field.base_type !== 'null') {
|
|
151
|
+
cases.push({
|
|
152
|
+
label: `null: ${field.name}`,
|
|
153
|
+
body: { ...base, [field.name]: null },
|
|
154
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// format violation for constrained string fields
|
|
158
|
+
const format = detect_format(field_schema);
|
|
159
|
+
if (format) {
|
|
160
|
+
const violation = format_violation(format);
|
|
161
|
+
if (violation) {
|
|
162
|
+
cases.push({
|
|
163
|
+
label: `format: ${field.name} (${violation.label})`,
|
|
164
|
+
body: { ...base, [field.name]: violation.value },
|
|
165
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// --- Boundary cases via JSON Schema introspection ---
|
|
170
|
+
try {
|
|
171
|
+
const json = z.toJSONSchema(field_schema);
|
|
172
|
+
// string length boundaries
|
|
173
|
+
if (field.base_type === 'string' ||
|
|
174
|
+
field.base_type === 'uuid' ||
|
|
175
|
+
field.base_type === 'email') {
|
|
176
|
+
if (typeof json.minLength === 'number' && json.minLength > 0) {
|
|
177
|
+
cases.push({
|
|
178
|
+
label: `empty string: ${field.name}`,
|
|
179
|
+
body: { ...base, [field.name]: '' },
|
|
180
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (typeof json.maxLength === 'number') {
|
|
184
|
+
cases.push({
|
|
185
|
+
label: `over maxLength: ${field.name} (${json.maxLength + 1} chars)`,
|
|
186
|
+
body: { ...base, [field.name]: 'x'.repeat(json.maxLength + 1) },
|
|
187
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// numeric boundaries
|
|
192
|
+
if (field.base_type === 'number' || field.base_type === 'int') {
|
|
193
|
+
if (typeof json.minimum === 'number') {
|
|
194
|
+
cases.push({
|
|
195
|
+
label: `below minimum: ${field.name} (${json.minimum - 1})`,
|
|
196
|
+
body: { ...base, [field.name]: json.minimum - 1 },
|
|
197
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (typeof json.maximum === 'number') {
|
|
201
|
+
cases.push({
|
|
202
|
+
label: `above maximum: ${field.name} (${json.maximum + 1})`,
|
|
203
|
+
body: { ...base, [field.name]: json.maximum + 1 },
|
|
204
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (typeof json.exclusiveMinimum === 'number') {
|
|
208
|
+
cases.push({
|
|
209
|
+
label: `at exclusive minimum: ${field.name} (${json.exclusiveMinimum})`,
|
|
210
|
+
body: { ...base, [field.name]: json.exclusiveMinimum },
|
|
211
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (typeof json.exclusiveMaximum === 'number') {
|
|
215
|
+
cases.push({
|
|
216
|
+
label: `at exclusive maximum: ${field.name} (${json.exclusiveMaximum})`,
|
|
217
|
+
body: { ...base, [field.name]: json.exclusiveMaximum },
|
|
218
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
// 0 and negative for positive-only fields
|
|
222
|
+
if (typeof json.minimum === 'number' && json.minimum > 0) {
|
|
223
|
+
cases.push({
|
|
224
|
+
label: `zero for positive-only: ${field.name}`,
|
|
225
|
+
body: { ...base, [field.name]: 0 },
|
|
226
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
227
|
+
});
|
|
228
|
+
cases.push({
|
|
229
|
+
label: `negative for positive-only: ${field.name}`,
|
|
230
|
+
body: { ...base, [field.name]: -1 },
|
|
231
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// array length boundaries
|
|
236
|
+
if (field.base_type === 'array') {
|
|
237
|
+
if (typeof json.minItems === 'number' && json.minItems > 0) {
|
|
238
|
+
cases.push({
|
|
239
|
+
label: `empty array for minItems > 0: ${field.name}`,
|
|
240
|
+
body: { ...base, [field.name]: [] },
|
|
241
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (typeof json.maxItems === 'number') {
|
|
245
|
+
// generate an array one item over the max (items are null — schema-agnostic)
|
|
246
|
+
cases.push({
|
|
247
|
+
label: `over maxItems: ${field.name} (${json.maxItems + 1} items)`,
|
|
248
|
+
body: { ...base, [field.name]: Array.from({ length: json.maxItems + 1 }, () => null) },
|
|
249
|
+
expected_error: ERROR_INVALID_REQUEST_BODY
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// schema can't be converted to JSON Schema, skip boundary cases
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return cases;
|
|
259
|
+
};
|
|
260
|
+
// --- Params test case generation ---
|
|
261
|
+
/**
|
|
262
|
+
* Generate adversarial test cases for a route's params schema.
|
|
263
|
+
*
|
|
264
|
+
* Params are always strings from URL segments. Only generates cases for
|
|
265
|
+
* format-constrained fields (uuid, pattern) since unconstrained string
|
|
266
|
+
* params accept any string value.
|
|
267
|
+
*/
|
|
268
|
+
export const generate_params_test_cases = (params_schema) => {
|
|
269
|
+
const fields = zod_extract_fields(params_schema);
|
|
270
|
+
const cases = [];
|
|
271
|
+
// build base params with valid-ish values
|
|
272
|
+
const base_params = {};
|
|
273
|
+
for (const field of fields) {
|
|
274
|
+
const field_schema = params_schema.shape[field.name];
|
|
275
|
+
const format = detect_format(field_schema);
|
|
276
|
+
if (format === 'uuid' || field.base_type === 'uuid') {
|
|
277
|
+
base_params[field.name] = '00000000-0000-0000-0000-000000000000';
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
base_params[field.name] = 'test_value';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const field of fields) {
|
|
284
|
+
const field_schema = params_schema.shape[field.name];
|
|
285
|
+
const format = detect_format(field_schema);
|
|
286
|
+
if (!format)
|
|
287
|
+
continue; // unconstrained string — any value passes
|
|
288
|
+
const violation = format_violation(format);
|
|
289
|
+
if (violation) {
|
|
290
|
+
cases.push({
|
|
291
|
+
label: `${violation.label}: param ${field.name}`,
|
|
292
|
+
params: { ...base_params, [field.name]: violation.value }
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return cases;
|
|
297
|
+
};
|
|
298
|
+
// --- Query test case generation ---
|
|
299
|
+
/**
|
|
300
|
+
* Generate adversarial test cases for a route's query schema.
|
|
301
|
+
*
|
|
302
|
+
* Query params are always strings from the URL. Generates cases for:
|
|
303
|
+
* - Missing required fields
|
|
304
|
+
* - Format violations on constrained fields (uuid, pattern)
|
|
305
|
+
*/
|
|
306
|
+
export const generate_query_test_cases = (query_schema) => {
|
|
307
|
+
const fields = zod_extract_fields(query_schema);
|
|
308
|
+
const cases = [];
|
|
309
|
+
// build base query with valid-ish values
|
|
310
|
+
const base_query = {};
|
|
311
|
+
for (const field of fields) {
|
|
312
|
+
const field_schema = query_schema.shape[field.name];
|
|
313
|
+
const format = detect_format(field_schema);
|
|
314
|
+
if (format === 'uuid' || field.base_type === 'uuid') {
|
|
315
|
+
base_query[field.name] = '00000000-0000-0000-0000-000000000000';
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
base_query[field.name] = 'test_value';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const field of fields) {
|
|
322
|
+
// missing required field
|
|
323
|
+
if (field.required && !field.has_default) {
|
|
324
|
+
const without = {};
|
|
325
|
+
for (const [k, v] of Object.entries(base_query)) {
|
|
326
|
+
if (k !== field.name)
|
|
327
|
+
without[k] = v;
|
|
328
|
+
}
|
|
329
|
+
cases.push({
|
|
330
|
+
label: `missing query: ${field.name}`,
|
|
331
|
+
query: without
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// format violation for constrained string fields
|
|
335
|
+
const field_schema = query_schema.shape[field.name];
|
|
336
|
+
const format = detect_format(field_schema);
|
|
337
|
+
if (!format)
|
|
338
|
+
continue;
|
|
339
|
+
const violation = format_violation(format);
|
|
340
|
+
if (violation) {
|
|
341
|
+
cases.push({
|
|
342
|
+
label: `${violation.label}: query ${field.name}`,
|
|
343
|
+
query: { ...base_query, [field.name]: violation.value }
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return cases;
|
|
348
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,yEAAyE;IACzE,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;
|
|
1
|
+
{"version":3,"file":"audit_completeness.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_completeness.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AA4B7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAUhE,OAAO,EAKN,KAAK,uBAAuB,EAC5B,MAAM,kBAAkB,CAAC;AA2B1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,0BAA0B,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC5C;;;;;OAKG;IACH,UAAU,EAAE,SAAS,CAAC;IACtB;;;;;;OAMG;IACH,cAAc,EAAE,cAAc,CAAC;IAC/B,yEAAyE;IACzE,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,aAAa,EAAE,uBAAuB,CAAC;CACvC;AAoGD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iCAAiC,GAAI,SAAS,4BAA4B,KAAG,IAgnBzF,CAAC"}
|
|
@@ -54,8 +54,12 @@ const create_admin_observer = (fixture) => fixture.create_account({ username: 'a
|
|
|
54
54
|
* `observer` is a dedicated admin account (see `create_admin_observer`)
|
|
55
55
|
* — its credentials are never the subject of the mutation under test, so the
|
|
56
56
|
* read works uniformly across every flow including session-revoking ones.
|
|
57
|
+
*
|
|
58
|
+
* In-process only, so no drain precedes the read (`create_test_app` settles a
|
|
59
|
+
* request's effects before it responds) — the cross-process reader is
|
|
60
|
+
* `cross_backend/audit_cross_helpers.ts`'s `list_audit_events`.
|
|
57
61
|
*/
|
|
58
|
-
const
|
|
62
|
+
const read_audit_log = async (app, rpc_path, observer, params = {}) => {
|
|
59
63
|
const res = await rpc_call_for_spec({
|
|
60
64
|
app,
|
|
61
65
|
path: rpc_path,
|
|
@@ -128,7 +132,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
128
132
|
})
|
|
129
133
|
});
|
|
130
134
|
assert.strictEqual(res.status, 200);
|
|
131
|
-
const events = await
|
|
135
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
132
136
|
assert_has_event(events, 'login', 'POST /login (success)');
|
|
133
137
|
});
|
|
134
138
|
test('login failure produces login event with failure outcome', async () => {
|
|
@@ -145,7 +149,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
145
149
|
})
|
|
146
150
|
});
|
|
147
151
|
assert.strictEqual(res.status, 401);
|
|
148
|
-
const events = await
|
|
152
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
149
153
|
assert_has_event(events, 'login', 'POST /login (failure)');
|
|
150
154
|
});
|
|
151
155
|
test('logout produces logout event', async () => {
|
|
@@ -158,7 +162,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
158
162
|
headers: fixture.create_session_headers({ 'content-type': 'application/json' })
|
|
159
163
|
});
|
|
160
164
|
assert.strictEqual(res.status, 200);
|
|
161
|
-
const events = await
|
|
165
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
162
166
|
assert_has_event(events, 'logout', 'POST /logout');
|
|
163
167
|
});
|
|
164
168
|
test('token create produces token_create event', async () => {
|
|
@@ -176,7 +180,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
176
180
|
headers: fixture.create_session_headers()
|
|
177
181
|
});
|
|
178
182
|
assert.ok(res.ok, `account_token_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
179
|
-
const events = await
|
|
183
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
180
184
|
assert_has_event(events, 'token_create', 'account_token_create RPC');
|
|
181
185
|
assert_event_credential_type(events, 'token_create', 'session', 'account_token_create RPC');
|
|
182
186
|
});
|
|
@@ -202,7 +206,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
202
206
|
headers: fixture.create_session_headers()
|
|
203
207
|
});
|
|
204
208
|
assert.ok(res.ok, `account_token_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
205
|
-
const events = await
|
|
209
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
206
210
|
assert_has_event(events, 'token_revoke', 'account_token_revoke RPC');
|
|
207
211
|
assert_event_credential_type(events, 'token_revoke', 'session', 'account_token_revoke RPC');
|
|
208
212
|
});
|
|
@@ -242,7 +246,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
242
246
|
headers: fixture.create_session_headers()
|
|
243
247
|
});
|
|
244
248
|
assert.ok(res.ok, `account_session_revoke failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
245
|
-
const events = await
|
|
249
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
246
250
|
assert_has_event(events, 'session_revoke', 'account_session_revoke RPC');
|
|
247
251
|
assert_event_credential_type(events, 'session_revoke', 'session', 'account_session_revoke RPC');
|
|
248
252
|
});
|
|
@@ -257,7 +261,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
257
261
|
headers: fixture.create_session_headers()
|
|
258
262
|
});
|
|
259
263
|
assert.ok(res.ok, `account_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
260
|
-
const events = await
|
|
264
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
261
265
|
assert_has_event(events, 'session_revoke_all', 'account_session_revoke_all RPC');
|
|
262
266
|
assert_event_credential_type(events, 'session_revoke_all', 'session', 'account_session_revoke_all RPC');
|
|
263
267
|
});
|
|
@@ -275,7 +279,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
275
279
|
})
|
|
276
280
|
});
|
|
277
281
|
assert.strictEqual(res.status, 200);
|
|
278
|
-
const events = await
|
|
282
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
279
283
|
assert_has_event(events, 'password_change', 'POST /password');
|
|
280
284
|
assert_event_credential_type(events, 'password_change', 'session', 'POST /password');
|
|
281
285
|
});
|
|
@@ -298,7 +302,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
298
302
|
// Admin offer emits `role_grant_offer_create` only — the role_grant doesn't
|
|
299
303
|
// exist yet. Drive the accept to confirm `role_grant_offer_accept` and
|
|
300
304
|
// `role_grant_create` both fire on the downstream consent transition.
|
|
301
|
-
const events_after_offer = await
|
|
305
|
+
const events_after_offer = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
302
306
|
assert_has_event(events_after_offer, 'role_grant_offer_create', 'role_grant_offer_create RPC');
|
|
303
307
|
const accept_res = await rpc_call_for_spec({
|
|
304
308
|
app: { request: fixture.transport },
|
|
@@ -308,7 +312,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
308
312
|
headers: target.create_session_headers()
|
|
309
313
|
});
|
|
310
314
|
assert.ok(accept_res.ok, `role_grant_offer_accept failed: ${accept_res.ok ? '' : JSON.stringify(accept_res.error)}`);
|
|
311
|
-
const events_after_accept = await
|
|
315
|
+
const events_after_accept = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
312
316
|
assert_has_event(events_after_accept, 'role_grant_offer_accept', 'offer accept RPC');
|
|
313
317
|
assert_has_event(events_after_accept, 'role_grant_create', 'offer accept RPC');
|
|
314
318
|
});
|
|
@@ -335,7 +339,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
335
339
|
headers: fixture.create_session_headers()
|
|
336
340
|
});
|
|
337
341
|
assert.ok(revoke_res.ok, `role_grant_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
338
|
-
const events = await
|
|
342
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
339
343
|
assert_has_event(events, 'role_grant_revoke', 'role_grant_revoke RPC');
|
|
340
344
|
// Audit envelope must populate both target columns —
|
|
341
345
|
// `role_grant_revoke` is the canonical actor-bound-subject event.
|
|
@@ -357,7 +361,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
357
361
|
headers: fixture.create_session_headers()
|
|
358
362
|
});
|
|
359
363
|
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
360
|
-
const events = await
|
|
364
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
361
365
|
// admin session revoke-all also produces session_revoke_all
|
|
362
366
|
assert_has_event(events, 'session_revoke_all', 'admin_session_revoke_all RPC');
|
|
363
367
|
});
|
|
@@ -373,7 +377,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
373
377
|
headers: fixture.create_session_headers()
|
|
374
378
|
});
|
|
375
379
|
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
376
|
-
const events = await
|
|
380
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
377
381
|
assert_has_event(events, 'token_revoke_all', 'admin_token_revoke_all RPC');
|
|
378
382
|
});
|
|
379
383
|
});
|
|
@@ -399,7 +403,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
399
403
|
headers: fixture.create_session_headers()
|
|
400
404
|
});
|
|
401
405
|
assert.ok(delete_res.ok, `invite_delete failed: ${delete_res.ok ? '' : JSON.stringify(delete_res.error)}`);
|
|
402
|
-
const events = await
|
|
406
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
403
407
|
assert_has_event(events, 'invite_create', 'invite_create RPC');
|
|
404
408
|
assert_has_event(events, 'invite_delete', 'invite_delete RPC');
|
|
405
409
|
});
|
|
@@ -417,7 +421,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
417
421
|
headers: fixture.create_session_headers()
|
|
418
422
|
});
|
|
419
423
|
assert.ok(res.ok, `app_settings_update failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
420
|
-
const events = await
|
|
424
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
421
425
|
assert_has_event(events, 'app_settings_update', 'app_settings_update RPC');
|
|
422
426
|
});
|
|
423
427
|
});
|
|
@@ -435,7 +439,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
435
439
|
headers: fixture.create_session_headers()
|
|
436
440
|
});
|
|
437
441
|
assert.ok(res.ok, `account_delete failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
438
|
-
const events = await
|
|
442
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
439
443
|
assert_has_event(events, 'account_delete', 'account_delete RPC');
|
|
440
444
|
assert_has_event(events, 'actor_delete', 'account_delete RPC (per-actor cascade)');
|
|
441
445
|
});
|
|
@@ -455,7 +459,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
455
459
|
headers: fixture.create_daemon_token_headers()
|
|
456
460
|
});
|
|
457
461
|
assert.ok(res.ok, `account_purge failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
458
|
-
const events = await
|
|
462
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
459
463
|
assert_has_event(events, 'account_purge', 'account_purge RPC');
|
|
460
464
|
assert_has_event(events, 'actor_purge', 'account_purge RPC (per-actor cascade)');
|
|
461
465
|
});
|
|
@@ -479,7 +483,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
479
483
|
headers: fixture.create_session_headers()
|
|
480
484
|
});
|
|
481
485
|
assert.ok(res.ok, `account_undelete failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
482
|
-
const events = await
|
|
486
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
483
487
|
assert_has_event(events, 'account_undelete', 'account_undelete RPC');
|
|
484
488
|
assert_has_event(events, 'actor_undelete', 'account_undelete RPC (per-actor cascade)');
|
|
485
489
|
});
|
|
@@ -514,7 +518,7 @@ export const describe_audit_completeness_tests = (options) => {
|
|
|
514
518
|
})
|
|
515
519
|
});
|
|
516
520
|
assert.strictEqual(res.status, 200);
|
|
517
|
-
const events = await
|
|
521
|
+
const events = await read_audit_log({ request: fixture.transport }, rpc_path, observer);
|
|
518
522
|
assert_has_event(events, 'signup', 'POST /signup');
|
|
519
523
|
});
|
|
520
524
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import { type AuditEmitter } from '../auth/audit_emitter.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type AuditLogEventJson, type AuditLogInput } from '../auth/audit_log_schema.js';
|
|
4
4
|
/**
|
|
5
5
|
* Register per-test `beforeEach` + `afterEach` hooks that catch any audit
|
|
6
6
|
* emission with a metadata shape that fails its `audit_metadata_schemas`
|
|
@@ -81,4 +81,30 @@ export interface RecordingAuditEmitter {
|
|
|
81
81
|
* so the freeze isn't load-bearing here.
|
|
82
82
|
*/
|
|
83
83
|
export declare const create_recording_audit_emitter: (calls_ref?: Array<AuditLogInput>) => RecordingAuditEmitter;
|
|
84
|
+
/** Options for `assert_audit_event_metadata`. */
|
|
85
|
+
export interface AssertAuditEventMetadataOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Refuse metadata keys the event type doesn't declare. Off by default: the
|
|
88
|
+
* builtin schemas admit consumer-added fields. For a suite whose backend
|
|
89
|
+
* adds none — a spine's own — so an undeclared key on any spine fails.
|
|
90
|
+
*/
|
|
91
|
+
readonly reject_unknown_keys?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Assert an audit row read off the wire carries the metadata its event type
|
|
95
|
+
* declares — parsed against `audit_metadata_schemas[event_type]` as declared,
|
|
96
|
+
* so a mistyped, `null`, or missing field fails, and with
|
|
97
|
+
* `reject_unknown_keys` so does a key the event doesn't declare. The wire
|
|
98
|
+
* schema (`AuditLogEventJson.metadata`) is a loose record, and only the TS
|
|
99
|
+
* spine validates metadata, at insert and fail-open, so this is the check that
|
|
100
|
+
* holds a spine's rows to their event's contract.
|
|
101
|
+
*
|
|
102
|
+
* Builtin event types only: a consumer-registered type has no entry here, and
|
|
103
|
+
* passes unexamined.
|
|
104
|
+
*
|
|
105
|
+
* @param event - an audit row from `audit_log_list` or an audit read like it
|
|
106
|
+
* @param options - `reject_unknown_keys` for the closed-world check
|
|
107
|
+
* @throws AssertionError naming the event type and the schema issues
|
|
108
|
+
*/
|
|
109
|
+
export declare const assert_audit_event_metadata: (event: Pick<AuditLogEventJson, "event_type" | "outcome" | "metadata">, options?: AssertAuditEventMetadataOptions) => void;
|
|
84
110
|
//# sourceMappingURL=audit_drift_guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"audit_drift_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/audit_drift_guard.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAW7B,OAAO,EAGN,KAAK,YAAY,EACjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAGN,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AAErC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,yBAAyB,QAAO,IAO5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,QAAO,IAgBxC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,eAAO,MAAM,8BAA8B,GAC1C,YAAY,KAAK,CAAC,aAAa,CAAC,KAC9B,qBAqCF,CAAC;AA2BF,iDAAiD;AACjD,MAAM,WAAW,+BAA+B;IAC/C;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACvC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B,GACvC,OAAO,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,SAAS,GAAG,UAAU,CAAC,EACrE,UAAU,+BAA+B,KACvC,IAcF,CAAC"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
2
|
import { assert, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
3
4
|
import { get_audit_metadata_validation_failures, get_audit_unknown_event_type_failures, reset_audit_metadata_validation_failures, reset_audit_unknown_event_type_failures } from '../auth/audit_log_queries.js';
|
|
4
5
|
import { get_audit_write_failures, reset_audit_write_failures } from '../auth/audit_emitter.js';
|
|
6
|
+
import { audit_metadata_schemas } from '../auth/audit_log_schema.js';
|
|
5
7
|
/**
|
|
6
8
|
* Register per-test `beforeEach` + `afterEach` hooks that catch any audit
|
|
7
9
|
* emission with a metadata shape that fails its `audit_metadata_schemas`
|
|
@@ -120,3 +122,51 @@ export const create_recording_audit_emitter = (calls_ref) => {
|
|
|
120
122
|
};
|
|
121
123
|
return { emitter, calls };
|
|
122
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* The closed-world twin of a builtin event's `audit_metadata_schemas` entry:
|
|
127
|
+
* the same fields, with unknown keys refused. The registry's schemas are
|
|
128
|
+
* `z.looseObject` so a consumer can add fields to a builtin event (a builtin's
|
|
129
|
+
* schema can't be overridden), so this is opt-in.
|
|
130
|
+
*/
|
|
131
|
+
const to_closed_metadata_schema = (schema) => {
|
|
132
|
+
if (schema instanceof z.ZodNullable) {
|
|
133
|
+
return to_closed_metadata_schema(schema.unwrap()).nullable();
|
|
134
|
+
}
|
|
135
|
+
if (schema instanceof z.ZodObject)
|
|
136
|
+
return z.strictObject(schema.shape);
|
|
137
|
+
return schema;
|
|
138
|
+
};
|
|
139
|
+
const declared_metadata_schemas = new Map(Object.entries(audit_metadata_schemas));
|
|
140
|
+
const closed_metadata_schemas = new Map(Object.entries(audit_metadata_schemas).map(([event_type, schema]) => [
|
|
141
|
+
event_type,
|
|
142
|
+
to_closed_metadata_schema(schema)
|
|
143
|
+
]));
|
|
144
|
+
/**
|
|
145
|
+
* Assert an audit row read off the wire carries the metadata its event type
|
|
146
|
+
* declares — parsed against `audit_metadata_schemas[event_type]` as declared,
|
|
147
|
+
* so a mistyped, `null`, or missing field fails, and with
|
|
148
|
+
* `reject_unknown_keys` so does a key the event doesn't declare. The wire
|
|
149
|
+
* schema (`AuditLogEventJson.metadata`) is a loose record, and only the TS
|
|
150
|
+
* spine validates metadata, at insert and fail-open, so this is the check that
|
|
151
|
+
* holds a spine's rows to their event's contract.
|
|
152
|
+
*
|
|
153
|
+
* Builtin event types only: a consumer-registered type has no entry here, and
|
|
154
|
+
* passes unexamined.
|
|
155
|
+
*
|
|
156
|
+
* @param event - an audit row from `audit_log_list` or an audit read like it
|
|
157
|
+
* @param options - `reject_unknown_keys` for the closed-world check
|
|
158
|
+
* @throws AssertionError naming the event type and the schema issues
|
|
159
|
+
*/
|
|
160
|
+
export const assert_audit_event_metadata = (event, options) => {
|
|
161
|
+
const schema = (options?.reject_unknown_keys ? closed_metadata_schemas : declared_metadata_schemas).get(event.event_type);
|
|
162
|
+
if (!schema)
|
|
163
|
+
return;
|
|
164
|
+
const parsed = schema.safeParse(event.metadata);
|
|
165
|
+
if (parsed.success)
|
|
166
|
+
return;
|
|
167
|
+
assert.fail(`audit row ${event.event_type} (outcome=${event.outcome}) carries metadata outside its ` +
|
|
168
|
+
`audit_metadata_schemas entry: ${JSON.stringify(event.metadata)} — ` +
|
|
169
|
+
parsed.error.issues
|
|
170
|
+
.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
|
|
171
|
+
.join('; '));
|
|
172
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,
|
|
1
|
+
{"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAwaF,CAAC"}
|