@fuzdev/fuz_app 0.68.0 → 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/admin_action_specs.d.ts +2 -3
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +2 -3
- 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 +28 -36
- 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/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/testing/CLAUDE.md +85 -2
- package/dist/testing/app_server.d.ts +34 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +31 -6
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +69 -1
- 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 +0 -9
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +0 -1
- 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/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 +0 -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 +34 -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/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/ui/AdminAccounts.svelte +74 -83
- 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 +12 -19
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +10 -24
- 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_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 +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cross-backend parity suite for **role-shaped** `cell_grant`s.
|
|
4
|
+
*
|
|
5
|
+
* The cell CRUD / relations suites exercise only actor-shaped grant
|
|
6
|
+
* principals. This suite covers the role-shaped path and its closed-registry
|
|
7
|
+
* gate — the security-correctness property that the Rust spine previously
|
|
8
|
+
* lacked (it created inert grant rows for any role string). Both spines now
|
|
9
|
+
* validate the role against a closed registry at create.
|
|
10
|
+
*
|
|
11
|
+
* - **role grant admits a holder; excludes a non-holder** — an owner grants
|
|
12
|
+
* `{role}` on a private cell; an account holding that role can `cell_get`
|
|
13
|
+
* it (200), an account without it gets the IDOR-mask 404.
|
|
14
|
+
* - **unknown role rejected at create (security-correctness)** — granting a
|
|
15
|
+
* role outside the registry is `invalid_params` / `cell_grant_unknown_role`,
|
|
16
|
+
* not a silent inert row.
|
|
17
|
+
* - **editor-level role grant admits edit** — a holder of an `editor`-level
|
|
18
|
+
* role grant can `cell_update` the cell's content.
|
|
19
|
+
*
|
|
20
|
+
* The holder is seeded via `extra_accounts` under `CELL_ROLE_HOLDER_USERNAME`
|
|
21
|
+
* holding `CELL_EDITOR_ROLE` (the role has no grant path, so it can't be
|
|
22
|
+
* offered — the bootstrap-cradle seed is the only path). Both legs configure
|
|
23
|
+
* that seed and register `CELL_EDITOR_ROLE` in their role registry; the Rust
|
|
24
|
+
* `testing_spine_stub` mirrors the same membership in its `known_roles`.
|
|
25
|
+
*
|
|
26
|
+
* Cites `security.md` §Authorization (role-shaped cell-grant validation).
|
|
27
|
+
* Runs both legs via the shared `{setup_test}` protocol: in-process
|
|
28
|
+
* (`auth/cell_grant_role_parity.db.test.ts`) + cross-process
|
|
29
|
+
* (`cross_backend/cell_grant_role.cross.test.ts`). Gated on
|
|
30
|
+
* `capabilities.cell_relations` (true on every spine, so it never skips).
|
|
31
|
+
*
|
|
32
|
+
* `$lib`-free by contract (relative specifiers only).
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
36
|
+
import { describe, assert } from 'vitest';
|
|
37
|
+
import { CellCreateOutput, CellGetOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
|
|
38
|
+
import { CellGrantCreateOutput, ERROR_CELL_GRANT_UNKNOWN_ROLE, } from '../../auth/cell_grant_action_specs.js';
|
|
39
|
+
import { test_if } from './capabilities.js';
|
|
40
|
+
import { cross_rpc_call, error_reason, expect_output, } from './cell_cross_helpers.js';
|
|
41
|
+
import { SPINE_CELL_EDITOR_ROLE, SPINE_RPC_PATH } from './default_spine_surface.js';
|
|
42
|
+
/** App role the holder is seeded with; matches the spine's registered role. */
|
|
43
|
+
export const CELL_EDITOR_ROLE = SPINE_CELL_EDITOR_ROLE;
|
|
44
|
+
/** Username the fixture seeds (via `extra_accounts`) holding `CELL_EDITOR_ROLE`. */
|
|
45
|
+
export const CELL_ROLE_HOLDER_USERNAME = 'cell_role_holder';
|
|
46
|
+
/** A role string deliberately absent from the registry. */
|
|
47
|
+
const UNREGISTERED_ROLE = 'not_a_registered_role';
|
|
48
|
+
export const describe_cell_grant_role_cross_tests = (options) => {
|
|
49
|
+
const { setup_test, capabilities } = options;
|
|
50
|
+
const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
|
|
51
|
+
describe('cell_grant role-shaped parity', () => {
|
|
52
|
+
test_if(capabilities.cell_relations, 'role grant admits a holder of the role and excludes a non-holder', async () => {
|
|
53
|
+
const fixture = await setup_test();
|
|
54
|
+
const t = fixture.transport;
|
|
55
|
+
const owner = await fixture.create_account({ username: 'cell_role_owner' });
|
|
56
|
+
const owner_h = owner.create_session_headers();
|
|
57
|
+
const holder = fixture.extra_accounts[CELL_ROLE_HOLDER_USERNAME];
|
|
58
|
+
assert.ok(holder, `fixture must seed the ${CELL_ROLE_HOLDER_USERNAME} extra account`);
|
|
59
|
+
const stranger = await fixture.create_account({ username: 'cell_role_stranger' });
|
|
60
|
+
// Owner creates a private cell (default visibility) and grants
|
|
61
|
+
// view access to anyone holding CELL_EDITOR_ROLE.
|
|
62
|
+
const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
|
|
63
|
+
const cell_id = created.cell.id;
|
|
64
|
+
expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'viewer', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
|
|
65
|
+
// Holder of the role is admitted through the role-shaped grant.
|
|
66
|
+
const holder_view = expect_output(await cross_rpc_call(t, rpc_path, 'cell_get', { id: cell_id }, holder.create_session_headers()), CellGetOutput);
|
|
67
|
+
assert.strictEqual(holder_view.cell.id, cell_id, 'holder sees the granted cell');
|
|
68
|
+
// A non-holder sees the IDOR-mask 404 — the grant keys on the role,
|
|
69
|
+
// not mere authentication.
|
|
70
|
+
const stranger_view = await cross_rpc_call(t, rpc_path, 'cell_get', { id: cell_id }, stranger.create_session_headers());
|
|
71
|
+
assert.ok(!stranger_view.ok, 'non-holder must not see the cell');
|
|
72
|
+
assert.strictEqual(error_reason(stranger_view), 'cell_not_found');
|
|
73
|
+
});
|
|
74
|
+
test_if(capabilities.cell_relations, 'role-shaped grant for an unregistered role is rejected at create', async () => {
|
|
75
|
+
const fixture = await setup_test();
|
|
76
|
+
const t = fixture.transport;
|
|
77
|
+
const owner = await fixture.create_account({ username: 'cell_unknown_role_owner' });
|
|
78
|
+
const owner_h = owner.create_session_headers();
|
|
79
|
+
const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
|
|
80
|
+
const denied = await cross_rpc_call(t, rpc_path, 'cell_grant_create', {
|
|
81
|
+
cell_id: created.cell.id,
|
|
82
|
+
level: 'viewer',
|
|
83
|
+
principal: { kind: 'role', role: UNREGISTERED_ROLE },
|
|
84
|
+
}, owner_h);
|
|
85
|
+
assert.ok(!denied.ok, 'granting an unregistered role must fail');
|
|
86
|
+
assert.strictEqual(error_reason(denied), ERROR_CELL_GRANT_UNKNOWN_ROLE);
|
|
87
|
+
});
|
|
88
|
+
test_if(capabilities.cell_relations, 'editor-level role grant admits content edit', async () => {
|
|
89
|
+
const fixture = await setup_test();
|
|
90
|
+
const t = fixture.transport;
|
|
91
|
+
const owner = await fixture.create_account({ username: 'cell_role_edit_owner' });
|
|
92
|
+
const owner_h = owner.create_session_headers();
|
|
93
|
+
const holder = fixture.extra_accounts[CELL_ROLE_HOLDER_USERNAME];
|
|
94
|
+
assert.ok(holder, `fixture must seed the ${CELL_ROLE_HOLDER_USERNAME} extra account`);
|
|
95
|
+
const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { data: { kind: 'note' } }, owner_h), CellCreateOutput);
|
|
96
|
+
const cell_id = created.cell.id;
|
|
97
|
+
expect_output(await cross_rpc_call(t, rpc_path, 'cell_grant_create', { cell_id, level: 'editor', principal: { kind: 'role', role: CELL_EDITOR_ROLE } }, owner_h), CellGrantCreateOutput);
|
|
98
|
+
const edited = expect_output(await cross_rpc_call(t, rpc_path, 'cell_update', { cell_id, data: { kind: 'note', label: 'by role editor' } }, holder.create_session_headers()), CellUpdateOutput);
|
|
99
|
+
assert.strictEqual(edited.cell.updated_by, holder.actor.id, 'edit attributed to the holder');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declarative conformance-case schema for the cross-backend behavioral +
|
|
4
|
+
* security suite.
|
|
5
|
+
*
|
|
6
|
+
* A conformance case is a single request → expected-response assertion,
|
|
7
|
+
* carried as **data**. The case references a `method` (an RPC method name
|
|
8
|
+
* or a REST auth-route suffix); the runner
|
|
9
|
+
* (`describe_conformance_table_tests`) resolves the `input` / `output`
|
|
10
|
+
* Zod schemas from the live action-spec registry / `RouteSpec` — the case
|
|
11
|
+
* never carries a schema. This is the opinionated behavioral/security
|
|
12
|
+
* layer on top of the spec-derived auto-enumeration
|
|
13
|
+
* (`describe_rpc_round_trip_tests` / `describe_rpc_attack_surface_tests`):
|
|
14
|
+
* the same case definition runs in-process (fast, every `gro test`) and
|
|
15
|
+
* cross-process (the conformance gate) against each impl's real auth
|
|
16
|
+
* resolution.
|
|
17
|
+
*
|
|
18
|
+
* The table is for single-request matrices (credential-type ceiling,
|
|
19
|
+
* privilege gates, IDOR masks, enumeration-equivalence, validation).
|
|
20
|
+
* Multi-step flows stay imperative in their own `describe_*` suites,
|
|
21
|
+
* sharing assertion primitives — there is deliberately no declarative
|
|
22
|
+
* setup DSL.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
/**
|
|
28
|
+
* Closed enum of fixture-provisioned principals a case runs `as`. Each
|
|
29
|
+
* value maps to a `TestFixture` accessor (or a seeded `extra_accounts`
|
|
30
|
+
* entry) in the runner's `resolve_principal` — there is **no** inline
|
|
31
|
+
* credential minting in a case (that would be the setup-DSL trap).
|
|
32
|
+
*
|
|
33
|
+
* - `keeper` — the per-test bootstrapped keeper (holds `ROLE_KEEPER` +
|
|
34
|
+
* `ROLE_ADMIN`), session credential.
|
|
35
|
+
* - `daemon` — the keeper authenticated via the daemon-token header.
|
|
36
|
+
* - `token` — the keeper authenticated via a bearer api-token (non-browser
|
|
37
|
+
* context; the runner suppresses `Origin` so the token isn't discarded).
|
|
38
|
+
* - `anonymous` — no credential, fresh cookie jar.
|
|
39
|
+
* - `fresh_non_admin` — a freshly minted account with no roles, session
|
|
40
|
+
* credential (via the production invite → signup → login flow).
|
|
41
|
+
* - `role_holder` — a seeded `extra_accounts` principal holding a specific
|
|
42
|
+
* role; the runner reads it by the username named in
|
|
43
|
+
* `ConformanceTableOptions.principals.role_holder`.
|
|
44
|
+
* - `wrong_role` — a seeded `extra_accounts` principal holding a role
|
|
45
|
+
* other than the one a route requires; named via
|
|
46
|
+
* `ConformanceTableOptions.principals.wrong_role`.
|
|
47
|
+
* - `expired_session` — the keeper account presented via an *expired
|
|
48
|
+
* server-side session* cookie (minted by `fixture.mint_expired_session()`:
|
|
49
|
+
* a backdated `auth_session` row behind a still-valid signed cookie
|
|
50
|
+
* payload, so the authoritative DB-row expiry gate is what refuses it).
|
|
51
|
+
*/
|
|
52
|
+
export declare const ConformancePrincipal: z.ZodEnum<{
|
|
53
|
+
keeper: "keeper";
|
|
54
|
+
token: "token";
|
|
55
|
+
daemon: "daemon";
|
|
56
|
+
anonymous: "anonymous";
|
|
57
|
+
fresh_non_admin: "fresh_non_admin";
|
|
58
|
+
role_holder: "role_holder";
|
|
59
|
+
wrong_role: "wrong_role";
|
|
60
|
+
expired_session: "expired_session";
|
|
61
|
+
}>;
|
|
62
|
+
export type ConformancePrincipal = z.infer<typeof ConformancePrincipal>;
|
|
63
|
+
/** The request a conformance case issues. */
|
|
64
|
+
export declare const ConformanceCaseRequest: z.ZodObject<{
|
|
65
|
+
method: z.ZodString;
|
|
66
|
+
params: z.ZodOptional<z.ZodUnknown>;
|
|
67
|
+
as: z.ZodEnum<{
|
|
68
|
+
keeper: "keeper";
|
|
69
|
+
token: "token";
|
|
70
|
+
daemon: "daemon";
|
|
71
|
+
anonymous: "anonymous";
|
|
72
|
+
fresh_non_admin: "fresh_non_admin";
|
|
73
|
+
role_holder: "role_holder";
|
|
74
|
+
wrong_role: "wrong_role";
|
|
75
|
+
expired_session: "expired_session";
|
|
76
|
+
}>;
|
|
77
|
+
verb: z.ZodOptional<z.ZodEnum<{
|
|
78
|
+
GET: "GET";
|
|
79
|
+
POST: "POST";
|
|
80
|
+
}>>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
export type ConformanceCaseRequest = z.infer<typeof ConformanceCaseRequest>;
|
|
83
|
+
/** The expected response shape a conformance case asserts. */
|
|
84
|
+
export declare const ConformanceCaseExpectation: z.ZodObject<{
|
|
85
|
+
status: z.ZodNumber;
|
|
86
|
+
error_reason: z.ZodOptional<z.ZodString>;
|
|
87
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
export type ConformanceCaseExpectation = z.infer<typeof ConformanceCaseExpectation>;
|
|
90
|
+
/**
|
|
91
|
+
* Marks a case as a deferred-by-design gap. The runner routes it through
|
|
92
|
+
* `xfail_until` instead of a normal `test` — visible (distinct from pass)
|
|
93
|
+
* and self-cleaning (flips red when the impl starts passing, forcing the
|
|
94
|
+
* marker's removal). Use for declared gaps (e.g. facts), never for
|
|
95
|
+
* in-scope gaps (those fail loud as a red `test`).
|
|
96
|
+
*/
|
|
97
|
+
export declare const ConformanceCaseXfail: z.ZodObject<{
|
|
98
|
+
tracking_id: z.ZodString;
|
|
99
|
+
reason: z.ZodString;
|
|
100
|
+
}, z.core.$strict>;
|
|
101
|
+
export type ConformanceCaseXfail = z.infer<typeof ConformanceCaseXfail>;
|
|
102
|
+
/**
|
|
103
|
+
* A single conformance case. `name` is the assertion; the optional
|
|
104
|
+
* free-text `note` is printed in the test label / failure output. A
|
|
105
|
+
* security case's `note` should reference a **public** fuz_app doc
|
|
106
|
+
* property (`security.md` / `architecture.md` / module TSDoc), since the
|
|
107
|
+
* table ships in a public package — not an internal planning doc. The note
|
|
108
|
+
* is documentation, not a gate: it stays free-text by design because a
|
|
109
|
+
* non-empty-string check never catches a *wrong* citation — the citation
|
|
110
|
+
* is verified in review.
|
|
111
|
+
*/
|
|
112
|
+
export declare const ConformanceCase: z.ZodObject<{
|
|
113
|
+
name: z.ZodString;
|
|
114
|
+
request: z.ZodObject<{
|
|
115
|
+
method: z.ZodString;
|
|
116
|
+
params: z.ZodOptional<z.ZodUnknown>;
|
|
117
|
+
as: z.ZodEnum<{
|
|
118
|
+
keeper: "keeper";
|
|
119
|
+
token: "token";
|
|
120
|
+
daemon: "daemon";
|
|
121
|
+
anonymous: "anonymous";
|
|
122
|
+
fresh_non_admin: "fresh_non_admin";
|
|
123
|
+
role_holder: "role_holder";
|
|
124
|
+
wrong_role: "wrong_role";
|
|
125
|
+
expired_session: "expired_session";
|
|
126
|
+
}>;
|
|
127
|
+
verb: z.ZodOptional<z.ZodEnum<{
|
|
128
|
+
GET: "GET";
|
|
129
|
+
POST: "POST";
|
|
130
|
+
}>>;
|
|
131
|
+
}, z.core.$strict>;
|
|
132
|
+
expect: z.ZodObject<{
|
|
133
|
+
status: z.ZodNumber;
|
|
134
|
+
error_reason: z.ZodOptional<z.ZodString>;
|
|
135
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
136
|
+
}, z.core.$strict>;
|
|
137
|
+
note: z.ZodOptional<z.ZodString>;
|
|
138
|
+
xfail: z.ZodOptional<z.ZodObject<{
|
|
139
|
+
tracking_id: z.ZodString;
|
|
140
|
+
reason: z.ZodString;
|
|
141
|
+
}, z.core.$strict>>;
|
|
142
|
+
}, z.core.$strict>;
|
|
143
|
+
export type ConformanceCase = z.infer<typeof ConformanceCase>;
|
|
144
|
+
//# sourceMappingURL=conformance_case.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance_case.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_case.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;EAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,6CAA6C;AAC7C,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;kBAgBjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B;;;;kBAqBrC,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB;;;kBAK/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAS1B,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Declarative conformance-case schema for the cross-backend behavioral +
|
|
4
|
+
* security suite.
|
|
5
|
+
*
|
|
6
|
+
* A conformance case is a single request → expected-response assertion,
|
|
7
|
+
* carried as **data**. The case references a `method` (an RPC method name
|
|
8
|
+
* or a REST auth-route suffix); the runner
|
|
9
|
+
* (`describe_conformance_table_tests`) resolves the `input` / `output`
|
|
10
|
+
* Zod schemas from the live action-spec registry / `RouteSpec` — the case
|
|
11
|
+
* never carries a schema. This is the opinionated behavioral/security
|
|
12
|
+
* layer on top of the spec-derived auto-enumeration
|
|
13
|
+
* (`describe_rpc_round_trip_tests` / `describe_rpc_attack_surface_tests`):
|
|
14
|
+
* the same case definition runs in-process (fast, every `gro test`) and
|
|
15
|
+
* cross-process (the conformance gate) against each impl's real auth
|
|
16
|
+
* resolution.
|
|
17
|
+
*
|
|
18
|
+
* The table is for single-request matrices (credential-type ceiling,
|
|
19
|
+
* privilege gates, IDOR masks, enumeration-equivalence, validation).
|
|
20
|
+
* Multi-step flows stay imperative in their own `describe_*` suites,
|
|
21
|
+
* sharing assertion primitives — there is deliberately no declarative
|
|
22
|
+
* setup DSL.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
/**
|
|
28
|
+
* Closed enum of fixture-provisioned principals a case runs `as`. Each
|
|
29
|
+
* value maps to a `TestFixture` accessor (or a seeded `extra_accounts`
|
|
30
|
+
* entry) in the runner's `resolve_principal` — there is **no** inline
|
|
31
|
+
* credential minting in a case (that would be the setup-DSL trap).
|
|
32
|
+
*
|
|
33
|
+
* - `keeper` — the per-test bootstrapped keeper (holds `ROLE_KEEPER` +
|
|
34
|
+
* `ROLE_ADMIN`), session credential.
|
|
35
|
+
* - `daemon` — the keeper authenticated via the daemon-token header.
|
|
36
|
+
* - `token` — the keeper authenticated via a bearer api-token (non-browser
|
|
37
|
+
* context; the runner suppresses `Origin` so the token isn't discarded).
|
|
38
|
+
* - `anonymous` — no credential, fresh cookie jar.
|
|
39
|
+
* - `fresh_non_admin` — a freshly minted account with no roles, session
|
|
40
|
+
* credential (via the production invite → signup → login flow).
|
|
41
|
+
* - `role_holder` — a seeded `extra_accounts` principal holding a specific
|
|
42
|
+
* role; the runner reads it by the username named in
|
|
43
|
+
* `ConformanceTableOptions.principals.role_holder`.
|
|
44
|
+
* - `wrong_role` — a seeded `extra_accounts` principal holding a role
|
|
45
|
+
* other than the one a route requires; named via
|
|
46
|
+
* `ConformanceTableOptions.principals.wrong_role`.
|
|
47
|
+
* - `expired_session` — the keeper account presented via an *expired
|
|
48
|
+
* server-side session* cookie (minted by `fixture.mint_expired_session()`:
|
|
49
|
+
* a backdated `auth_session` row behind a still-valid signed cookie
|
|
50
|
+
* payload, so the authoritative DB-row expiry gate is what refuses it).
|
|
51
|
+
*/
|
|
52
|
+
export const ConformancePrincipal = z.enum([
|
|
53
|
+
'keeper',
|
|
54
|
+
'daemon',
|
|
55
|
+
'token',
|
|
56
|
+
'anonymous',
|
|
57
|
+
'fresh_non_admin',
|
|
58
|
+
'role_holder',
|
|
59
|
+
'wrong_role',
|
|
60
|
+
'expired_session',
|
|
61
|
+
]);
|
|
62
|
+
/** The request a conformance case issues. */
|
|
63
|
+
export const ConformanceCaseRequest = z.strictObject({
|
|
64
|
+
method: z.string().meta({
|
|
65
|
+
description: 'RPC method name (e.g. `admin_account_list`) or a REST auth-route suffix ' +
|
|
66
|
+
'(e.g. `/login`). A leading `/` selects the REST branch; otherwise the ' +
|
|
67
|
+
'runner resolves the RPC action from the spec registry.',
|
|
68
|
+
}),
|
|
69
|
+
params: z
|
|
70
|
+
.unknown()
|
|
71
|
+
.optional()
|
|
72
|
+
.meta({ description: 'Request params / body. Omit for nullary methods.' }),
|
|
73
|
+
as: ConformancePrincipal,
|
|
74
|
+
verb: z
|
|
75
|
+
.enum(['POST', 'GET'])
|
|
76
|
+
.optional()
|
|
77
|
+
.meta({ description: 'HTTP verb. Defaults to POST; use GET for `side_effects: false` reads.' }),
|
|
78
|
+
});
|
|
79
|
+
/** The expected response shape a conformance case asserts. */
|
|
80
|
+
export const ConformanceCaseExpectation = z.strictObject({
|
|
81
|
+
status: z.number().int().meta({ description: 'Expected HTTP status code.' }),
|
|
82
|
+
error_reason: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.meta({
|
|
86
|
+
description: 'Expected error reason — pass the IMPORTED `ERROR_*` constant from ' +
|
|
87
|
+
'`http/error_schemas.ts`, never a string literal. Asserted against the RPC ' +
|
|
88
|
+
'`error.data.reason` (when the denial carries one) or the REST flat-body ' +
|
|
89
|
+
'`error` field. The pre-validation 401 carries `data.reason` too; a denial ' +
|
|
90
|
+
'that genuinely omits it falls back to the `status` assertion to pin the class.',
|
|
91
|
+
}),
|
|
92
|
+
fields: z
|
|
93
|
+
.record(z.string(), z.unknown())
|
|
94
|
+
.optional()
|
|
95
|
+
.meta({
|
|
96
|
+
description: 'Specific field-value assertions on the success `result` (2xx) or the error ' +
|
|
97
|
+
'`error.data` (non-2xx). Each key must deep-equal the corresponding response field.',
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Marks a case as a deferred-by-design gap. The runner routes it through
|
|
102
|
+
* `xfail_until` instead of a normal `test` — visible (distinct from pass)
|
|
103
|
+
* and self-cleaning (flips red when the impl starts passing, forcing the
|
|
104
|
+
* marker's removal). Use for declared gaps (e.g. facts), never for
|
|
105
|
+
* in-scope gaps (those fail loud as a red `test`).
|
|
106
|
+
*/
|
|
107
|
+
export const ConformanceCaseXfail = z.strictObject({
|
|
108
|
+
tracking_id: z
|
|
109
|
+
.string()
|
|
110
|
+
.meta({ description: 'Tracking id for the deferred gap (issue id or tracking slug).' }),
|
|
111
|
+
reason: z.string().meta({ description: 'Why this case is deferred-by-design.' }),
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* A single conformance case. `name` is the assertion; the optional
|
|
115
|
+
* free-text `note` is printed in the test label / failure output. A
|
|
116
|
+
* security case's `note` should reference a **public** fuz_app doc
|
|
117
|
+
* property (`security.md` / `architecture.md` / module TSDoc), since the
|
|
118
|
+
* table ships in a public package — not an internal planning doc. The note
|
|
119
|
+
* is documentation, not a gate: it stays free-text by design because a
|
|
120
|
+
* non-empty-string check never catches a *wrong* citation — the citation
|
|
121
|
+
* is verified in review.
|
|
122
|
+
*/
|
|
123
|
+
export const ConformanceCase = z.strictObject({
|
|
124
|
+
name: z.string().meta({ description: 'The assertion, used as the test label.' }),
|
|
125
|
+
request: ConformanceCaseRequest,
|
|
126
|
+
expect: ConformanceCaseExpectation,
|
|
127
|
+
note: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.meta({ description: 'Free-text note printed in the label / failure output.' }),
|
|
131
|
+
xfail: ConformanceCaseXfail.optional(),
|
|
132
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
import type { AppSurfaceSpec } from '../../http/surface.js';
|
|
3
|
+
import type { SessionOptions } from '../../auth/session_cookie.js';
|
|
4
|
+
import { type RpcEndpointsSuiteOption } from '../rpc_helpers.js';
|
|
5
|
+
import { type ConformanceCase } from './conformance_case.js';
|
|
6
|
+
import type { BackendCapabilities } from './capabilities.js';
|
|
7
|
+
import type { SetupTest } from './setup.js';
|
|
8
|
+
/**
|
|
9
|
+
* Names a seeded `extra_accounts` username for the `role_holder` /
|
|
10
|
+
* `wrong_role` principals — the only two that aren't backed by an
|
|
11
|
+
* always-available fixture accessor. Suites exercising those principals
|
|
12
|
+
* declare the matching `extra_accounts` at setup and name them here.
|
|
13
|
+
*/
|
|
14
|
+
export interface ConformancePrincipalConfig {
|
|
15
|
+
/** `extra_accounts` username for the `role_holder` principal. */
|
|
16
|
+
readonly role_holder?: string;
|
|
17
|
+
/** `extra_accounts` username for the `wrong_role` principal. */
|
|
18
|
+
readonly wrong_role?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Options for `describe_conformance_table_tests`. */
|
|
21
|
+
export interface ConformanceTableOptions {
|
|
22
|
+
/** The conformance cases to run, in order. */
|
|
23
|
+
readonly cases: ReadonlyArray<ConformanceCase>;
|
|
24
|
+
/** Per-test fixture producer (in-process or cross-process). */
|
|
25
|
+
readonly setup_test: SetupTest;
|
|
26
|
+
/** Surface spec — supplies the `RouteSpec`s for the REST branch. */
|
|
27
|
+
readonly surface_source: AppSurfaceSpec;
|
|
28
|
+
/** Declared backend capabilities (reserved for capability-gated rows). */
|
|
29
|
+
readonly capabilities: BackendCapabilities;
|
|
30
|
+
/** RPC endpoints — resolved to find each method's action spec. */
|
|
31
|
+
readonly rpc_endpoints: RpcEndpointsSuiteOption;
|
|
32
|
+
/** Session options — needed to resolve the `rpc_endpoints` factory form. */
|
|
33
|
+
readonly session_options: SessionOptions<string>;
|
|
34
|
+
/** Maps the `role_holder` / `wrong_role` principals to seeded usernames. */
|
|
35
|
+
readonly principals?: ConformancePrincipalConfig;
|
|
36
|
+
/** `describe` block label. Defaults to `'conformance table'`. */
|
|
37
|
+
readonly suite_name?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register a `describe` block running every `ConformanceCase` as a
|
|
41
|
+
* vitest `test` (or `xfail_until` for deferred-by-design rows). Drives
|
|
42
|
+
* either transport via the shared `{setup_test, surface_source,
|
|
43
|
+
* capabilities}` protocol.
|
|
44
|
+
*/
|
|
45
|
+
export declare const describe_conformance_table_tests: (options: ConformanceTableOptions) => void;
|
|
46
|
+
//# sourceMappingURL=conformance_table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance_table.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/conformance_table.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwB9B,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAMjE,OAAO,EAIN,KAAK,uBAAuB,EAC5B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAC,KAAK,eAAe,EAA4B,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,KAAK,EAAC,SAAS,EAAc,MAAM,YAAY,CAAC;AAGvD;;;;;GAKG;AACH,MAAM,WAAW,0BAA0B;IAC1C,iEAAiE;IACjE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAC/C,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,kEAAkE;IAClE,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD,4EAA4E;IAC5E,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACjD,iEAAiE;IACjE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAgOD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,uBAAuB,KAAG,IAgBnF,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import '../assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Table runner for the declarative cross-backend conformance suite.
|
|
4
|
+
*
|
|
5
|
+
* `describe_conformance_table_tests` takes a list of `ConformanceCase`
|
|
6
|
+
* rows plus the standard `{setup_test, surface_source, capabilities}`
|
|
7
|
+
* fixture protocol every Tier 1 suite uses — so **one runner drives both
|
|
8
|
+
* transports**: in-process via `default_in_process_setup` (fast, every
|
|
9
|
+
* `gro test`) and cross-process via `default_cross_process_setup` (the
|
|
10
|
+
* conformance gate, exercising each impl's real auth resolution over real
|
|
11
|
+
* HTTP). Same case definition, transport-parameterized.
|
|
12
|
+
*
|
|
13
|
+
* Each row references a `method`; the runner resolves its `input` /
|
|
14
|
+
* `output` schema from the live spec registry (RPC) or `RouteSpec` (the 6
|
|
15
|
+
* REST auth routes) — the row never carries a schema. The principal the
|
|
16
|
+
* row runs `as` resolves to a `TestFixture` accessor via
|
|
17
|
+
* `resolve_principal` — no inline credential minting.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import { assert, describe, test } from 'vitest';
|
|
22
|
+
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';
|
|
24
|
+
import {} from './conformance_case.js';
|
|
25
|
+
import { xfail_until } from './xfail.js';
|
|
26
|
+
/**
|
|
27
|
+
* Map a `ConformancePrincipal` onto the transport + headers it
|
|
28
|
+
* authenticates with, reading exclusively from the per-test `TestFixture`.
|
|
29
|
+
*
|
|
30
|
+
* The five always-available principals resolve from fixture accessors;
|
|
31
|
+
* `role_holder` / `wrong_role` read a seeded `extra_accounts` entry named
|
|
32
|
+
* via `options.principals` (throws a clear setup error when unconfigured).
|
|
33
|
+
*/
|
|
34
|
+
const resolve_principal = async (fixture, as, principals) => {
|
|
35
|
+
switch (as) {
|
|
36
|
+
case 'keeper':
|
|
37
|
+
// Keeper carries its session cookie via `create_session_headers`
|
|
38
|
+
// (in-process the transport is stateless; cross-process the jar
|
|
39
|
+
// also holds it — the explicit header is the same value).
|
|
40
|
+
return { transport: fixture.transport, headers: fixture.create_session_headers() };
|
|
41
|
+
case 'daemon':
|
|
42
|
+
// Daemon-token is a non-browser credential — empty jar + no Origin.
|
|
43
|
+
return {
|
|
44
|
+
transport: fixture.fresh_transport({ origin: null }),
|
|
45
|
+
headers: fixture.create_daemon_token_headers(),
|
|
46
|
+
suppress_default_origin: true,
|
|
47
|
+
};
|
|
48
|
+
case 'token':
|
|
49
|
+
// Bearer is discarded in a browser context — empty jar + no Origin.
|
|
50
|
+
return {
|
|
51
|
+
transport: fixture.fresh_transport({ origin: null }),
|
|
52
|
+
headers: fixture.create_bearer_headers(),
|
|
53
|
+
suppress_default_origin: true,
|
|
54
|
+
};
|
|
55
|
+
case 'anonymous':
|
|
56
|
+
// Fresh jar so the keeper cookie (cross-process) can't leak in.
|
|
57
|
+
return { transport: fixture.fresh_transport(), headers: {} };
|
|
58
|
+
case 'fresh_non_admin': {
|
|
59
|
+
const account = await fixture.create_account();
|
|
60
|
+
return { transport: fixture.fresh_transport(), headers: account.create_session_headers() };
|
|
61
|
+
}
|
|
62
|
+
case 'expired_session': {
|
|
63
|
+
// The keeper presented via an expired server-side session — fresh
|
|
64
|
+
// jar so only the (expired) cookie this seam returns is sent. The
|
|
65
|
+
// minted cookie payload is valid; the backdated `auth_session` row
|
|
66
|
+
// is what the DB-row expiry gate refuses.
|
|
67
|
+
const cookie = await fixture.mint_expired_session();
|
|
68
|
+
return { transport: fixture.fresh_transport(), headers: { cookie } };
|
|
69
|
+
}
|
|
70
|
+
case 'role_holder':
|
|
71
|
+
case 'wrong_role': {
|
|
72
|
+
const username = principals?.[as];
|
|
73
|
+
if (!username) {
|
|
74
|
+
throw new Error(`conformance: principal '${as}' requires options.principals.${as} naming a seeded ` +
|
|
75
|
+
`extra_accounts username (declare the account at setup via extra_accounts).`);
|
|
76
|
+
}
|
|
77
|
+
const extra = fixture.extra_accounts[username];
|
|
78
|
+
if (!extra) {
|
|
79
|
+
throw new Error(`conformance: extra_accounts['${username}'] not seeded for principal '${as}' — ` +
|
|
80
|
+
`declare it in the suite's extra_accounts option.`);
|
|
81
|
+
}
|
|
82
|
+
return { transport: fixture.fresh_transport(), headers: extra.create_session_headers() };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** Assert each expected field deep-equals the corresponding response field. */
|
|
87
|
+
const assert_fields = (actual, fields, label) => {
|
|
88
|
+
assert.ok(actual !== null && typeof actual === 'object', `${label}: expected an object to read fields from, got ${JSON.stringify(actual)}`);
|
|
89
|
+
const record = actual;
|
|
90
|
+
for (const [key, expected] of Object.entries(fields)) {
|
|
91
|
+
assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const is_success_status = (status) => status >= 200 && status < 300;
|
|
95
|
+
/**
|
|
96
|
+
* Run one conformance case end-to-end: resolve the principal, dispatch the
|
|
97
|
+
* request, and assert the expected status / reason / fields.
|
|
98
|
+
*/
|
|
99
|
+
const run_case = async (c, options, resolved_rpc_endpoints) => {
|
|
100
|
+
const fixture = await options.setup_test();
|
|
101
|
+
const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
|
|
102
|
+
if (c.request.method.startsWith('/')) {
|
|
103
|
+
await run_rest_case(c, options, transport, headers);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
await run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
|
|
107
|
+
};
|
|
108
|
+
/** Dispatch + assert a case targeting an RPC method. */
|
|
109
|
+
const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
|
|
110
|
+
const found = find_rpc_action(resolved_rpc_endpoints, c.request.method);
|
|
111
|
+
if (!found) {
|
|
112
|
+
throw new Error(`conformance: RPC method '${c.request.method}' not found on the surface — ` +
|
|
113
|
+
`check the method name or that the action is registered on rpc_endpoints.`);
|
|
114
|
+
}
|
|
115
|
+
const res = await rpc_call({
|
|
116
|
+
app: transport,
|
|
117
|
+
path: found.path,
|
|
118
|
+
method: c.request.method,
|
|
119
|
+
params: c.request.params,
|
|
120
|
+
headers,
|
|
121
|
+
...(c.request.verb && { verb: c.request.verb }),
|
|
122
|
+
...(suppress_default_origin && { suppress_default_origin: true }),
|
|
123
|
+
});
|
|
124
|
+
if (is_success_status(c.expect.status)) {
|
|
125
|
+
assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
|
|
126
|
+
assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
|
|
127
|
+
const parsed = found.action.spec.output.safeParse(res.result);
|
|
128
|
+
assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
129
|
+
if (c.expect.fields)
|
|
130
|
+
assert_fields(res.result, c.expect.fields, c.name);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
assert.ok(!res.ok, `${c.name}: expected error status ${c.expect.status} but got success`);
|
|
134
|
+
assert.strictEqual(res.status, c.expect.status, `${c.name}: error status`);
|
|
135
|
+
if (c.expect.error_reason !== undefined) {
|
|
136
|
+
const reason = res.error.data?.reason;
|
|
137
|
+
// Most RPC denials carry `error.data.reason` (incl. the pre-validation
|
|
138
|
+
// 401 now); a denial that genuinely omits it falls back to the status
|
|
139
|
+
// assertion above to pin the denial class.
|
|
140
|
+
if (reason !== undefined) {
|
|
141
|
+
assert.strictEqual(reason, c.expect.error_reason, `${c.name}: error.data.reason`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (c.expect.fields)
|
|
145
|
+
assert_fields(res.error.data, c.expect.fields, c.name);
|
|
146
|
+
};
|
|
147
|
+
/** Dispatch + assert a case targeting one of the 6 REST auth routes. */
|
|
148
|
+
const run_rest_case = async (c, options, transport, headers) => {
|
|
149
|
+
const suffix = c.request.method;
|
|
150
|
+
if (!rest_auth_route_suffixes.includes(suffix)) {
|
|
151
|
+
throw new Error(`conformance: REST method '${c.request.method}' is not a known auth-route suffix ` +
|
|
152
|
+
`(${rest_auth_route_suffixes.join(', ')}). Use an RPC method name for RPC actions.`);
|
|
153
|
+
}
|
|
154
|
+
const verb = c.request.verb ?? 'POST';
|
|
155
|
+
const route = find_auth_route(options.surface_source.route_specs, suffix, verb);
|
|
156
|
+
if (!route) {
|
|
157
|
+
throw new Error(`conformance: no REST route spec for ${verb} ${suffix} on the surface.`);
|
|
158
|
+
}
|
|
159
|
+
const init = {
|
|
160
|
+
method: verb,
|
|
161
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
162
|
+
...(verb !== 'GET' &&
|
|
163
|
+
c.request.params !== undefined && { body: JSON.stringify(c.request.params) }),
|
|
164
|
+
};
|
|
165
|
+
const response = await transport(route.path, init);
|
|
166
|
+
assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
|
|
167
|
+
const body = await response.json().catch(() => undefined);
|
|
168
|
+
if (is_success_status(c.expect.status)) {
|
|
169
|
+
const parsed = route.output.safeParse(body);
|
|
170
|
+
assert.ok(parsed.success, `${c.name}: body does not match route output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
|
|
171
|
+
}
|
|
172
|
+
else if (c.expect.error_reason !== undefined) {
|
|
173
|
+
const error = body?.error;
|
|
174
|
+
assert.strictEqual(error, c.expect.error_reason, `${c.name}: body.error`);
|
|
175
|
+
}
|
|
176
|
+
if (c.expect.fields)
|
|
177
|
+
assert_fields(body, c.expect.fields, c.name);
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* Register a `describe` block running every `ConformanceCase` as a
|
|
181
|
+
* vitest `test` (or `xfail_until` for deferred-by-design rows). Drives
|
|
182
|
+
* either transport via the shared `{setup_test, surface_source,
|
|
183
|
+
* capabilities}` protocol.
|
|
184
|
+
*/
|
|
185
|
+
export const describe_conformance_table_tests = (options) => {
|
|
186
|
+
const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
|
|
187
|
+
describe(options.suite_name ?? 'conformance table', () => {
|
|
188
|
+
for (const c of options.cases) {
|
|
189
|
+
const label = c.note ? `${c.name} — ${c.note}` : c.name;
|
|
190
|
+
const body = () => run_case(c, options, resolved_rpc_endpoints);
|
|
191
|
+
if (c.xfail) {
|
|
192
|
+
xfail_until(c.xfail.tracking_id, c.xfail.reason, label, body);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
test(label, body);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
};
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;GAKG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBASpC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAStC,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;CAC/B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAmCF,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;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA2CF,CAAC"}
|
|
@@ -16,7 +16,6 @@ export const ts_default_capabilities = Object.freeze({
|
|
|
16
16
|
cell_crud: true,
|
|
17
17
|
cell_relations: true,
|
|
18
18
|
account_lifecycle: true,
|
|
19
|
-
in_process_only: false,
|
|
20
19
|
});
|
|
21
20
|
/**
|
|
22
21
|
* Capabilities for the Rust family. Adds `trusted_proxy: true` (the
|
|
@@ -33,7 +32,6 @@ export const rust_default_capabilities = Object.freeze({
|
|
|
33
32
|
cell_crud: true,
|
|
34
33
|
cell_relations: true,
|
|
35
34
|
account_lifecycle: true,
|
|
36
|
-
in_process_only: false,
|
|
37
35
|
});
|
|
38
36
|
/** Bootstrap block built from the default secrets + supplied paths. */
|
|
39
37
|
const build_default_bootstrap = (paths, overrides) => ({
|