companion-loopback-guard-stress.test.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Tier 4 — STRESS: high-volume and adversarial-volume behaviour. |
| 3 | * |
| 4 | * The guard is pure and synchronous, so "stress" here means correctness and stability under |
| 5 | * large request counts: many auth attempts (gate §10 stress requirement), large allowlists, |
| 6 | * large rate windows, and pathological header bags. No socket, no concurrency primitives — the |
| 7 | * pure function must stay correct and bounded regardless of volume. |
| 8 | * |
| 9 | * Reference: docs/COMPANION-APP-DESIGN-AND-AUTHORIZATION-GATE.md §10 (stress: many auth attempts). |
| 10 | */ |
| 11 | import { describe, it } from 'node:test'; |
| 12 | import assert from 'node:assert/strict'; |
| 13 | import { |
| 14 | verifyLoopbackRequest, |
| 15 | createLoopbackRateState, |
| 16 | recordLoopbackRequest, |
| 17 | shouldCountTowardRateLimit, |
| 18 | LOOPBACK_GUARD_REASONS, |
| 19 | } from '../lib/companion-loopback-guard.mjs'; |
| 20 | |
| 21 | const PORT = '50001'; |
| 22 | const TOKEN = 'stress-' + 'e'.repeat(40); |
| 23 | const ALLOWED_HOSTS = [`127.0.0.1:${PORT}`, `localhost:${PORT}`]; |
| 24 | |
| 25 | function base(overrides = {}, rateState) { |
| 26 | return { |
| 27 | method: 'POST', |
| 28 | headers: { Host: `127.0.0.1:${PORT}`, Origin: `http://127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'same-origin' }, |
| 29 | token: TOKEN, |
| 30 | expectedToken: TOKEN, |
| 31 | allowedHosts: ALLOWED_HOSTS, |
| 32 | now: 0, |
| 33 | rateState, |
| 34 | ...overrides, |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | describe('Stress — 100k auth attempts with wrong tokens', () => { |
| 39 | it('every wrong-token request is denied; never a single accidental allow', () => { |
| 40 | let allows = 0; |
| 41 | for (let i = 0; i < 100_000; i++) { |
| 42 | // generous rate budget so we isolate the token decision, not the rate decision |
| 43 | const v = verifyLoopbackRequest(base({ token: `bad-${i}` }, { windowMs: 60_000, maxRequests: 1_000_000, timestamps: [] })); |
| 44 | if (v.allow) allows++; |
| 45 | } |
| 46 | assert.equal(allows, 0); |
| 47 | }); |
| 48 | }); |
| 49 | |
| 50 | describe('Stress — rate window stays bounded under sustained load', () => { |
| 51 | it('timestamps array never grows beyond what the window can hold', () => { |
| 52 | const windowMs = 1000; |
| 53 | let state = createLoopbackRateState({ windowMs, maxRequests: 100 }); |
| 54 | let maxLen = 0; |
| 55 | for (let i = 0; i < 50_000; i++) { |
| 56 | // advance time by 1ms each request → window holds ~1000 entries max before pruning |
| 57 | const now = i; |
| 58 | const v = verifyLoopbackRequest(base({ now }, state)); |
| 59 | if (shouldCountTowardRateLimit(v)) state = recordLoopbackRequest(state, now); |
| 60 | if (state.timestamps.length > maxLen) maxLen = state.timestamps.length; |
| 61 | } |
| 62 | // Only slot-consuming (token-stage) verdicts are recorded; once maxRequests slots are filled |
| 63 | // in-window the guard returns 429 and the caller records nothing, so the array is bounded by |
| 64 | // maxRequests, never by total request volume. |
| 65 | assert.ok(maxLen <= 100, `timestamps grew to ${maxLen}, expected ≤ 100 (maxRequests)`); |
| 66 | }); |
| 67 | }); |
| 68 | |
| 69 | describe('Stress — very large allowlist', () => { |
| 70 | it('matches correctly even with a 10k-entry allowedHosts', () => { |
| 71 | const big = []; |
| 72 | for (let i = 0; i < 10_000; i++) big.push(`127.0.0.1:${10000 + i}`); |
| 73 | big.push(`127.0.0.1:${PORT}`); |
| 74 | const v = verifyLoopbackRequest(base({ allowedHosts: big }, createLoopbackRateState())); |
| 75 | assert.equal(v.allow, true); |
| 76 | }); |
| 77 | }); |
| 78 | |
| 79 | describe('Stress — pathological header bags', () => { |
| 80 | it('handles many junk headers without misclassifying the decision', () => { |
| 81 | const headers = { Host: `127.0.0.1:${PORT}`, Origin: `http://127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'same-origin' }; |
| 82 | for (let i = 0; i < 5000; i++) headers[`X-Junk-${i}`] = `v${i}`; |
| 83 | const v = verifyLoopbackRequest(base({ headers }, createLoopbackRateState())); |
| 84 | assert.equal(v.allow, true); |
| 85 | }); |
| 86 | |
| 87 | it('handles a header value that is a huge string without leaking it or crashing', () => { |
| 88 | const huge = 'A'.repeat(1_000_000); |
| 89 | const v = verifyLoopbackRequest(base({ |
| 90 | headers: { Host: `127.0.0.1:${PORT}`, 'X-Evil': huge, 'Sec-Fetch-Site': 'same-origin', Origin: `http://127.0.0.1:${PORT}` }, |
| 91 | }, createLoopbackRateState())); |
| 92 | assert.equal(v.allow, true); |
| 93 | assert.ok(!v.reason.includes('A')); |
| 94 | }); |
| 95 | }); |
| 96 | |
| 97 | describe('Stress — interleaved mixed verdicts remain individually correct', () => { |
| 98 | it('a rotating mix of good/bad-host/cross-site/bad-token always yields the right reason', () => { |
| 99 | const rotation = [ |
| 100 | { o: {}, want: LOOPBACK_GUARD_REASONS.OK, allow: true }, |
| 101 | { o: { headers: { Host: 'bad.example' } }, want: LOOPBACK_GUARD_REASONS.HOST_NOT_ALLOWED, allow: false }, |
| 102 | { o: { headers: { Host: `127.0.0.1:${PORT}`, 'Sec-Fetch-Site': 'cross-site' } }, want: LOOPBACK_GUARD_REASONS.CROSS_SITE_FORBIDDEN, allow: false }, |
| 103 | { o: { token: 'nope' }, want: LOOPBACK_GUARD_REASONS.INVALID_TOKEN, allow: false }, |
| 104 | ]; |
| 105 | for (let i = 0; i < 20_000; i++) { |
| 106 | const { o, want, allow } = rotation[i % rotation.length]; |
| 107 | const v = verifyLoopbackRequest(base({ ...o }, { windowMs: 60_000, maxRequests: 1_000_000, timestamps: [] })); |
| 108 | assert.equal(v.reason, want); |
| 109 | assert.equal(v.allow, allow); |
| 110 | } |
| 111 | }); |
| 112 | }); |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
1 day ago