proposal-approve-rbac-fix-stress.test.mjs
133 lines 5.1 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 10 hours ago
1 /**
2 * Stress tests — proposal approve RBAC fix.
3 *
4 * Verifies the resolveHostedActorRole logic behaves correctly under:
5 * - Rapid concurrent role resolution calls (simulated).
6 * - Large numbers of distinct subs, some admin, some not.
7 * - Multiple bridge error scenarios in sequence.
8 */
9
10 import { test, describe } from 'node:test';
11 import assert from 'node:assert/strict';
12 import jwt from 'jsonwebtoken';
13
14 const SECRET = 'stress-test-secret';
15
16 function makeToken(sub, role, secret = SECRET) {
17 return jwt.sign({ sub, role }, secret, { expiresIn: '1h' });
18 }
19
20 /**
21 * Mirrors resolveHostedActorRole logic with injectable bridge behavior.
22 */
23 async function resolveRole({ bridgeRole, bridgeStatus, adminSet, token, sub }) {
24 function roleForSub(s) { return adminSet.has(s) ? 'admin' : 'member'; }
25 let role = 'member';
26 let mayApproveProposals = false;
27 let bridgeResolved = false;
28 if (bridgeStatus === 200 && bridgeRole) {
29 role = bridgeRole;
30 bridgeResolved = true;
31 mayApproveProposals = role === 'admin';
32 }
33 if (!bridgeResolved) {
34 try {
35 const payload = jwt.verify(token, SECRET);
36 role = payload.role || roleForSub(payload.sub);
37 mayApproveProposals = role === 'admin';
38 } catch (_) {}
39 }
40 // Gateway override
41 if (sub && role !== 'admin' && roleForSub(sub) === 'admin') {
42 role = 'admin';
43 mayApproveProposals = true;
44 }
45 return { role, mayApproveProposals };
46 }
47
48 describe('Stress: concurrent role resolutions', () => {
49 test('100 concurrent role resolutions — all complete correctly', async () => {
50 const adminSubs = new Set(['google:admin-a', 'google:admin-b']);
51 const subs = [
52 ...Array(50).fill(null).map((_, i) => `google:member-${i}`),
53 'google:admin-a',
54 'google:admin-b',
55 ...Array(48).fill(null).map((_, i) => `google:other-${i}`),
56 ];
57 const results = await Promise.all(subs.map((sub) => {
58 const isAdmin = adminSubs.has(sub);
59 const token = makeToken(sub, isAdmin ? 'admin' : 'member');
60 return resolveRole({
61 bridgeRole: null, bridgeStatus: 401,
62 adminSet: adminSubs, token, sub,
63 });
64 }));
65 let adminCount = 0;
66 for (let i = 0; i < results.length; i++) {
67 const expected = adminSubs.has(subs[i]) ? 'admin' : 'member';
68 assert.equal(results[i].role, expected, `Sub ${subs[i]}: expected ${expected}, got ${results[i].role}`);
69 if (results[i].role === 'admin') adminCount++;
70 }
71 assert.equal(adminCount, 2, 'Exactly 2 admin results (the two admin subs)');
72 });
73
74 test('500 sequential bridge-fail resolutions all fallback correctly', async () => {
75 const adminSet = new Set(['google:real-admin']);
76 for (let i = 0; i < 500; i++) {
77 const sub = `google:user-${i}`;
78 const token = makeToken(sub, 'member');
79 const result = await resolveRole({
80 bridgeRole: null, bridgeStatus: 500,
81 adminSet, token, sub,
82 });
83 assert.equal(result.role, 'member', `User ${i} should remain member after bridge failure`);
84 }
85 });
86
87 test('admin subs keep admin role across 200 calls with alternating bridge responses', async () => {
88 const adminSet = new Set(['google:admin-x']);
89 const token = makeToken('google:admin-x', 'member'); // JWT says member but sub is in adminSet
90 for (let i = 0; i < 200; i++) {
91 const bridgeStatus = i % 2 === 0 ? 200 : 401;
92 const bridgeRole = bridgeStatus === 200 ? 'member' : null;
93 const result = await resolveRole({
94 bridgeRole, bridgeStatus,
95 adminSet,
96 token,
97 sub: 'google:admin-x',
98 });
99 // Gateway override should ALWAYS promote, regardless of bridge status
100 assert.equal(result.role, 'admin', `Iteration ${i}: gateway admin override should win`);
101 }
102 });
103 });
104
105 describe('Stress: adminUserIdsSet scale', () => {
106 test('adminUserIdsSet with 10,000 entries: lookup is O(1) and correct', () => {
107 const adminSet = new Set(Array.from({ length: 10000 }, (_, i) => `google:admin-${i}`));
108 adminSet.add('google:the-chosen-one');
109 // Not admin
110 const notAdmin = adminSet.has('google:impostor');
111 assert.equal(notAdmin, false, 'Non-admin sub not in large set');
112 // Is admin
113 const isAdmin = adminSet.has('google:the-chosen-one');
114 assert.equal(isAdmin, true, 'Admin sub found in large set');
115 // Specific numeric member
116 const isNumberedAdmin = adminSet.has('google:admin-9999');
117 assert.equal(isNumberedAdmin, true, 'Numbered admin found');
118 const notNumberedAdmin = adminSet.has('google:admin-10000');
119 assert.equal(notNumberedAdmin, false, 'Out-of-range admin not found');
120 });
121
122 test('bridge fallback JWT verify does not degrade under repeated calls', async () => {
123 const token = makeToken('google:stress-user', 'admin');
124 const start = performance.now();
125 for (let i = 0; i < 1000; i++) {
126 const payload = jwt.verify(token, SECRET);
127 assert.equal(payload.role, 'admin');
128 }
129 const elapsed = performance.now() - start;
130 // 1000 verifications should complete in under 2 seconds on any reasonable hardware
131 assert.ok(elapsed < 2000, `1000 jwt.verify calls completed in ${elapsed.toFixed(0)}ms (must be < 2000ms)`);
132 });
133 });
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 10 hours ago