@fuzdev/fuz_app 0.93.0 → 0.95.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/README.md +2 -2
- package/dist/actions/action_codegen.d.ts +18 -18
- package/dist/actions/action_codegen.js +30 -30
- package/dist/actions/action_event.js +1 -1
- package/dist/actions/action_event_helpers.d.ts +1 -1
- package/dist/actions/action_event_types.d.ts +1 -1
- package/dist/actions/action_rpc.d.ts +2 -2
- package/dist/actions/broadcast_api.d.ts +1 -1
- package/dist/actions/broadcast_api.js +1 -1
- package/dist/actions/perform_action.d.ts +2 -2
- package/dist/actions/register_action_ws.d.ts +2 -2
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.js +1 -1
- package/dist/actions/request_tracker.svelte.d.ts +1 -1
- package/dist/actions/request_tracker.svelte.js +1 -1
- package/dist/actions/rpc_client.d.ts +1 -1
- package/dist/actions/socket.svelte.d.ts +2 -2
- package/dist/actions/transports_ws.js +1 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
- package/dist/actions/transports_ws_backend.d.ts +1 -1
- package/dist/actions/transports_ws_backend.js +2 -2
- package/dist/auth/account_action_specs.js +1 -1
- package/dist/auth/account_schema.d.ts +1 -1
- package/dist/auth/account_schema.js +1 -1
- package/dist/auth/actor_lookup_action_specs.js +1 -1
- package/dist/auth/actor_lookup_queries.d.ts +1 -1
- package/dist/auth/actor_search_action_specs.js +1 -1
- package/dist/auth/actor_search_queries.d.ts +1 -1
- package/dist/auth/admin_action_specs.js +1 -1
- package/dist/auth/api_token.js +1 -1
- package/dist/auth/api_token_queries.d.ts +1 -1
- package/dist/auth/app_settings_schema.d.ts +1 -1
- package/dist/auth/app_settings_schema.js +1 -1
- package/dist/auth/audit_emitter.d.ts +2 -2
- package/dist/auth/audit_log_routes.d.ts +1 -1
- package/dist/auth/audit_log_schema.d.ts +1 -1
- package/dist/auth/audit_log_schema.js +2 -2
- package/dist/auth/bearer_auth.d.ts +1 -1
- package/dist/auth/bootstrap_account.d.ts +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -1
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/bootstrap_routes.js +1 -1
- package/dist/auth/cell_action_specs.js +2 -2
- package/dist/auth/cell_audit_action_specs.js +1 -1
- package/dist/auth/cell_audit_metadata.js +1 -1
- package/dist/auth/cell_field_action_specs.js +1 -1
- package/dist/auth/cell_field_audit_metadata.js +1 -1
- package/dist/auth/cell_grant_action_specs.js +1 -1
- package/dist/auth/cell_grant_audit_metadata.js +1 -1
- package/dist/auth/cell_item_action_specs.d.ts +1 -1
- package/dist/auth/cell_item_action_specs.js +3 -3
- package/dist/auth/cell_item_audit_metadata.js +1 -1
- package/dist/auth/cell_relation_visibility.d.ts +1 -1
- package/dist/auth/cleanup.d.ts +1 -1
- package/dist/auth/daemon_token_middleware.d.ts +1 -1
- package/dist/auth/deps.d.ts +2 -2
- package/dist/auth/invite_schema.d.ts +1 -1
- package/dist/auth/invite_schema.js +1 -1
- package/dist/auth/request_context.d.ts +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +1 -1
- package/dist/auth/role_grant_offer_notifications.d.ts +1 -1
- package/dist/auth/role_grant_offer_notifications.js +1 -1
- package/dist/auth/role_grant_offer_queries.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.d.ts +1 -1
- package/dist/auth/role_grant_offer_schema.js +1 -1
- package/dist/auth/role_grant_queries.d.ts +1 -1
- package/dist/auth/session_queries.d.ts +1 -1
- package/dist/auth/session_queries.js +1 -1
- package/dist/auth/signup_route_schema.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/logger.d.ts +1 -1
- package/dist/cli/logger.js +1 -1
- package/dist/db/CLAUDE.md +1 -1
- package/dist/db/cell_audit_queries.d.ts +1 -1
- package/dist/db/cell_field_queries.d.ts +1 -1
- package/dist/db/cell_grant_queries.d.ts +1 -1
- package/dist/db/cell_item_queries.d.ts +1 -1
- package/dist/db/cell_queries.d.ts +3 -3
- package/dist/db/cell_queries.js +1 -1
- package/dist/db/fact_disk_storage.d.ts +2 -2
- package/dist/db/fact_disk_storage.js +4 -4
- package/dist/db/fact_queries.d.ts +1 -1
- package/dist/db/fact_store.d.ts +3 -3
- package/dist/db/fact_store.js +2 -2
- package/dist/db/file_fact_url.d.ts +1 -1
- package/dist/db/file_fact_url.js +1 -1
- package/dist/db/migrate.js +1 -1
- package/dist/db/status.js +1 -1
- package/dist/http/auth_shape.js +2 -2
- package/dist/http/common_routes.d.ts +1 -1
- package/dist/http/db_routes.d.ts +1 -1
- package/dist/http/origin.js +1 -1
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/proxy.d.ts +1 -1
- package/dist/http/route_spec.d.ts +1 -1
- package/dist/rate_limiter.js +1 -1
- package/dist/realtime/sse.d.ts +1 -1
- package/dist/realtime/sse_auth_guard.d.ts +1 -1
- package/dist/runtime/deno.js +1 -1
- package/dist/server/app_backend.d.ts +1 -1
- package/dist/server/app_backend.js +1 -1
- package/dist/server/env.js +1 -1
- package/dist/server/fact_write.d.ts +2 -2
- package/dist/server/fact_write.js +1 -1
- package/dist/server/serve_fact_route.d.ts +1 -1
- package/dist/server/serve_fact_route.js +2 -2
- package/dist/server/startup.d.ts +1 -1
- package/dist/testing/CLAUDE.md +61 -9
- package/dist/testing/adversarial_input.js +1 -1
- package/dist/testing/app_server.d.ts +1 -1
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/auth_apps.js +1 -1
- package/dist/testing/cross_backend/bench/bench_report.d.ts +1 -1
- package/dist/testing/cross_backend/bench/bench_report.js +2 -2
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +1 -1
- package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +1 -1
- package/dist/testing/cross_backend/bootstrap_backend.d.ts +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +25 -4
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -4
- package/dist/testing/cross_backend/cell_relations.js +1 -1
- package/dist/testing/cross_backend/conformance_case.d.ts +4 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +26 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +21 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +109 -7
- package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +1 -1
- package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +1 -1
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +7 -0
- package/dist/testing/cross_backend/default_spine_surface.d.ts +1 -1
- package/dist/testing/cross_backend/fact_serving.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts +23 -0
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -0
- package/dist/testing/cross_backend/peer_ping_ws.js +209 -0
- package/dist/testing/cross_backend/setup.d.ts +1 -1
- package/dist/testing/cross_backend/setup.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +2 -2
- package/dist/testing/cross_backend/testing_server_core.d.ts +1 -1
- package/dist/testing/cross_backend/testing_server_core.js +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +1 -1
- package/dist/testing/db.js +4 -4
- package/dist/testing/middleware.js +1 -1
- package/dist/testing/role_grant_helpers.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +18 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +24 -2
- package/dist/testing/schema_generators.d.ts +1 -1
- package/dist/testing/schema_generators.js +1 -1
- package/dist/testing/stubs.js +1 -1
- package/dist/testing/testing_rate_limiter.d.ts +1 -1
- package/dist/testing/testing_rate_limiter.js +1 -1
- package/dist/testing/transports/bootstrap.d.ts +1 -1
- package/dist/testing/transports/bootstrap.js +1 -1
- package/dist/testing/transports/ws_client.d.ts +40 -1
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_client.js +46 -1
- package/dist/testing/transports/ws_transport.d.ts +10 -1
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +6 -10
- package/dist/testing/ws_round_trip.d.ts +13 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +16 -12
- package/dist/ui/AdminSurface.svelte +1 -1
- package/dist/ui/AppShell.svelte +1 -1
- package/dist/ui/BootstrapForm.svelte +1 -1
- package/dist/ui/CLAUDE.md +1 -1
- package/dist/ui/ConfirmButton.svelte +2 -2
- package/dist/ui/ConfirmButton.svelte.d.ts +1 -1
- package/dist/ui/LoginForm.svelte +1 -1
- package/dist/ui/MenuLink.svelte +1 -1
- package/dist/ui/MenuLink.svelte.d.ts +1 -1
- package/dist/ui/PopoverButton.svelte +1 -1
- package/dist/ui/PopoverButton.svelte.d.ts +1 -1
- package/dist/ui/SignupForm.svelte +1 -1
- package/dist/ui/account_sessions_state.svelte.js +1 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +1 -1
- package/dist/ui/admin_invites_state.svelte.d.ts +1 -1
- package/dist/ui/admin_invites_state.svelte.js +1 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +1 -1
- package/dist/ui/app_settings_state.svelte.js +1 -1
- package/dist/ui/async_slot.svelte.d.ts +2 -2
- package/dist/ui/async_slot.svelte.js +2 -2
- package/dist/ui/audit_log_state.svelte.js +1 -1
- package/dist/ui/auth_state.svelte.d.ts +1 -1
- package/dist/ui/auth_state.svelte.js +3 -3
- package/dist/ui/format_scope.js +1 -1
- package/dist/ui/popover.svelte.d.ts +1 -1
- package/dist/ui/popover.svelte.js +2 -2
- package/dist/ui/position_helpers.js +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +1 -1
- package/dist/ui/sidebar_state.svelte.js +1 -1
- package/dist/ui/table_state.svelte.d.ts +1 -1
- package/dist/ui/table_state.svelte.js +2 -2
- package/package.json +2 -2
|
@@ -16,11 +16,17 @@ import '../assert_dev_env.js';
|
|
|
16
16
|
* row runs `as` resolves to a `TestFixture` accessor via
|
|
17
17
|
* `resolve_principal` — no inline credential minting.
|
|
18
18
|
*
|
|
19
|
+
* Every response also passes an always-on **no-fingerprint** invariant
|
|
20
|
+
* (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
|
|
21
|
+
* `X-Powered-By` / `WWW-Authenticate` must stay absent on both spines), and
|
|
22
|
+
* a row may pin further header expectations via `expect.headers`. Headers are
|
|
23
|
+
* deliberately kept out of the equivalence-group `{status, body}` comparison.
|
|
24
|
+
*
|
|
19
25
|
* @module
|
|
20
26
|
*/
|
|
21
27
|
import { assert, describe, test } from 'vitest';
|
|
22
28
|
import { find_auth_route, rest_auth_route_suffixes, } from '../integration_helpers.js';
|
|
23
|
-
import { find_rpc_action, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
29
|
+
import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
|
|
24
30
|
import {} from './conformance_case.js';
|
|
25
31
|
import { xfail_until } from './xfail.js';
|
|
26
32
|
/**
|
|
@@ -91,19 +97,70 @@ const assert_fields = (actual, fields, label) => {
|
|
|
91
97
|
assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
|
|
92
98
|
}
|
|
93
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Response headers that fingerprint the backend implementation or framework.
|
|
102
|
+
* Neither spine emits these; the runner asserts they stay absent on EVERY
|
|
103
|
+
* conformance response so a framework upgrade or consumer middleware that adds
|
|
104
|
+
* (say) `Server:` to one spine can't silently become a backend-identifying
|
|
105
|
+
* oracle. Lowercased — matched against the lowercased-key snapshot
|
|
106
|
+
* `headers_to_record` produces.
|
|
107
|
+
*/
|
|
108
|
+
export const FINGERPRINT_HEADERS = [
|
|
109
|
+
'server',
|
|
110
|
+
'x-powered-by',
|
|
111
|
+
'www-authenticate',
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Assert a response carries none of the `FINGERPRINT_HEADERS`. Run on every
|
|
115
|
+
* case unconditionally — the always-on no-fingerprint floor.
|
|
116
|
+
*/
|
|
117
|
+
export const assert_no_fingerprint_headers = (headers, label) => {
|
|
118
|
+
for (const name of FINGERPRINT_HEADERS) {
|
|
119
|
+
assert.ok(!(name in headers), `${label}: response leaked backend-fingerprinting header '${name}: ${headers[name]}' — ` +
|
|
120
|
+
`Server / X-Powered-By / WWW-Authenticate must stay absent on both spines.`);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Assert each declared header expectation: a string value must be present and
|
|
125
|
+
* equal (header name matched case-insensitively), `null` must be absent. The
|
|
126
|
+
* negative-space twin for headers — `expect.headers` pins a header beyond the
|
|
127
|
+
* always-on no-fingerprint floor.
|
|
128
|
+
*/
|
|
129
|
+
export const assert_expected_headers = (headers, expected, label) => {
|
|
130
|
+
for (const [name, value] of Object.entries(expected)) {
|
|
131
|
+
const key = name.toLowerCase();
|
|
132
|
+
if (value === null) {
|
|
133
|
+
assert.ok(!(key in headers), `${label}: header '${name}' expected absent but present as '${headers[key]}'`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
assert.strictEqual(headers[key], value, `${label}: header '${name}'`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Run a case's header invariants: the always-on no-fingerprint floor on every
|
|
142
|
+
* response plus any declared `expect.headers`. Header assertions are kept off
|
|
143
|
+
* the equivalence-group `{status, body}` normalized response — `Set-Cookie` /
|
|
144
|
+
* `Date` legitimately vary, so headers are never compared for byte-identity.
|
|
145
|
+
*/
|
|
146
|
+
const assert_response_headers = (headers, c) => {
|
|
147
|
+
assert_no_fingerprint_headers(headers, c.name);
|
|
148
|
+
if (c.expect.headers)
|
|
149
|
+
assert_expected_headers(headers, c.expect.headers, c.name);
|
|
150
|
+
};
|
|
94
151
|
const is_success_status = (status) => status >= 200 && status < 300;
|
|
95
152
|
/**
|
|
96
153
|
* Run one conformance case end-to-end: resolve the principal, dispatch the
|
|
97
|
-
* request, and assert the expected status / reason / fields.
|
|
154
|
+
* request, and assert the expected status / reason / fields. Returns the
|
|
155
|
+
* normalized response so the caller can feed an equivalence group.
|
|
98
156
|
*/
|
|
99
157
|
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
100
158
|
const fixture = await options.setup_test();
|
|
101
159
|
const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
|
|
102
160
|
if (c.request.method.startsWith('/')) {
|
|
103
|
-
|
|
104
|
-
return;
|
|
161
|
+
return run_rest_case(c, options, transport, headers);
|
|
105
162
|
}
|
|
106
|
-
|
|
163
|
+
return run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
|
|
107
164
|
};
|
|
108
165
|
/** Dispatch + assert a case targeting an RPC method. */
|
|
109
166
|
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
@@ -121,6 +178,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
121
178
|
...(c.request.verb && { verb: c.request.verb }),
|
|
122
179
|
...(suppress_default_origin && { suppress_default_origin: true }),
|
|
123
180
|
});
|
|
181
|
+
assert_response_headers(res.headers, c);
|
|
124
182
|
if (is_success_status(c.expect.status)) {
|
|
125
183
|
assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
|
|
126
184
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
|
|
@@ -128,7 +186,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
128
186
|
assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
129
187
|
if (c.expect.fields)
|
|
130
188
|
assert_fields(res.result, c.expect.fields, c.name);
|
|
131
|
-
return;
|
|
189
|
+
return { status: res.status, body: res.result };
|
|
132
190
|
}
|
|
133
191
|
assert.ok(!res.ok, `${c.name}: expected error status ${c.expect.status} but got success`);
|
|
134
192
|
assert.strictEqual(res.status, c.expect.status, `${c.name}: error status`);
|
|
@@ -145,6 +203,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
|
|
|
145
203
|
}
|
|
146
204
|
if (c.expect.fields)
|
|
147
205
|
assert_fields(res.error.data, c.expect.fields, c.name);
|
|
206
|
+
return { status: res.status, body: res.error };
|
|
148
207
|
};
|
|
149
208
|
/** Dispatch + assert a case targeting one of the 6 REST auth routes. */
|
|
150
209
|
const run_rest_case = async (c, options, transport, headers) => {
|
|
@@ -165,6 +224,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
165
224
|
c.request.params !== undefined && { body: JSON.stringify(c.request.params) }),
|
|
166
225
|
};
|
|
167
226
|
const response = await transport(route.path, init);
|
|
227
|
+
assert_response_headers(headers_to_record(response.headers), c);
|
|
168
228
|
assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
|
|
169
229
|
const body = await response.json().catch(() => undefined);
|
|
170
230
|
if (is_success_status(c.expect.status)) {
|
|
@@ -177,6 +237,7 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
177
237
|
}
|
|
178
238
|
if (c.expect.fields)
|
|
179
239
|
assert_fields(body, c.expect.fields, c.name);
|
|
240
|
+
return { status: response.status, body };
|
|
180
241
|
};
|
|
181
242
|
/**
|
|
182
243
|
* Register a `describe` block running every `ConformanceCase` as a
|
|
@@ -186,10 +247,28 @@ const run_rest_case = async (c, options, transport, headers) => {
|
|
|
186
247
|
*/
|
|
187
248
|
export const describe_conformance_table_tests = (options) => {
|
|
188
249
|
const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
250
|
+
// Per-group normalized responses, populated as the member case tests run
|
|
251
|
+
// and consulted by the per-group byte-identity assertions registered after
|
|
252
|
+
// the case loop — vitest runs a describe's tests in registration order, so
|
|
253
|
+
// the group assertions execute last, after every member has recorded.
|
|
254
|
+
const equivalence_groups = new Map();
|
|
255
|
+
const group_names = new Set();
|
|
256
|
+
for (const c of options.cases) {
|
|
257
|
+
if (c.expect.equivalence_group)
|
|
258
|
+
group_names.add(c.expect.equivalence_group);
|
|
259
|
+
}
|
|
189
260
|
describe(options.suite_name ?? 'conformance table', () => {
|
|
190
261
|
for (const c of options.cases) {
|
|
191
262
|
const label = c.note ? `${c.name} — ${c.note}` : c.name;
|
|
192
|
-
const
|
|
263
|
+
const group = c.expect.equivalence_group;
|
|
264
|
+
const body = async () => {
|
|
265
|
+
const normalized = await run_case(c, options, resolved_rpc_endpoints);
|
|
266
|
+
if (group) {
|
|
267
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
268
|
+
members.push({ name: c.name, normalized });
|
|
269
|
+
equivalence_groups.set(group, members);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
193
272
|
if (c.xfail) {
|
|
194
273
|
xfail_until(c.xfail.tracking_id, c.xfail.reason, label, body);
|
|
195
274
|
}
|
|
@@ -197,5 +276,28 @@ export const describe_conformance_table_tests = (options) => {
|
|
|
197
276
|
test(label, body);
|
|
198
277
|
}
|
|
199
278
|
}
|
|
279
|
+
// Negative-space gate: every member of an equivalence group must have
|
|
280
|
+
// produced a byte-identical `{status, body}`. This holds the impl under
|
|
281
|
+
// test to "a prober cannot distinguish these masked paths" — the
|
|
282
|
+
// promotion of a masked pair (wrong-password ≡ account-not-found,
|
|
283
|
+
// found-but-unauthorized ≡ not-found) from "same status + reason" to
|
|
284
|
+
// "wire-indistinguishable". Runs per impl, so each spine is held to it.
|
|
285
|
+
for (const group of group_names) {
|
|
286
|
+
test(`equivalence group '${group}' — members are wire-indistinguishable`, () => {
|
|
287
|
+
const members = equivalence_groups.get(group) ?? [];
|
|
288
|
+
assert.ok(members.length >= 2, `equivalence group '${group}': expected >= 2 members to have recorded, got ` +
|
|
289
|
+
`${members.length} — a group pins indistinguishability between paths, so it ` +
|
|
290
|
+
`needs at least a pair (check the member cases ran and weren't all xfail).`);
|
|
291
|
+
const [first, ...rest] = members;
|
|
292
|
+
// `members.length >= 2` guarantees `first` is defined; the explicit
|
|
293
|
+
// assert narrows it for the typechecker (noUncheckedIndexedAccess).
|
|
294
|
+
assert.ok(first);
|
|
295
|
+
for (const member of rest) {
|
|
296
|
+
assert.deepStrictEqual(member.normalized, first.normalized, `equivalence group '${group}': '${member.name}' is distinguishable from ` +
|
|
297
|
+
`'${first.name}' — a prober could tell these masked paths apart. The ` +
|
|
298
|
+
`normalized {status, body} must be byte-identical across all members.`);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
200
302
|
});
|
|
201
303
|
};
|
|
@@ -13,7 +13,7 @@ import '../assert_dev_env.js';
|
|
|
13
13
|
*
|
|
14
14
|
* ```ts
|
|
15
15
|
* import {create_cross_backend_global_setup} from
|
|
16
|
-
* '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.
|
|
16
|
+
* '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts';
|
|
17
17
|
* import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
|
|
18
18
|
* import './cross_test_types.js'; // augments inject('backend_handle')
|
|
19
19
|
*
|
|
@@ -17,7 +17,7 @@ import '../assert_dev_env.js';
|
|
|
17
17
|
*
|
|
18
18
|
* ```ts
|
|
19
19
|
* import {create_dual_spawn_global_setup} from
|
|
20
|
-
* '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.
|
|
20
|
+
* '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.ts';
|
|
21
21
|
* import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
|
|
22
22
|
* import './cross_test_types.js'; // augments the two provide keys
|
|
23
23
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,
|
|
1
|
+
{"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAmBtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,oDAAoD;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
|
|
@@ -35,6 +35,10 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
35
35
|
// posture). A Bun-served consumer overrides to `false` (see the bun spine
|
|
36
36
|
// config) — fail-loud rather than silently skipping the smuggle detector.
|
|
37
37
|
oversized_reject_closes_connection: true,
|
|
38
|
+
// The TS server can't yet initiate server→client requests (the
|
|
39
|
+
// `BackendWebsocketTransport.send()` request path is the deferred
|
|
40
|
+
// twin-impl convergence), so the TS family skips the peer/ping suite.
|
|
41
|
+
peer_request: false,
|
|
38
42
|
});
|
|
39
43
|
/**
|
|
40
44
|
* Capabilities for the Rust family. The non-gating wiring facts live in
|
|
@@ -57,6 +61,9 @@ export const rust_default_capabilities = Object.freeze({
|
|
|
57
61
|
// hyper sends an RST on the oversized-body reject — the connection closes
|
|
58
62
|
// and the pipelined request is never reached.
|
|
59
63
|
oversized_reject_closes_connection: true,
|
|
64
|
+
// Server-initiated requests landed Rust-first canonical — the Rust spine
|
|
65
|
+
// drives the `peer/ping` round-trip, so the family opts into the suite.
|
|
66
|
+
peer_request: true,
|
|
60
67
|
});
|
|
61
68
|
/**
|
|
62
69
|
* Shape notes for TS-family backends — wiring facts, not gating flags.
|
|
@@ -23,7 +23,7 @@ import '../assert_dev_env.js';
|
|
|
23
23
|
*
|
|
24
24
|
* @module
|
|
25
25
|
*/
|
|
26
|
-
import type { Logger } from '@fuzdev/fuz_util/log.
|
|
26
|
+
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
27
27
|
import type { NotificationSender } from '../../auth/role_grant_offer_notifications.js';
|
|
28
28
|
import { type RoleSchemaResult } from '../../auth/role_schema.js';
|
|
29
29
|
import { type SessionOptions } from '../../auth/session_cookie.js';
|
|
@@ -38,7 +38,7 @@ import '../assert_dev_env.js';
|
|
|
38
38
|
*/
|
|
39
39
|
import { describe, assert } from 'vitest';
|
|
40
40
|
import { z } from 'zod';
|
|
41
|
-
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.
|
|
41
|
+
import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
42
42
|
import { CellCreateOutput } from '../../auth/cell_action_specs.js';
|
|
43
43
|
import { test_if } from './capabilities.js';
|
|
44
44
|
import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import { type BackendCapabilities } from './capabilities.js';
|
|
3
|
+
import type { SetupTest } from './setup.js';
|
|
4
|
+
/** Configuration for {@link describe_peer_ping_ws_tests}. */
|
|
5
|
+
export interface PeerPingWsTestOptions {
|
|
6
|
+
/** Per-test fixture producer (`default_cross_process_setup(handle, ...)`). */
|
|
7
|
+
readonly setup_test: SetupTest;
|
|
8
|
+
/** Backend capability flags; every case gates on `capabilities.peer_request`. */
|
|
9
|
+
readonly capabilities: BackendCapabilities;
|
|
10
|
+
/** Base URL the backend is reachable at (e.g. `http://localhost:1177`). */
|
|
11
|
+
readonly base_url: string;
|
|
12
|
+
/** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
|
|
13
|
+
readonly ws_path: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register the server-initiated `peer/ping` suite — a positive round-trip
|
|
17
|
+
* plus the security negatives the design doc's §Security surface mandates
|
|
18
|
+
* (unsolicited-response rejection, per-connection id isolation, never-reply
|
|
19
|
+
* `Timeout`, wrong-shape reply rejection, plus client-error forwarding and
|
|
20
|
+
* the HTTP no-transport path). Gated on `capabilities.peer_request`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const describe_peer_ping_ws_tests: (options: PeerPingWsTestOptions) => void;
|
|
23
|
+
//# sourceMappingURL=peer_ping_ws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer_ping_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/peer_ping_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,OAAO,EAAC,KAAK,mBAAmB,EAAU,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAkB1C,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,qBAAqB,KAAG,IAmN5E,CAAC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-process **server-initiated `peer/ping`** suite — the machinery
|
|
4
|
+
* proof for `ActionPeer` (a backend initiating a JSON-RPC request to a
|
|
5
|
+
* connected client and awaiting its typed reply). The sibling of the
|
|
6
|
+
* one-way notification suite (`role_grant_offer_notification_ws.ts`),
|
|
7
|
+
* extended from server→client *notifications* to server→client
|
|
8
|
+
* *request/response*.
|
|
9
|
+
*
|
|
10
|
+
* `peer/ping` is `initiator: both`: the client→server direction already
|
|
11
|
+
* exists as `heartbeat`; the new server→client direction is what this
|
|
12
|
+
* exercises. The observable trigger is the client invoking the `peer/ping`
|
|
13
|
+
* **action** over its own socket — the handler turns around and *initiates*
|
|
14
|
+
* a `peer/ping` request back to that socket, awaits the client's echo,
|
|
15
|
+
* validates it against `PingResponse`, and returns the validated shape. So
|
|
16
|
+
* one client RPC drives the whole round-trip, and every outcome
|
|
17
|
+
* (success / `Timeout` / wrong-shape / client-error) surfaces as that RPC's
|
|
18
|
+
* wire response — directly assertable.
|
|
19
|
+
*
|
|
20
|
+
* The client side attaches an `on_request` responder **at construction**
|
|
21
|
+
* (via `create_ws_transport`'s seam) so the server-initiated request is
|
|
22
|
+
* answered as soon as it arrives. Security negatives use the raw
|
|
23
|
+
* `WsClient.send` to inject unsolicited / cross-connection frames.
|
|
24
|
+
*
|
|
25
|
+
* **Per-spec auth.** `peer/ping` is `auth: public` (a liveness echo is
|
|
26
|
+
* non-sensitive — see the design doc); the WS upgrade itself still
|
|
27
|
+
* authenticates, so the suite drives it over the keeper's session.
|
|
28
|
+
*
|
|
29
|
+
* Gated on `capabilities.peer_request` — `true` only for the Rust spine
|
|
30
|
+
* (server-initiated requests landed Rust-first canonical); the TS family
|
|
31
|
+
* skips until its server transport's request path lands (deferred twin-impl
|
|
32
|
+
* convergence). Cross-process only: `create_ws_transport` needs a real bound
|
|
33
|
+
* socket, so wire it from a `*.cross.test.ts`.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
import { assert, describe } from 'vitest';
|
|
38
|
+
import { rpc_call } from '../rpc_helpers.js';
|
|
39
|
+
import { create_ws_transport } from '../transports/ws_transport.js';
|
|
40
|
+
import { is_response_for, } from '../transports/ws_client.js';
|
|
41
|
+
import { create_jsonrpc_request, is_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
|
|
42
|
+
import { test_if } from './capabilities.js';
|
|
43
|
+
/** JSON-RPC endpoint path — matches the spine's `/api/rpc` (and the forge's). */
|
|
44
|
+
const RPC_PATH = '/api/rpc';
|
|
45
|
+
/** Wire method (both directions). Mirrors the Rust `PEER_PING_METHOD`. */
|
|
46
|
+
const PEER_PING_METHOD = 'peer/ping';
|
|
47
|
+
// Stable `data.reason` discriminators — the cross-impl wire contract, kept
|
|
48
|
+
// as an independent TS-side copy of the Rust `REASON_PEER_*` constants
|
|
49
|
+
// (parity by test, not codegen).
|
|
50
|
+
const REASON_PEER_TIMEOUT = 'peer_timeout';
|
|
51
|
+
const REASON_PEER_INVALID_REPLY = 'peer_ping_invalid_reply';
|
|
52
|
+
const REASON_PEER_NO_TRANSPORT = 'peer_no_transport';
|
|
53
|
+
/** A reply payload that satisfies the Rust `PingResponse` shape. */
|
|
54
|
+
const valid_reply = (nonce) => ({ nonce, protocol_version: 1 });
|
|
55
|
+
/**
|
|
56
|
+
* Register the server-initiated `peer/ping` suite — a positive round-trip
|
|
57
|
+
* plus the security negatives the design doc's §Security surface mandates
|
|
58
|
+
* (unsolicited-response rejection, per-connection id isolation, never-reply
|
|
59
|
+
* `Timeout`, wrong-shape reply rejection, plus client-error forwarding and
|
|
60
|
+
* the HTTP no-transport path). Gated on `capabilities.peer_request`.
|
|
61
|
+
*/
|
|
62
|
+
export const describe_peer_ping_ws_tests = (options) => {
|
|
63
|
+
const { setup_test, capabilities, base_url, ws_path } = options;
|
|
64
|
+
// -- shared helpers -------------------------------------------------------
|
|
65
|
+
/** Open a WS transport for a session cookie, with an optional responder. */
|
|
66
|
+
const open_ws = (cookie, on_request) => {
|
|
67
|
+
assert.ok(cookie, 'expected a session cookie for the WS upgrade');
|
|
68
|
+
return create_ws_transport({
|
|
69
|
+
base_url,
|
|
70
|
+
ws_path,
|
|
71
|
+
cookies: [cookie],
|
|
72
|
+
origin: base_url,
|
|
73
|
+
on_request,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
/** A responder that echoes a valid `PingResponse` for every `peer/ping`. */
|
|
77
|
+
const echo_responder = (req) => {
|
|
78
|
+
const nonce = req.params?.nonce ?? 0;
|
|
79
|
+
return { result: valid_reply(nonce) };
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Invoke `peer/ping` over the socket and return the **raw** response
|
|
83
|
+
* frame (success or error) so the negatives can assert on
|
|
84
|
+
* `error.data.reason`. The client wait is generous (longer than any
|
|
85
|
+
* server-side `timeout_ms`) so the server's own outcome is what we read.
|
|
86
|
+
*/
|
|
87
|
+
const ping_raw = (ws, id, params) => ws
|
|
88
|
+
.send(create_jsonrpc_request(PEER_PING_METHOD, params, id))
|
|
89
|
+
.then(() => ws.wait_for(is_response_for(id), 8000));
|
|
90
|
+
const error_reason = (frame) => frame.error.data?.reason;
|
|
91
|
+
// -- tests ----------------------------------------------------------------
|
|
92
|
+
describe('peer/ping server-initiated request (cross-process)', () => {
|
|
93
|
+
test_if(capabilities.peer_request, 'a client peer/ping round-trips: server pings back, validates the echo, returns it', async () => {
|
|
94
|
+
const fixture = await setup_test();
|
|
95
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, echo_responder);
|
|
96
|
+
try {
|
|
97
|
+
const result = await ws.request(1, PEER_PING_METHOD, { nonce: 42 }, 8000);
|
|
98
|
+
assert.strictEqual(result.nonce, 42, 'the server echoes the issued nonce back');
|
|
99
|
+
assert.strictEqual(typeof result.protocol_version, 'number');
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await ws.close();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
test_if(capabilities.peer_request, 'a never-replying client yields a server-side Timeout (the liveness signal)', async () => {
|
|
106
|
+
const fixture = await setup_test();
|
|
107
|
+
// Responder swallows the ping (returns nothing) — the half-open peer.
|
|
108
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, () => undefined);
|
|
109
|
+
try {
|
|
110
|
+
// Short server-side deadline so the negative runs fast.
|
|
111
|
+
const frame = await ping_raw(ws, 2, { nonce: 1, timeout_ms: 300 });
|
|
112
|
+
assert.ok('error' in frame, 'a swallowed ping must error, not succeed');
|
|
113
|
+
assert.strictEqual(error_reason(frame), REASON_PEER_TIMEOUT);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await ws.close();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
test_if(capabilities.peer_request, 'a wrong-shape reply is rejected at the validation boundary, never handed to the caller', async () => {
|
|
120
|
+
const fixture = await setup_test();
|
|
121
|
+
// Reply that fails `PingResponse` validation (nonce wrong type,
|
|
122
|
+
// protocol_version missing).
|
|
123
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, () => ({
|
|
124
|
+
result: { nonce: 'not-a-number' },
|
|
125
|
+
}));
|
|
126
|
+
try {
|
|
127
|
+
const frame = await ping_raw(ws, 3, { nonce: 1, timeout_ms: 2000 });
|
|
128
|
+
assert.ok('error' in frame, 'a malformed reply must error, not return garbage');
|
|
129
|
+
assert.strictEqual(error_reason(frame), REASON_PEER_INVALID_REPLY);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await ws.close();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
test_if(capabilities.peer_request, 'a client JSON-RPC error reply surfaces as the action error (ClientError forwarded)', async () => {
|
|
136
|
+
const fixture = await setup_test();
|
|
137
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, () => ({
|
|
138
|
+
error: { code: -32603, message: 'client refused', data: { reason: 'client_says_no' } },
|
|
139
|
+
}));
|
|
140
|
+
try {
|
|
141
|
+
const frame = await ping_raw(ws, 4, { nonce: 1, timeout_ms: 2000 });
|
|
142
|
+
assert.ok('error' in frame, 'a client error reply must surface as an error');
|
|
143
|
+
assert.strictEqual(frame.error.code, -32603);
|
|
144
|
+
assert.strictEqual(error_reason(frame), 'client_says_no');
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
await ws.close();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
test_if(capabilities.peer_request, 'an unsolicited response is dropped: it resolves nothing and the socket survives', async () => {
|
|
151
|
+
const fixture = await setup_test();
|
|
152
|
+
const ws = await open_ws(fixture.create_session_headers().cookie, echo_responder);
|
|
153
|
+
try {
|
|
154
|
+
// Inject a reply for an id the server never issued on this socket.
|
|
155
|
+
await ws.send({ jsonrpc: '2.0', id: 's999999', result: valid_reply(7) });
|
|
156
|
+
// A legitimate round-trip still works → the junk frame neither
|
|
157
|
+
// crashed the read loop nor corrupted the pending registry.
|
|
158
|
+
const result = await ws.request(5, PEER_PING_METHOD, { nonce: 99 }, 8000);
|
|
159
|
+
assert.strictEqual(result.nonce, 99);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await ws.close();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
test_if(capabilities.peer_request, 'a reply on socket B never resolves a request issued on socket A (per-connection isolation)', async () => {
|
|
166
|
+
const fixture = await setup_test();
|
|
167
|
+
// A has NO responder, so the inbound server-initiated request is
|
|
168
|
+
// surfaced as a message A can observe (to learn its server id);
|
|
169
|
+
// A never answers it.
|
|
170
|
+
const ws_a = await open_ws(fixture.create_session_headers().cookie);
|
|
171
|
+
const ws_b = await open_ws(fixture.create_session_headers().cookie, echo_responder);
|
|
172
|
+
try {
|
|
173
|
+
// A invokes peer/ping with a short server-side deadline; don't
|
|
174
|
+
// await yet — observe the server→client request first.
|
|
175
|
+
const ping_promise = ping_raw(ws_a, 6, { nonce: 1, timeout_ms: 1200 });
|
|
176
|
+
// The server-initiated request A received (id "sN", nonce 1).
|
|
177
|
+
const inbound = await ws_a.wait_for((m) => is_jsonrpc_request(m) && m.method === PEER_PING_METHOD, 5000);
|
|
178
|
+
// B replies for A's id — a cross-connection echo. Correct
|
|
179
|
+
// isolation drops it (not pending on B), so A still times out.
|
|
180
|
+
await ws_b.send({
|
|
181
|
+
jsonrpc: '2.0',
|
|
182
|
+
id: inbound.id,
|
|
183
|
+
result: valid_reply(inbound.params.nonce),
|
|
184
|
+
});
|
|
185
|
+
const frame = await ping_promise;
|
|
186
|
+
assert.ok('error' in frame, "B's cross-connection reply must not resolve A's request");
|
|
187
|
+
assert.strictEqual(error_reason(frame), REASON_PEER_TIMEOUT);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await ws_a.close();
|
|
191
|
+
await ws_b.close();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
test_if(capabilities.peer_request, 'peer/ping over HTTP (no return socket) is rejected as no-transport', async () => {
|
|
195
|
+
const fixture = await setup_test();
|
|
196
|
+
const res = await rpc_call({
|
|
197
|
+
app: fixture.transport,
|
|
198
|
+
path: RPC_PATH,
|
|
199
|
+
method: PEER_PING_METHOD,
|
|
200
|
+
params: { nonce: 1 },
|
|
201
|
+
headers: fixture.create_session_headers(),
|
|
202
|
+
});
|
|
203
|
+
assert.isFalse(res.ok, 'HTTP peer/ping has no socket to ping');
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
assert.strictEqual(res.error.data?.reason, REASON_PEER_NO_TRANSPORT);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
2
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
3
3
|
import type { TestAccount } from '../app_server.js';
|
|
4
4
|
import type { BackendCapabilities } from './capabilities.js';
|
|
5
5
|
import { type FetchTransport } from '../transports/fetch_transport.js';
|
|
@@ -22,7 +22,7 @@ import '../assert_dev_env.js';
|
|
|
22
22
|
* @module
|
|
23
23
|
*/
|
|
24
24
|
import { z } from 'zod';
|
|
25
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
25
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
26
26
|
import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
|
|
27
27
|
import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
|
|
28
28
|
import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
|
|
@@ -66,8 +66,8 @@ import '../assert_dev_env.js';
|
|
|
66
66
|
* @module
|
|
67
67
|
*/
|
|
68
68
|
import { z } from 'zod';
|
|
69
|
-
import { Uuid } from '@fuzdev/fuz_util/id.
|
|
70
|
-
import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.
|
|
69
|
+
import { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
70
|
+
import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
71
71
|
import { rpc_action } from '../../actions/action_rpc.js';
|
|
72
72
|
import { query_put_fact } from '../../db/fact_queries.js';
|
|
73
73
|
import { ROLE_ADMIN, ROLE_KEEPER } from '../../auth/role_schema.js';
|
|
@@ -28,7 +28,7 @@ import '../assert_dev_env.js';
|
|
|
28
28
|
*/
|
|
29
29
|
import type { Context, Hono } from 'hono';
|
|
30
30
|
import type { UpgradeWebSocket } from 'hono/ws';
|
|
31
|
-
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.
|
|
31
|
+
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.ts';
|
|
32
32
|
import type { RuntimeDeps } from '../../runtime/deps.js';
|
|
33
33
|
/**
|
|
34
34
|
* Adapter-built handle to a bound HTTP server.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import '../assert_dev_env.js';
|
|
2
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
2
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
3
3
|
import { write_daemon_info, read_daemon_info, is_daemon_running } from '../../cli/daemon.js';
|
|
4
4
|
/**
|
|
5
5
|
* Loopback bind hosts — the only ones the test binary may serve on. It ships
|
|
@@ -41,7 +41,7 @@ import '../assert_dev_env.js';
|
|
|
41
41
|
* @module
|
|
42
42
|
*/
|
|
43
43
|
import { assert, describe } from 'vitest';
|
|
44
|
-
import { assert_rejects } from '@fuzdev/fuz_util/testing.
|
|
44
|
+
import { assert_rejects } from '@fuzdev/fuz_util/testing.ts';
|
|
45
45
|
import { heartbeat_action_spec } from '../../actions/heartbeat.js';
|
|
46
46
|
import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
|
|
47
47
|
import { create_ws_transport } from '../transports/ws_transport.js';
|
package/dist/testing/db.js
CHANGED
|
@@ -8,9 +8,9 @@ import './assert_dev_env.js';
|
|
|
8
8
|
*
|
|
9
9
|
* @example
|
|
10
10
|
* ```ts
|
|
11
|
-
* import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.
|
|
12
|
-
* import {run_migrations} from '@fuzdev/fuz_app/db/migrate.
|
|
13
|
-
* import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.
|
|
11
|
+
* import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.ts';
|
|
12
|
+
* import {run_migrations} from '@fuzdev/fuz_app/db/migrate.ts';
|
|
13
|
+
* import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.ts';
|
|
14
14
|
*
|
|
15
15
|
* const init_schema = async (db) => {
|
|
16
16
|
* await run_migrations(db, [auth_migration_ns]);
|
|
@@ -23,7 +23,7 @@ import './assert_dev_env.js';
|
|
|
23
23
|
* @module
|
|
24
24
|
*/
|
|
25
25
|
import { describe, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
26
|
-
import { to_error_message } from '@fuzdev/fuz_util/error.
|
|
26
|
+
import { to_error_message } from '@fuzdev/fuz_util/error.ts';
|
|
27
27
|
import { create_pglite_db } from '../db/db_pglite.js';
|
|
28
28
|
import { assert_valid_sql_identifier } from '../db/sql_identifier.js';
|
|
29
29
|
import { create_pg_db } from '../db/db_pg.js';
|
|
@@ -10,7 +10,7 @@ import './assert_dev_env.js';
|
|
|
10
10
|
*/
|
|
11
11
|
import { vi, test, assert, describe } from 'vitest';
|
|
12
12
|
import { Hono } from 'hono';
|
|
13
|
-
import { Logger } from '@fuzdev/fuz_util/log.
|
|
13
|
+
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
14
14
|
import { create_bearer_auth_middleware } from '../auth/bearer_auth.js';
|
|
15
15
|
import { query_validate_api_token } from '../auth/api_token_queries.js';
|
|
16
16
|
import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from '../auth/account_queries.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import './assert_dev_env.js';
|
|
2
|
-
import type { Uuid } from '@fuzdev/fuz_util/id.
|
|
2
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
3
3
|
import type { TestAccount, TestApp } from './app_server.js';
|
|
4
4
|
import type { TestFixture } from './cross_backend/setup.js';
|
|
5
5
|
import { type RpcCallArgs } from './rpc_helpers.js';
|