@fuzdev/fuz_app 0.67.1 → 0.69.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/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +10 -3
- package/dist/auth/CLAUDE.md +99 -5
- package/dist/auth/account_queries.d.ts +87 -4
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +107 -17
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +8 -0
- package/dist/auth/admin_action_specs.d.ts +170 -3
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +148 -4
- package/dist/auth/admin_actions.d.ts +4 -14
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +246 -40
- package/dist/auth/audit_log_ddl.d.ts +10 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +13 -4
- package/dist/auth/audit_log_schema.d.ts +34 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +73 -0
- package/dist/auth/auth_ddl.d.ts +2 -2
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +10 -2
- package/dist/auth/cell_action_specs.d.ts +1295 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_action_specs.js +397 -0
- package/dist/auth/cell_actions.d.ts +63 -0
- package/dist/auth/cell_actions.d.ts.map +1 -0
- package/dist/auth/cell_actions.js +546 -0
- package/dist/auth/cell_audit_action_specs.d.ts +131 -0
- package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_audit_action_specs.js +70 -0
- package/dist/auth/cell_audit_actions.d.ts +18 -0
- package/dist/auth/cell_audit_actions.d.ts.map +1 -0
- package/dist/auth/cell_audit_actions.js +59 -0
- package/dist/auth/cell_audit_events.d.ts +28 -0
- package/dist/auth/cell_audit_events.d.ts.map +1 -0
- package/dist/auth/cell_audit_events.js +42 -0
- package/dist/auth/cell_audit_metadata.d.ts +48 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_audit_metadata.js +46 -0
- package/dist/auth/cell_authorize.d.ts +88 -0
- package/dist/auth/cell_authorize.d.ts.map +1 -0
- package/dist/auth/cell_authorize.js +172 -0
- package/dist/auth/cell_data_schema.d.ts +44 -0
- package/dist/auth/cell_data_schema.d.ts.map +1 -0
- package/dist/auth/cell_data_schema.js +42 -0
- package/dist/auth/cell_field_action_specs.d.ts +244 -0
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_field_action_specs.js +136 -0
- package/dist/auth/cell_field_actions.d.ts +34 -0
- package/dist/auth/cell_field_actions.d.ts.map +1 -0
- package/dist/auth/cell_field_actions.js +153 -0
- package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
- package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_field_audit_metadata.js +28 -0
- package/dist/auth/cell_grant_action_specs.d.ts +333 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_grant_action_specs.js +148 -0
- package/dist/auth/cell_grant_actions.d.ts +50 -0
- package/dist/auth/cell_grant_actions.d.ts.map +1 -0
- package/dist/auth/cell_grant_actions.js +208 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_grant_audit_metadata.js +54 -0
- package/dist/auth/cell_item_action_specs.d.ts +331 -0
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_item_action_specs.js +182 -0
- package/dist/auth/cell_item_actions.d.ts +37 -0
- package/dist/auth/cell_item_actions.d.ts.map +1 -0
- package/dist/auth/cell_item_actions.js +204 -0
- package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
- package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_item_audit_metadata.js +32 -0
- package/dist/auth/cell_relation_visibility.d.ts +32 -0
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
- package/dist/auth/cell_relation_visibility.js +57 -0
- package/dist/auth/deps.d.ts +9 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +30 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +54 -0
- package/dist/auth/signup_routes.d.ts +0 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +9 -3
- package/dist/auth/standard_rpc_actions.d.ts +5 -5
- package/dist/auth/standard_rpc_actions.js +4 -4
- package/dist/db/CLAUDE.md +118 -0
- package/dist/db/cell_audit_queries.d.ts +26 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -0
- package/dist/db/cell_audit_queries.js +53 -0
- package/dist/db/cell_ddl.d.ts +151 -0
- package/dist/db/cell_ddl.d.ts.map +1 -0
- package/dist/db/cell_ddl.js +247 -0
- package/dist/db/cell_field_queries.d.ts +105 -0
- package/dist/db/cell_field_queries.d.ts.map +1 -0
- package/dist/db/cell_field_queries.js +113 -0
- package/dist/db/cell_grant_queries.d.ts +132 -0
- package/dist/db/cell_grant_queries.d.ts.map +1 -0
- package/dist/db/cell_grant_queries.js +145 -0
- package/dist/db/cell_history_ddl.d.ts +38 -0
- package/dist/db/cell_history_ddl.d.ts.map +1 -0
- package/dist/db/cell_history_ddl.js +61 -0
- package/dist/db/cell_item_queries.d.ts +107 -0
- package/dist/db/cell_item_queries.d.ts.map +1 -0
- package/dist/db/cell_item_queries.js +119 -0
- package/dist/db/cell_queries.d.ts +327 -0
- package/dist/db/cell_queries.d.ts.map +1 -0
- package/dist/db/cell_queries.js +431 -0
- package/dist/db/fact_ddl.d.ts +38 -0
- package/dist/db/fact_ddl.d.ts.map +1 -0
- package/dist/db/fact_ddl.js +71 -0
- package/dist/db/fact_queries.d.ts +140 -0
- package/dist/db/fact_queries.d.ts.map +1 -0
- package/dist/db/fact_queries.js +161 -0
- package/dist/db/fact_store.d.ts +112 -0
- package/dist/db/fact_store.d.ts.map +1 -0
- package/dist/db/fact_store.js +225 -0
- package/dist/server/app_server.d.ts +1 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -5
- package/dist/server/env.d.ts +2 -0
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +6 -0
- package/dist/server/fact_write.d.ts +32 -0
- package/dist/server/fact_write.d.ts.map +1 -0
- package/dist/server/fact_write.js +56 -0
- package/dist/server/file_fact_fetcher.d.ts +42 -0
- package/dist/server/file_fact_fetcher.d.ts.map +1 -0
- package/dist/server/file_fact_fetcher.js +60 -0
- package/dist/server/file_fact_url.d.ts +53 -0
- package/dist/server/file_fact_url.d.ts.map +1 -0
- package/dist/server/file_fact_url.js +52 -0
- package/dist/server/serve_fact_route.d.ts +78 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -0
- package/dist/server/serve_fact_route.js +205 -0
- package/dist/testing/CLAUDE.md +142 -6
- package/dist/testing/app_server.d.ts +46 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +67 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +67 -1
- package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/account_lifecycle.js +144 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +10 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_lookup.js +83 -0
- package/dist/testing/cross_backend/actor_search.d.ts +6 -0
- package/dist/testing/cross_backend/actor_search.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_search.js +92 -0
- package/dist/testing/cross_backend/app_settings.d.ts +6 -0
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -0
- package/dist/testing/cross_backend/app_settings.js +95 -0
- package/dist/testing/cross_backend/backend_config.d.ts +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +29 -7
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -1
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
- package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_crud.js +168 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts +8 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_grant_role.js +102 -0
- package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_relations.js +229 -0
- package/dist/testing/cross_backend/conformance_case.d.ts +144 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_case.js +132 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +46 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_table.js +199 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +6 -2
- package/dist/testing/cross_backend/default_spine_surface.d.ts +17 -9
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +20 -12
- package/dist/testing/cross_backend/origin.d.ts +10 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin.js +73 -0
- package/dist/testing/cross_backend/setup.d.ts +22 -40
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +39 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +90 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +91 -3
- package/dist/testing/cross_backend/xfail.d.ts +15 -0
- package/dist/testing/cross_backend/xfail.d.ts.map +1 -0
- package/dist/testing/cross_backend/xfail.js +37 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -0
- package/dist/testing/integration.d.ts +2 -3
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +20 -85
- package/dist/testing/rate_limiting.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +3 -3
- package/dist/testing/sse_round_trip.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +0 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +4 -0
- package/dist/ui/AdminAccounts.svelte +84 -35
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +21 -23
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +17 -26
- package/dist/ui/OpenSignupToggle.svelte +2 -5
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +9 -10
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +7 -17
- package/dist/ui/admin_accounts_state.svelte.d.ts +41 -20
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +52 -22
- package/dist/ui/admin_invites_state.svelte.d.ts +8 -11
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +7 -16
- package/dist/ui/admin_rpc_adapters.d.ts +6 -2
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +5 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +6 -10
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +4 -14
- package/dist/ui/app_settings_state.svelte.d.ts +8 -12
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +6 -16
- package/dist/ui/audit_log_state.svelte.d.ts +9 -8
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +8 -20
- package/package.json +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for the account-lifecycle admin verbs:
|
|
4
|
+
* `account_delete` (soft), `account_undelete` (reactivation), and
|
|
5
|
+
* `account_purge` (keeper hard-delete), plus the keeper guard.
|
|
6
|
+
*
|
|
7
|
+
* Like the cell suites, these verbs can't ride the generic
|
|
8
|
+
* `describe_rpc_round_trip_tests`: they're stateful and destructive (a
|
|
9
|
+
* generic round-trip would tombstone the bootstrapped keeper). They
|
|
10
|
+
* live-mount on every spine's RPC path but stay off the declared surface,
|
|
11
|
+
* so this dedicated suite is their cross-impl validator. Every success
|
|
12
|
+
* `result` is parsed against the verb's declared Zod **output** schema, so
|
|
13
|
+
* a TS↔Rust envelope drift fails the assertion.
|
|
14
|
+
*
|
|
15
|
+
* `$lib`-free by contract (relative specifiers only) so it can be imported
|
|
16
|
+
* from the spawnable cross-process test files.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import { describe, assert } from 'vitest';
|
|
21
|
+
import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput, AuditLogListOutput, ERROR_CANNOT_DELETE_KEEPER, } from '../../auth/admin_action_specs.js';
|
|
22
|
+
import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED } from '../../http/error_schemas.js';
|
|
23
|
+
import { test_if } from './capabilities.js';
|
|
24
|
+
import { cross_rpc_call, error_reason, expect_output, } from './cell_cross_helpers.js';
|
|
25
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
26
|
+
export const describe_account_lifecycle_cross_tests = (options) => {
|
|
27
|
+
const { setup_test, capabilities } = options;
|
|
28
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
29
|
+
describe('account lifecycle parity', () => {
|
|
30
|
+
test_if(capabilities.account_lifecycle, 'soft-delete → undelete round-trip (admin)', async () => {
|
|
31
|
+
const fixture = await setup_test();
|
|
32
|
+
const victim = await fixture.create_account({ username: 'lifecycle_victim' });
|
|
33
|
+
const t = fixture.fresh_transport();
|
|
34
|
+
// Keeper account holds ROLE_ADMIN — its session is admin-capable.
|
|
35
|
+
const admin_headers = fixture.create_session_headers();
|
|
36
|
+
const deleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: victim.account.id }, admin_headers), AccountDeleteOutput);
|
|
37
|
+
assert.strictEqual(deleted.deleted, true);
|
|
38
|
+
const undeleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers), AccountUndeleteOutput);
|
|
39
|
+
assert.strictEqual(undeleted.undeleted, true);
|
|
40
|
+
});
|
|
41
|
+
test_if(capabilities.account_lifecycle, 'purge (keeper, confirmed)', async () => {
|
|
42
|
+
const fixture = await setup_test();
|
|
43
|
+
const victim = await fixture.create_account({ username: 'lifecycle_purge' });
|
|
44
|
+
const t = fixture.fresh_transport({ origin: null });
|
|
45
|
+
// Purge is keeper-gated: daemon-token credential, not a session.
|
|
46
|
+
const purged = expect_output(await cross_rpc_call(t, rpc_path, 'account_purge', { account_id: victim.account.id, confirm: true }, fixture.create_daemon_token_headers()), AccountPurgeOutput);
|
|
47
|
+
assert.strictEqual(purged.purged, true);
|
|
48
|
+
});
|
|
49
|
+
test_if(capabilities.account_lifecycle, 'keeper guard: delete + purge refuse the keeper account', async () => {
|
|
50
|
+
const fixture = await setup_test();
|
|
51
|
+
const t = fixture.fresh_transport();
|
|
52
|
+
const del = await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: fixture.account.id }, fixture.create_session_headers());
|
|
53
|
+
assert.strictEqual(del.ok, false, 'delete of keeper account must be refused');
|
|
54
|
+
assert.strictEqual(error_reason(del), ERROR_CANNOT_DELETE_KEEPER);
|
|
55
|
+
const tp = fixture.fresh_transport({ origin: null });
|
|
56
|
+
const purge = await cross_rpc_call(tp, rpc_path, 'account_purge', { account_id: fixture.account.id, confirm: true }, fixture.create_daemon_token_headers());
|
|
57
|
+
assert.strictEqual(purge.ok, false, 'purge of keeper account must be refused');
|
|
58
|
+
assert.strictEqual(error_reason(purge), ERROR_CANNOT_DELETE_KEEPER);
|
|
59
|
+
});
|
|
60
|
+
test_if(capabilities.account_lifecycle, 'fail-closed: a soft-deleted account’s session + bearer credentials no longer authenticate', async () => {
|
|
61
|
+
const fixture = await setup_test();
|
|
62
|
+
const victim = await fixture.create_account({ username: 'lifecycle_failclosed' });
|
|
63
|
+
const admin_headers = fixture.create_session_headers();
|
|
64
|
+
// Sanity: the victim's session authenticates while active, so the
|
|
65
|
+
// post-deletion 401 is a real fail-closed transition, not a
|
|
66
|
+
// never-valid credential passing vacuously.
|
|
67
|
+
const before = await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'account_verify', undefined, victim.create_session_headers());
|
|
68
|
+
assert.ok(before.ok, 'victim session authenticates before deletion');
|
|
69
|
+
const deleted = expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'account_delete', { account_id: victim.account.id }, admin_headers), AccountDeleteOutput);
|
|
70
|
+
assert.strictEqual(deleted.deleted, true);
|
|
71
|
+
// The tombstone blocks auth resolution (and the soft-delete
|
|
72
|
+
// revoked sessions/tokens) — the stale session credential must
|
|
73
|
+
// fail closed with a generic 401, not partially authenticate.
|
|
74
|
+
const session_probe = await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'account_verify', undefined, victim.create_session_headers());
|
|
75
|
+
assert.strictEqual(session_probe.ok, false, 'soft-deleted account session must not authenticate');
|
|
76
|
+
assert.strictEqual(error_reason(session_probe), ERROR_AUTHENTICATION_REQUIRED);
|
|
77
|
+
// The victim's bearer token must fail closed too.
|
|
78
|
+
const bearer_probe = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'account_verify', undefined, victim.create_bearer_headers());
|
|
79
|
+
assert.strictEqual(bearer_probe.ok, false, 'soft-deleted account bearer token must not authenticate');
|
|
80
|
+
assert.strictEqual(error_reason(bearer_probe), ERROR_AUTHENTICATION_REQUIRED);
|
|
81
|
+
});
|
|
82
|
+
test_if(capabilities.account_lifecycle, 'keeper guard emits a fail-loud failure-audit row (drained, cross-impl)', async () => {
|
|
83
|
+
const fixture = await setup_test();
|
|
84
|
+
const t = fixture.fresh_transport();
|
|
85
|
+
// Refused keeper self-delete — the guard fires before any mutation
|
|
86
|
+
// and emits a forensic `outcome: failure` audit row.
|
|
87
|
+
const del = await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: fixture.account.id }, fixture.create_session_headers());
|
|
88
|
+
assert.strictEqual(error_reason(del), ERROR_CANNOT_DELETE_KEEPER);
|
|
89
|
+
// Deterministic barrier before reading: await in-flight
|
|
90
|
+
// fire-and-forget audit writes (the real await on the Rust spine;
|
|
91
|
+
// satisfied-by-construction on the TS spine via await_pending_effects).
|
|
92
|
+
const td = fixture.fresh_transport({ origin: null });
|
|
93
|
+
const drained = await cross_rpc_call(td, rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
|
|
94
|
+
assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
|
|
95
|
+
// The failure row is now authoritative on both spines. `_testing_reset`
|
|
96
|
+
// wiped audit_log at setup, so the refused delete is the only
|
|
97
|
+
// account_delete event.
|
|
98
|
+
const listed = expect_output(await cross_rpc_call(t, rpc_path, 'audit_log_list', { event_type: 'account_delete' }, fixture.create_session_headers()), AuditLogListOutput);
|
|
99
|
+
const failure = listed.events.find((e) => e.outcome === 'failure' &&
|
|
100
|
+
e.metadata?.reason === ERROR_CANNOT_DELETE_KEEPER);
|
|
101
|
+
assert.ok(failure, 'keeper-removal guard must emit an account_delete outcome=failure audit row with reason cannot_delete_keeper');
|
|
102
|
+
});
|
|
103
|
+
test_if(capabilities.account_lifecycle, 'deterministic: double-undelete → second call is not_found', async () => {
|
|
104
|
+
const fixture = await setup_test();
|
|
105
|
+
const victim = await fixture.create_account({ username: 'lifecycle_double' });
|
|
106
|
+
const t = fixture.fresh_transport();
|
|
107
|
+
const admin_headers = fixture.create_session_headers();
|
|
108
|
+
const deleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: victim.account.id }, admin_headers), AccountDeleteOutput);
|
|
109
|
+
assert.strictEqual(deleted.deleted, true);
|
|
110
|
+
// First undelete clears the tombstone.
|
|
111
|
+
const undeleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers), AccountUndeleteOutput);
|
|
112
|
+
assert.strictEqual(undeleted.undeleted, true);
|
|
113
|
+
// Second undelete on the now-active account is a deterministic
|
|
114
|
+
// not_found — the query only matches soft-deleted rows, so the
|
|
115
|
+
// outcome is the same on both spines (no silent idempotent ok).
|
|
116
|
+
const again = await cross_rpc_call(t, rpc_path, 'account_undelete', { account_id: victim.account.id }, admin_headers);
|
|
117
|
+
assert.strictEqual(again.ok, false, 'double-undelete must not silently succeed');
|
|
118
|
+
assert.strictEqual(error_reason(again), ERROR_ACCOUNT_NOT_FOUND);
|
|
119
|
+
});
|
|
120
|
+
// The last-admin guard (`ERROR_CANNOT_DELETE_LAST_ADMIN`) is **not**
|
|
121
|
+
// cross-process-testable against this fixture: the per-test keeper
|
|
122
|
+
// permanently holds `ROLE_ADMIN` (bootstrap seeds `[ROLE_KEEPER,
|
|
123
|
+
// ROLE_ADMIN]` and there is no remove-admin-from-keeper path), so a
|
|
124
|
+
// non-keeper admin is never the *sole* active admin and the guard
|
|
125
|
+
// never fires here. Its logic is covered in-process by
|
|
126
|
+
// `src/test/auth/account_keeper_guard.db.test.ts`.
|
|
127
|
+
test_if(capabilities.account_lifecycle, 'admin_account_list include_deleted surfaces tombstoned rows with deleted_at set', async () => {
|
|
128
|
+
const fixture = await setup_test();
|
|
129
|
+
const victim = await fixture.create_account({ username: 'lifecycle_listed' });
|
|
130
|
+
const admin_headers = fixture.create_session_headers();
|
|
131
|
+
const t = fixture.fresh_transport();
|
|
132
|
+
const deleted = expect_output(await cross_rpc_call(t, rpc_path, 'account_delete', { account_id: victim.account.id }, admin_headers), AccountDeleteOutput);
|
|
133
|
+
assert.strictEqual(deleted.deleted, true);
|
|
134
|
+
// Default listing excludes the tombstone.
|
|
135
|
+
const active_only = expect_output(await cross_rpc_call(t, rpc_path, 'admin_account_list', {}, admin_headers), AdminAccountListOutput);
|
|
136
|
+
assert.ok(!active_only.accounts.some((a) => a.account.id === victim.account.id), 'default listing excludes the soft-deleted account');
|
|
137
|
+
// `include_deleted` surfaces it with `deleted_at` populated.
|
|
138
|
+
const with_deleted = expect_output(await cross_rpc_call(t, rpc_path, 'admin_account_list', { include_deleted: true }, admin_headers), AdminAccountListOutput);
|
|
139
|
+
const row = with_deleted.accounts.find((a) => a.account.id === victim.account.id);
|
|
140
|
+
assert.ok(row, 'include_deleted surfaces the tombstoned row');
|
|
141
|
+
assert.ok(row.account.deleted_at !== null, 'tombstoned row carries a non-null deleted_at on both spines');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { CellCrossTestOptions } from './cell_cross_helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the actor-lookup parity suite. Shares the shape of the cell /
|
|
5
|
+
* origin suites (`setup_test` / `capabilities` / `rpc_path`); reuses
|
|
6
|
+
* `CellCrossTestOptions` rather than minting a structural duplicate.
|
|
7
|
+
*/
|
|
8
|
+
export type ActorLookupCrossTestOptions = CellCrossTestOptions;
|
|
9
|
+
export declare const describe_actor_lookup_cross_tests: (options: ActorLookupCrossTestOptions) => void;
|
|
10
|
+
//# sourceMappingURL=actor_lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor_lookup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_lookup.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqC9B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AAGlE;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,oBAAoB,CAAC;AAS/D,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAkDxF,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for `actor_lookup`.
|
|
4
|
+
*
|
|
5
|
+
* `actor_lookup` is an opt-in batched id → label resolver
|
|
6
|
+
* (`{ids} → {actors: [{id, username, display_name?}]}`), not folded into the
|
|
7
|
+
* standard bundle. It's live-mounted on the spine RPC path but kept off the
|
|
8
|
+
* declared surface (`create_spine_surface_spec`) — like cells / ws / sse — so
|
|
9
|
+
* the standard cross suite's generic round-trip never drives it; this
|
|
10
|
+
* dedicated suite is its validator. Three cases over raw transport calls:
|
|
11
|
+
*
|
|
12
|
+
* - **anonymous → 401** — the account-grain auth gate refuses an
|
|
13
|
+
* unauthenticated caller before the handler runs.
|
|
14
|
+
* - **keeper resolves own actor → 200** — the populated round trip: the
|
|
15
|
+
* returned row carries the keeper's `id` + `username`, and **no**
|
|
16
|
+
* `account_id` / `email` / timestamp / role field (the wire shape's
|
|
17
|
+
* deliberate info-leak posture). This is the assertion that exercises the
|
|
18
|
+
* Rust row→JSON mapping against the TS canonical shape.
|
|
19
|
+
* - **empty `ids` → 400** — the `min(1)` input bound is enforced on both
|
|
20
|
+
* spines (TS Zod, Rust `parse_ids`).
|
|
21
|
+
*
|
|
22
|
+
* Runs both legs via the shared `{setup_test}` protocol: the in-process leg
|
|
23
|
+
* (`auth/actor_lookup_parity.db.test.ts`, plain `gro test`) and the
|
|
24
|
+
* cross-process leg (`cross_backend/actor_lookup.cross.test.ts`, the TS spine
|
|
25
|
+
* binaries + Rust `testing_spine_stub` over real HTTP). `actor_lookup` is
|
|
26
|
+
* mounted on every spine, so the suite is ungated.
|
|
27
|
+
*
|
|
28
|
+
* `$lib`-free by contract (relative specifiers only), like the sibling
|
|
29
|
+
* cross-backend suites.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import { describe, test, assert } from 'vitest';
|
|
34
|
+
import { actor_lookup_action_spec } from '../../auth/actor_lookup_action_specs.js';
|
|
35
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
36
|
+
/** Keys that must never appear on an `actor_lookup` result row. */
|
|
37
|
+
const forbidden_row_keys = ['account_id', 'email', 'created_at', 'updated_at', 'role'];
|
|
38
|
+
/** Build the JSON-RPC envelope body for an `actor_lookup` call. */
|
|
39
|
+
const lookup_envelope = (ids, id) => JSON.stringify({ jsonrpc: '2.0', method: actor_lookup_action_spec.method, params: { ids }, id });
|
|
40
|
+
export const describe_actor_lookup_cross_tests = (options) => {
|
|
41
|
+
const { setup_test } = options;
|
|
42
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
43
|
+
describe('actor_lookup parity', () => {
|
|
44
|
+
test('anonymous → 401 (account-grain auth gate)', async () => {
|
|
45
|
+
const fixture = await setup_test();
|
|
46
|
+
const res = await fixture.fresh_transport()(rpc_path, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'content-type': 'application/json' },
|
|
49
|
+
body: lookup_envelope([fixture.actor.id], 'anon-lookup'),
|
|
50
|
+
});
|
|
51
|
+
assert.strictEqual(res.status, 401, 'unauthenticated actor_lookup must be refused');
|
|
52
|
+
});
|
|
53
|
+
test('keeper resolves own actor → 200 with id + username, no control fields', async () => {
|
|
54
|
+
const fixture = await setup_test();
|
|
55
|
+
const res = await fixture.transport(rpc_path, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
58
|
+
body: lookup_envelope([fixture.actor.id], 'keeper-lookup'),
|
|
59
|
+
});
|
|
60
|
+
assert.strictEqual(res.status, 200, 'authenticated actor_lookup must succeed');
|
|
61
|
+
const body = (await res.json());
|
|
62
|
+
const actors = body.result?.actors;
|
|
63
|
+
assert(Array.isArray(actors), 'response carries an actors array');
|
|
64
|
+
assert.strictEqual(actors.length, 1, 'the keeper actor resolves to exactly one row');
|
|
65
|
+
const row = actors[0];
|
|
66
|
+
assert(row !== undefined, 'the resolved row is present');
|
|
67
|
+
assert.strictEqual(row.id, fixture.actor.id, 'resolved row id matches the requested actor');
|
|
68
|
+
assert.strictEqual(row.username, fixture.account.username, 'resolved row carries the keeper username');
|
|
69
|
+
for (const key of forbidden_row_keys) {
|
|
70
|
+
assert(!(key in row), `actor_lookup row must not leak '${key}'`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
test('empty ids → 400 (min(1) input bound)', async () => {
|
|
74
|
+
const fixture = await setup_test();
|
|
75
|
+
const res = await fixture.transport(rpc_path, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
78
|
+
body: lookup_envelope([], 'empty-lookup'),
|
|
79
|
+
});
|
|
80
|
+
assert.strictEqual(res.status, 400, 'empty ids must fail input validation');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { CellCrossTestOptions } from './cell_cross_helpers.js';
|
|
3
|
+
/** Options for the actor-search parity suite (shares the cell/origin shape). */
|
|
4
|
+
export type ActorSearchCrossTestOptions = CellCrossTestOptions;
|
|
5
|
+
export declare const describe_actor_search_cross_tests: (options: ActorSearchCrossTestOptions) => void;
|
|
6
|
+
//# sourceMappingURL=actor_search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor_search.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/actor_search.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2C9B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AAGlE,gFAAgF;AAChF,MAAM,MAAM,2BAA2B,GAAG,oBAAoB,CAAC;AAS/D,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IAyDxF,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for `actor_search`.
|
|
4
|
+
*
|
|
5
|
+
* `actor_search` is an opt-in case-insensitive prefix search over
|
|
6
|
+
* `actor.name` (`{query, scope_ids?, limit?} → {actors: [{id, username,
|
|
7
|
+
* display_name?}]}`), not folded into the standard bundle. Like
|
|
8
|
+
* `actor_lookup` / cells, it's live-mounted on the spine RPC path but kept
|
|
9
|
+
* off the declared surface, so this dedicated suite is its validator. The
|
|
10
|
+
* security property under test is the **empty-`scope_ids` admin gate**:
|
|
11
|
+
*
|
|
12
|
+
* - **anonymous → 401** — the account-grain auth gate refuses an
|
|
13
|
+
* unauthenticated caller before the handler runs.
|
|
14
|
+
* - **non-admin + no `scope_ids` → 400** `actor_search_scope_required` — an
|
|
15
|
+
* unbounded global search is admin-only; a non-admin must scope the query.
|
|
16
|
+
* This is the core security assertion, exercised against each impl's real
|
|
17
|
+
* auth resolution.
|
|
18
|
+
* - **non-admin + `scope_ids` → 200** — passing a scope bypasses the admin
|
|
19
|
+
* requirement (results are filtered to actors holding active role_grants on
|
|
20
|
+
* those scopes); an unheld scope simply yields an empty result, not a
|
|
21
|
+
* rejection — proving the gate keys on `scope_ids` presence, not identity.
|
|
22
|
+
* - **admin + no `scope_ids` → 200** — the admin path reaches the unbounded
|
|
23
|
+
* search.
|
|
24
|
+
*
|
|
25
|
+
* Cites `security.md` §Authorization (the `actor_search` scope gate).
|
|
26
|
+
*
|
|
27
|
+
* Runs both legs via the shared `{setup_test}` protocol: in-process
|
|
28
|
+
* (`auth/actor_search_parity.db.test.ts`) + cross-process
|
|
29
|
+
* (`cross_backend/actor_search.cross.test.ts`, TS spine binaries + Rust
|
|
30
|
+
* `testing_spine_stub`). Mounted on every spine, so the suite is ungated.
|
|
31
|
+
*
|
|
32
|
+
* `$lib`-free by contract (relative specifiers only).
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { describe, test, assert } from 'vitest';
|
|
37
|
+
import { actor_search_action_spec, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, } from '../../auth/actor_search_action_specs.js';
|
|
38
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
39
|
+
/** Nil UUID — an unheld scope id (no actor holds a role_grant on it). */
|
|
40
|
+
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
41
|
+
/** Build the JSON-RPC envelope body for an `actor_search` call. */
|
|
42
|
+
const search_envelope = (params, id) => JSON.stringify({ jsonrpc: '2.0', method: actor_search_action_spec.method, params, id });
|
|
43
|
+
export const describe_actor_search_cross_tests = (options) => {
|
|
44
|
+
const { setup_test } = options;
|
|
45
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
46
|
+
describe('actor_search parity', () => {
|
|
47
|
+
test('anonymous → 401 (account-grain auth gate)', async () => {
|
|
48
|
+
const fixture = await setup_test();
|
|
49
|
+
const res = await fixture.fresh_transport()(rpc_path, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: search_envelope({ query: 'a' }, 'anon-search'),
|
|
53
|
+
});
|
|
54
|
+
assert.strictEqual(res.status, 401, 'unauthenticated actor_search must be refused');
|
|
55
|
+
});
|
|
56
|
+
test('non-admin + no scope_ids → 400 actor_search_scope_required', async () => {
|
|
57
|
+
const fixture = await setup_test();
|
|
58
|
+
const account = await fixture.create_account();
|
|
59
|
+
const res = await fixture.fresh_transport()(rpc_path, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
|
|
62
|
+
body: search_envelope({ query: 'a' }, 'nonadmin-noscope'),
|
|
63
|
+
});
|
|
64
|
+
assert.strictEqual(res.status, 400, 'non-admin unbounded search must be rejected');
|
|
65
|
+
const body = (await res.json());
|
|
66
|
+
assert.strictEqual(body.error?.data?.reason, ERROR_ACTOR_SEARCH_SCOPE_REQUIRED, 'rejection carries the scope-required reason');
|
|
67
|
+
});
|
|
68
|
+
test('non-admin + scope_ids → 200 (scope bypasses admin gate)', async () => {
|
|
69
|
+
const fixture = await setup_test();
|
|
70
|
+
const account = await fixture.create_account();
|
|
71
|
+
const res = await fixture.fresh_transport()(rpc_path, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { ...account.create_session_headers(), 'content-type': 'application/json' },
|
|
74
|
+
body: search_envelope({ query: 'a', scope_ids: [NIL_UUID] }, 'nonadmin-scope'),
|
|
75
|
+
});
|
|
76
|
+
assert.strictEqual(res.status, 200, 'non-admin with a scope filter is allowed');
|
|
77
|
+
const body = (await res.json());
|
|
78
|
+
assert(Array.isArray(body.result?.actors), 'response carries an actors array');
|
|
79
|
+
});
|
|
80
|
+
test('admin + no scope_ids → 200 (admin reaches unbounded search)', async () => {
|
|
81
|
+
const fixture = await setup_test();
|
|
82
|
+
const res = await fixture.transport(rpc_path, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
85
|
+
body: search_envelope({ query: 'a' }, 'admin-noscope'),
|
|
86
|
+
});
|
|
87
|
+
assert.strictEqual(res.status, 200, 'admin unbounded search is allowed');
|
|
88
|
+
const body = (await res.json());
|
|
89
|
+
assert(Array.isArray(body.result?.actors), 'response carries an actors array');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { CellCrossTestOptions } from './cell_cross_helpers.js';
|
|
3
|
+
/** Options for the app-settings effect suite (shares the cell/origin shape). */
|
|
4
|
+
export type AppSettingsCrossTestOptions = CellCrossTestOptions;
|
|
5
|
+
export declare const describe_app_settings_cross_tests: (options: AppSettingsCrossTestOptions) => void;
|
|
6
|
+
//# sourceMappingURL=app_settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app_settings.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/app_settings.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,yBAAyB,CAAC;AAGlE,gFAAgF;AAChF,MAAM,MAAM,2BAA2B,GAAG,oBAAoB,CAAC;AAiB/D,eAAO,MAAM,iCAAiC,GAAI,SAAS,2BAA2B,KAAG,IA8DxF,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend effect suite for the `open_signup` app setting.
|
|
4
|
+
*
|
|
5
|
+
* The declarative conformance table pins the admin gate on
|
|
6
|
+
* `app_settings_get` / `app_settings_update` (401 / 403 / 200). This suite
|
|
7
|
+
* pins the **behavioral effect** of the toggle end to end: an admin flips
|
|
8
|
+
* `open_signup` via `app_settings_update`, and a subsequent anonymous
|
|
9
|
+
* `POST /signup` observes the new value.
|
|
10
|
+
*
|
|
11
|
+
* - **toggle on → anonymous signup without an invite succeeds (200)** — with
|
|
12
|
+
* `open_signup: true`, the invite gate is skipped.
|
|
13
|
+
* - **toggle off → anonymous signup is refused (403 `no_matching_invite`)** —
|
|
14
|
+
* flipping it back restores the invite requirement, proving the gate keys
|
|
15
|
+
* on the live value rather than a one-time read.
|
|
16
|
+
*
|
|
17
|
+
* The signup handler reads the toggle fresh from the database on every
|
|
18
|
+
* request, so the admin's write is visible to the next signup. This suite
|
|
19
|
+
* runs in a single process, so it validates the read-through *mechanism* —
|
|
20
|
+
* not multi-process consistency (which the fresh-read shape provides by
|
|
21
|
+
* construction but no single-binary test can observe).
|
|
22
|
+
*
|
|
23
|
+
* Cites `security.md` §Signup. Runs both legs via the shared `{setup_test}`
|
|
24
|
+
* protocol: in-process (`auth/app_settings_parity.db.test.ts`) +
|
|
25
|
+
* cross-process (`cross_backend/app_settings.cross.test.ts`, TS spine
|
|
26
|
+
* binaries + Rust `testing_spine_stub`). Mounted on every spine, so the
|
|
27
|
+
* suite is ungated.
|
|
28
|
+
*
|
|
29
|
+
* `$lib`-free by contract (relative specifiers only).
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import { describe, test, assert } from 'vitest';
|
|
34
|
+
import { app_settings_update_action_spec } from '../../auth/admin_action_specs.js';
|
|
35
|
+
import { ERROR_NO_MATCHING_INVITE } from '../../http/error_schemas.js';
|
|
36
|
+
import { SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
37
|
+
/** REST signup path on the spine surface (`/api/account` prefix + `/signup`). */
|
|
38
|
+
const SIGNUP_PATH = '/api/account/signup';
|
|
39
|
+
/** A password that satisfies the creation-strength schema (min 12). */
|
|
40
|
+
const SIGNUP_PASSWORD = 'securepassword123';
|
|
41
|
+
/** Build the JSON-RPC envelope for an `app_settings_update` call. */
|
|
42
|
+
const update_envelope = (open_signup, id) => JSON.stringify({
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
method: app_settings_update_action_spec.method,
|
|
45
|
+
params: { open_signup },
|
|
46
|
+
id,
|
|
47
|
+
});
|
|
48
|
+
export const describe_app_settings_cross_tests = (options) => {
|
|
49
|
+
const { setup_test } = options;
|
|
50
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
51
|
+
describe('app_settings open_signup effect', () => {
|
|
52
|
+
test('admin enables open_signup → anonymous signup without invite succeeds', async () => {
|
|
53
|
+
const fixture = await setup_test();
|
|
54
|
+
const enable = await fixture.transport(rpc_path, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
57
|
+
body: update_envelope(true, 'enable-open-signup'),
|
|
58
|
+
});
|
|
59
|
+
assert.strictEqual(enable.status, 200, 'admin app_settings_update must succeed');
|
|
60
|
+
const res = await fixture.fresh_transport()(SIGNUP_PATH, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ username: 'open_signup_user', password: SIGNUP_PASSWORD }),
|
|
64
|
+
});
|
|
65
|
+
assert.strictEqual(res.status, 200, 'open signup must admit an anonymous account');
|
|
66
|
+
const body = (await res.json());
|
|
67
|
+
assert.strictEqual(body.ok, true, 'signup response reports success');
|
|
68
|
+
});
|
|
69
|
+
test('admin disables open_signup → anonymous signup is refused (no_matching_invite)', async () => {
|
|
70
|
+
const fixture = await setup_test();
|
|
71
|
+
// Enable then disable so the assertion proves the *flip back* takes
|
|
72
|
+
// effect, not merely the closed default.
|
|
73
|
+
const enable = await fixture.transport(rpc_path, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
76
|
+
body: update_envelope(true, 'enable-before-disable'),
|
|
77
|
+
});
|
|
78
|
+
assert.strictEqual(enable.status, 200, 'admin app_settings_update (enable) must succeed');
|
|
79
|
+
const disable = await fixture.transport(rpc_path, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { ...fixture.create_session_headers(), 'content-type': 'application/json' },
|
|
82
|
+
body: update_envelope(false, 'disable-open-signup'),
|
|
83
|
+
});
|
|
84
|
+
assert.strictEqual(disable.status, 200, 'admin app_settings_update (disable) must succeed');
|
|
85
|
+
const res = await fixture.fresh_transport()(SIGNUP_PATH, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'content-type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({ username: 'closed_signup_user', password: SIGNUP_PASSWORD }),
|
|
89
|
+
});
|
|
90
|
+
assert.strictEqual(res.status, 403, 'closed signup must refuse a no-invite anonymous account');
|
|
91
|
+
const body = (await res.json());
|
|
92
|
+
assert.strictEqual(body.error, ERROR_NO_MATCHING_INVITE, 'rejection carries the no-matching-invite reason');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -106,7 +106,7 @@ export interface BackendConfig {
|
|
|
106
106
|
/**
|
|
107
107
|
* Capabilities this backend supports — drives `test_if(capabilities.X, ...)`
|
|
108
108
|
* gating in suite bodies. See `capabilities.ts` for the vocabulary and
|
|
109
|
-
* existing flags.
|
|
109
|
+
* existing flags.
|
|
110
110
|
*/
|
|
111
111
|
readonly capabilities: BackendCapabilities;
|
|
112
112
|
}
|
|
@@ -36,14 +36,36 @@ export interface BackendCapabilities {
|
|
|
36
36
|
*/
|
|
37
37
|
readonly sse: boolean;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* Cell CRUD verbs (`cell_create` / `cell_get` / `cell_update` /
|
|
40
|
+
* `cell_delete` / `cell_list`) are live-mounted on the backend's RPC
|
|
41
|
+
* path and its DB carries the `fuz_cell` migration namespace. Gates the
|
|
42
|
+
* dedicated `describe_cell_crud_cross_tests` suite. Like `ws` / `sse`,
|
|
43
|
+
* cells stay off the standard declared surface — only this flag opts a
|
|
44
|
+
* backend into the cell parity coverage.
|
|
45
45
|
*/
|
|
46
|
-
readonly
|
|
46
|
+
readonly cell_crud: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The relation / ACL / audit cell verbs beyond plain CRUD
|
|
49
|
+
* (`cell_grant_*` / `cell_field_*` / `cell_item_*` / `cell_clone` /
|
|
50
|
+
* `cell_audit_list`) are live-mounted on the backend's RPC path. Gates
|
|
51
|
+
* the dedicated `describe_cell_relations_cross_tests` suite — grant
|
|
52
|
+
* lifecycle, field / item bidirectional relations, clone shallow + deep,
|
|
53
|
+
* manage-tier audit gating, and the now-reachable
|
|
54
|
+
* `cell_visibility_manage_only` 403 (editor-grant principal). Like
|
|
55
|
+
* `cell_crud`, these stay off the standard declared surface; a backend
|
|
56
|
+
* mounting only plain CRUD declares `cell_crud: true, cell_relations: false`.
|
|
57
|
+
*/
|
|
58
|
+
readonly cell_relations: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* The account-lifecycle admin verbs (`account_delete` soft-delete,
|
|
61
|
+
* `account_undelete` reactivation, `account_purge` keeper hard-delete)
|
|
62
|
+
* are live-mounted on the backend's RPC path. Gates the dedicated
|
|
63
|
+
* `describe_account_lifecycle_cross_tests` suite. Like cells, these
|
|
64
|
+
* destructive/stateful verbs stay off the standard declared surface (the
|
|
65
|
+
* generic round-trip can't drive them — they delete the subject), so
|
|
66
|
+
* this flag opts a backend into the lifecycle parity coverage.
|
|
67
|
+
*/
|
|
68
|
+
readonly account_lifecycle: boolean;
|
|
47
69
|
}
|
|
48
70
|
/**
|
|
49
71
|
* Capability declarations for the in-process Hono transport. Every flag
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;OAQG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBASpC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
|
|
@@ -26,7 +26,9 @@ export const in_process_capabilities = Object.freeze({
|
|
|
26
26
|
login_rate_limit: true,
|
|
27
27
|
ws: true,
|
|
28
28
|
sse: true,
|
|
29
|
-
|
|
29
|
+
cell_crud: true,
|
|
30
|
+
cell_relations: true,
|
|
31
|
+
account_lifecycle: true,
|
|
30
32
|
});
|
|
31
33
|
/**
|
|
32
34
|
* Conditional `test()` wrapper — registers a vitest case only when
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { FetchTransport } from '../transports/fetch_transport.js';
|
|
4
|
+
import type { BackendCapabilities } from './capabilities.js';
|
|
5
|
+
import type { SetupTest } from './setup.js';
|
|
6
|
+
/** Shared options for the cell cross-backend parity suites. */
|
|
7
|
+
export interface CellCrossTestOptions {
|
|
8
|
+
/** Per-test fixture-producing function (fresh keeper + db per call). */
|
|
9
|
+
readonly setup_test: SetupTest;
|
|
10
|
+
/** Backend capability declarations — each suite gates on its own flag. */
|
|
11
|
+
readonly capabilities: BackendCapabilities;
|
|
12
|
+
/** RPC endpoint path the cell verbs are mounted on. Default `/api/rpc`. */
|
|
13
|
+
readonly rpc_path?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Minimal JSON-RPC envelope shape the suites read off responses. */
|
|
16
|
+
export interface RpcResult {
|
|
17
|
+
readonly ok: boolean;
|
|
18
|
+
readonly result?: unknown;
|
|
19
|
+
readonly error?: {
|
|
20
|
+
readonly code: number;
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly data?: unknown;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* POST a JSON-RPC call over a cross-process `FetchTransport` with the given
|
|
27
|
+
* auth headers. Distinct from `rpc_helpers.ts`'s `app`-based `rpc_call`: this
|
|
28
|
+
* variant drives the cookie-jar `FetchTransport` the cross-backend harness
|
|
29
|
+
* spawns against, and returns the slim `RpcResult` the cell suites read.
|
|
30
|
+
*/
|
|
31
|
+
export declare const cross_rpc_call: (transport: FetchTransport, path: string, method: string, params: unknown, headers: Record<string, string>) => Promise<RpcResult>;
|
|
32
|
+
/** Pull `error.data.reason` off an RPC error envelope (undefined if absent). */
|
|
33
|
+
export declare const error_reason: (r: RpcResult) => unknown;
|
|
34
|
+
/**
|
|
35
|
+
* Assert the call succeeded and the `result` matches the verb's declared
|
|
36
|
+
* output schema — the wire-shape parity gate. Returns the parsed output.
|
|
37
|
+
*/
|
|
38
|
+
export declare const expect_output: <T>(r: RpcResult, schema: z.ZodType<T>) => T;
|
|
39
|
+
//# sourceMappingURL=cell_cross_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_cross_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_cross_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmB9B,OAAO,KAAK,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAG3B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAE1C,+DAA+D;AAC/D,MAAM,WAAW,oBAAoB;IACpC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAC,CAAC;CAC5F;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAC1B,WAAW,cAAc,EACzB,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,QAAQ,OAAO,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7B,OAAO,CAAC,SAAS,CAMnB,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,YAAY,GAAI,GAAG,SAAS,KAAG,OAG/B,CAAC;AAEd;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,GAAG,SAAS,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAG,CAQrE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared call-site primitives for the cell cross-backend parity suites
|
|
4
|
+
* (`cell_crud.ts` + `cell_relations.ts`).
|
|
5
|
+
*
|
|
6
|
+
* The cell verbs are stateful and authz-shaped, so both suites POST raw
|
|
7
|
+
* JSON-RPC envelopes (threading ids + auth headers across calls) and parse
|
|
8
|
+
* every success `result` against the verb's declared Zod **output** schema —
|
|
9
|
+
* the wire-shape parity gate. A TS↔Rust envelope drift, not just a payload
|
|
10
|
+
* field drift, fails the assertion.
|
|
11
|
+
*
|
|
12
|
+
* `$lib`-free by contract (relative specifiers only) so the suites can be
|
|
13
|
+
* imported from the spawnable cross-process test files.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import { assert } from 'vitest';
|
|
18
|
+
import { create_rpc_post_init } from '../rpc_helpers.js';
|
|
19
|
+
/**
|
|
20
|
+
* POST a JSON-RPC call over a cross-process `FetchTransport` with the given
|
|
21
|
+
* auth headers. Distinct from `rpc_helpers.ts`'s `app`-based `rpc_call`: this
|
|
22
|
+
* variant drives the cookie-jar `FetchTransport` the cross-backend harness
|
|
23
|
+
* spawns against, and returns the slim `RpcResult` the cell suites read.
|
|
24
|
+
*/
|
|
25
|
+
export const cross_rpc_call = async (transport, path, method, params, headers) => {
|
|
26
|
+
const init = create_rpc_post_init(method, params);
|
|
27
|
+
Object.assign(init.headers, headers);
|
|
28
|
+
const res = await transport(path, init);
|
|
29
|
+
const body = (await res.json());
|
|
30
|
+
return { ok: res.ok, result: body.result, error: body.error };
|
|
31
|
+
};
|
|
32
|
+
/** Pull `error.data.reason` off an RPC error envelope (undefined if absent). */
|
|
33
|
+
export const error_reason = (r) => r.error && typeof r.error.data === 'object' && r.error.data !== null
|
|
34
|
+
? r.error.data.reason
|
|
35
|
+
: undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Assert the call succeeded and the `result` matches the verb's declared
|
|
38
|
+
* output schema — the wire-shape parity gate. Returns the parsed output.
|
|
39
|
+
*/
|
|
40
|
+
export const expect_output = (r, schema) => {
|
|
41
|
+
assert.ok(r.ok, `expected success, got ${JSON.stringify(r.error)}`);
|
|
42
|
+
const parsed = schema.safeParse(r.result);
|
|
43
|
+
assert.ok(parsed.success, `result does not match output schema: ${parsed.success ? '' : JSON.stringify(parsed.error.issues)} (got ${JSON.stringify(r.result)})`);
|
|
44
|
+
return parsed.data;
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqD9B,OAAO,EAIN,KAAK,oBAAoB,EACzB,MAAM,yBAAyB,CAAC;AAGjC,eAAO,MAAM,8BAA8B,GAAI,SAAS,oBAAoB,KAAG,IAwS9E,CAAC"}
|