proposal-approve-rbac-fix-stress.test.mjs
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