companion-loopback-guard-stress.test.mjs
112 lines 4.9 KB
Raw
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