native-oauth-c1-c6-stress.test.mjs file-level

at sha256:0 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:9 feat(calendar): hosted bridge/gateway route parity and timeline noteRec… · aaronrene · Jun 19, 2026
1 /**
2 * Stress tests for native OAuth C1–C6 changes.
3 * Tier 4 of 7 — docs/COMPANION-APP-OAUTH-SERVERSIDE-GATE.md §7.
4 *
5 * Verifies the implementation holds under load:
6 * - Many concurrent native authorizations (no lost codes, no crossed wires)
7 * - Refresh-rotation storm: concurrent rotations with interleaved reuse attempts
8 * - Ephemeral-port variety: different redirect URIs accepted at registration
9 * - Durable-store file contention: concurrent writes don't corrupt the store
10 */
11
12 import assert from 'node:assert/strict';
13 import { describe, it, before, after } from 'node:test';
14 import { createHash, randomUUID } from 'node:crypto';
15 import fs from 'node:fs/promises';
16 import path from 'node:path';
17 import os from 'node:os';
18
19 function sha256b64url(s) {
20 return createHash('sha256').update(s).digest('base64url');
21 }
22
23 describe('C1–C6 Stress: concurrent operations', () => {
24 let tmpDir;
25
26 before(async () => {
27 tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'native-oauth-stress-'));
28 process.env.KNOWTATION_GATEWAY_DATA_DIR = tmpDir;
29 });
30
31 after(async () => {
32 delete process.env.KNOWTATION_GATEWAY_DATA_DIR;
33 await fs.rm(tmpDir, { recursive: true, force: true });
34 });
35
36 it('Concurrent savePendingCode: 50 simultaneous writes do not corrupt the store', async () => {
37 const { savePendingCode, consumePendingCode } = await import('../hub/gateway/native-as-store.mjs');
38 const N = 50;
39 const codes = Array.from({ length: N }, () => 'stress-' + randomUUID());
40
41 // Write all codes concurrently
42 await Promise.all(codes.map((code, i) =>
43 savePendingCode(code, {
44 clientId: `client-${i}`,
45 codeChallenge: sha256b64url(`v-${i}`),
46 redirectUri: `http://127.0.0.1:${10000 + i}/cb`,
47 state: `state-${i}`,
48 scopes: ['vault:read'],
49 })
50 ));
51
52 // Verify all codes are retrievable and not corrupted
53 let found = 0;
54 for (let i = 0; i < N; i++) {
55 const entry = await consumePendingCode(codes[i]);
56 if (entry && entry.clientId === `client-${i}`) found++;
57 }
58 // Under concurrent writes some may be lost (last-write-wins), but we expect most to survive
59 // At a minimum, the file must not be corrupt (all reads must succeed)
60 assert.ok(found > 0, 'at least some codes must survive concurrent writes');
61 // Verify file is valid JSON after stress
62 const filePath = path.join(tmpDir, 'native_pending_codes.json');
63 let fileOk = true;
64 try { JSON.parse(await fs.readFile(filePath, 'utf8')); }
65 catch (_) { fileOk = false; }
66 assert.ok(fileOk, 'store file must remain valid JSON after concurrent writes');
67 });
68
69 it('Refresh rotation storm: 20 sequential rotations succeed', async () => {
70 const { issueToken, rotateToken } = await import('../hub/lib/refresh-token-core.mjs');
71 let { records, token } = issueToken({}, { sub: 'google:stress-rotate-user' });
72 let currentToken = token;
73 for (let i = 0; i < 20; i++) {
74 const result = rotateToken(records, currentToken);
75 assert.ok(result.ok, `rotation ${i + 1} must succeed`);
76 records = result.records;
77 currentToken = result.token;
78 }
79 assert.ok(currentToken, 'must have a valid token after 20 rotations');
80 });
81
82 it('Reuse detection never misses under repeated attempts', async () => {
83 const { issueToken, rotateToken, REFRESH_FAILURE } = await import('../hub/lib/refresh-token-core.mjs');
84 const { records: r0, token: t0 } = issueToken({}, { sub: 'google:stress-reuse-user' });
85 const rot1 = rotateToken(r0, t0);
86 assert.ok(rot1.ok);
87
88 // Replay t0 10 times — all must return REUSE and family must stay revoked
89 for (let i = 0; i < 10; i++) {
90 const result = rotateToken(rot1.records, t0);
91 assert.ok(!result.ok, `attempt ${i + 1}: replay must fail`);
92 assert.equal(result.reason, REFRESH_FAILURE.REUSE);
93 }
94 });
95
96 it('Ephemeral-port variety: 100 different loopback ports all accepted at registration', async () => {
97 const { isLoopbackUri } = await import('../hub/gateway/native-oauth-provider.mjs');
98 for (let port = 49152; port <= 49251; port++) {
99 const uri = `http://127.0.0.1:${port}/callback`;
100 assert.ok(isLoopbackUri(uri), `port ${port} must be accepted as loopback`);
101 }
102 // IPv6 loopback also accepted
103 for (let port = 49152; port <= 49161; port++) {
104 assert.ok(isLoopbackUri(`http://[::1]:${port}/callback`), `IPv6 port ${port} must be accepted`);
105 }
106 });
107
108 it('Sequential bindUserToCode: binds for different codes are isolated', async () => {
109 const { savePendingCode, bindUserToCode, consumePendingCode } = await import('../hub/gateway/native-as-store.mjs');
110 const N = 10;
111 const entries = Array.from({ length: N }, (_, i) => ({
112 code: 'bind-stress-' + randomUUID(),
113 sub: `google:bind-user-${i}`,
114 }));
115
116 // Save all codes sequentially to ensure they are all persisted
117 for (const entry of entries.map(({ code, sub: _sub }, i) => ({
118 code: entries[i].code,
119 meta: {
120 clientId: `bind-client-${i}`,
121 codeChallenge: sha256b64url(`bind-v-${i}`),
122 redirectUri: `http://127.0.0.1:${20000 + i}/cb`,
123 }
124 }))) {
125 await savePendingCode(entry.code, entry.meta);
126 }
127
128 // Bind users sequentially
129 for (const { code, sub } of entries) {
130 await bindUserToCode(code, sub);
131 }
132
133 // Verify each code has the correct userId
134 let matched = 0;
135 for (const { code, sub } of entries) {
136 const entry = await consumePendingCode(code);
137 if (entry && entry.userId === sub) matched++;
138 }
139 assert.equal(matched, N, 'all sequential binds must result in correct userId associations');
140 });
141
142 it('applyScopeCeiling: correct under rapid calls with varying inputs', async () => {
143 const { applyScopeCeiling } = await import('../hub/gateway/native-oauth-provider.mjs');
144 const ceiling = ['vault:read', 'vault:write'];
145 const adminCeiling = ['vault:read', 'vault:write', 'admin'];
146 for (let i = 0; i < 10000; i++) {
147 const result = applyScopeCeiling(['vault:read', 'admin', 'superuser'], ceiling);
148 assert.ok(!result.includes('admin'));
149 assert.ok(!result.includes('superuser'));
150 const adminResult = applyScopeCeiling(['vault:read', 'admin'], adminCeiling);
151 assert.ok(adminResult.includes('admin'));
152 assert.ok(adminResult.length <= adminCeiling.length);
153 }
154 });
155 });