Cross-backend parity suite for action rate limiting over a real
WebSocket, and its budget shared with HTTP RPC.
An action spec's rate_limit class ('ip' / 'account' / 'both') is
charged by the dispatcher on both transports, and a server holds one
limiter per axis, so an action's budget is per caller, not per transport.
The in-process tests pin that through an assembled app; this suite pins it
over the wire, against backends spawned with small shared limiters (the
action rate-limit toggles — ACTION_RATE_LIMIT_ENABLED_ENV and
ACTION_RATE_LIMIT_MAX_ATTEMPTS_ENV — see global_setup_login_security.ts):
- account axis over WS —
account_token_create (`rate_limit:
'account' on both spines) with malformed params: the first max_attempts`
calls each answer invalid_params (the throttle runs before params
validation, so a malformed call still charges), and the next answers
rate_limited with a retry_after. - one account bucket across transports — calls split between HTTP RPC and
WS exhaust one budget: the call past the cap is refused on WS and on HTTP
(
429). - IP axis, keyed by the resolved client IP —
peer/ping (`rate_limit:
'ip', public) with malformed params, from one X-Forwarded-For` address
over anonymous HTTP and over a WS whose upgrade carried the same header:
the call past the cap is refused on both, while a second address keeps its
own budget on both. - account axis at the dispatchers —
cell_create (`rate_limit:
'account'`, on both spines' RPC and WS mounts) with malformed params: the
call past the cap answers 429 over HTTP and rate_limited over WS, calls
split between the two transports exhaust one budget, and a budget split
between cell_create and account_token_create is one bucket.
Charge sites. The TS spine charges every classed action in its
dispatcher. The Rust stub has two sites holding the same limiters: the auth
families (account_token_create) charge inside their handlers, and the
dispatcher states charge every other classed spec. So the
account_token_create cases pin the Rust in-handler site, peer/ping pins
the Rust WS dispatcher's IP axis, and cell_create pins the account axis of
both Rust dispatchers, RPC and WS.
Isolation. Limiter state is in-memory and _testing_reset wipes only
the database, so every case reads a bucket no other case touched: the
account cases each run on the fresh keeper the reset seeds (a new account
id), and the IP case uses addresses no other case sends. Login limiters are
also on in this project; nothing here logs in.
Cross-process only: the composition under test is the spawned binary's
(create_app_server threading one limiter pair to both mounts on TS; the
stub's dispatcher states plus its auth-family builders on Rust).