Skip to content

Commit ce39475

Browse files
bs-nubankclaude
andcommitted
test(auth): add unit tests for OAuth state CSRF extraction
Adds the extractCsrfFromState helper (mirrors AuthManager logic) and a dedicated describe block with 6 test cases covering: - base64 JSON format (v0.0.9+ cloud function) - raw hex format (≤v0.0.7 cloud function) - null / empty state - base64 JSON without csrf field (fallback) - non-JSON base64 garbage (fallback) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d6d5770 commit ce39475

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

workspace-server/src/__tests__/auth/AuthManager.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,31 @@ jest.mock('googleapis');
1414
jest.mock('../../utils/logger');
1515
jest.mock('../../utils/secure-browser-launcher');
1616

17+
/**
18+
* Helper that mirrors the CSRF extraction logic in AuthManager.authWithWeb,
19+
* so we can unit-test all state-format permutations without spinning up an
20+
* HTTP server or touching the private method directly.
21+
*
22+
* Supports two formats returned by the cloud function:
23+
* - v0.0.9+: full base64-encoded JSON {"uri":…,"manual":…,"csrf":"<hex>"}
24+
* - ≤v0.0.7: raw hex CSRF string
25+
*/
26+
function extractCsrfFromState(returnedState: string | null): string | null {
27+
if (!returnedState) return null;
28+
try {
29+
const decoded = JSON.parse(
30+
Buffer.from(returnedState, 'base64').toString('utf8'),
31+
);
32+
if (typeof decoded?.csrf === 'string') {
33+
return decoded.csrf;
34+
}
35+
} catch {
36+
// Not base64 JSON — fall through
37+
}
38+
// Fallback: treat as raw hex token (≤v0.0.7 cloud function)
39+
return returnedState;
40+
}
41+
1742
// Mock fetch globally for refreshToken tests
1843
global.fetch = jest.fn();
1944

@@ -261,3 +286,37 @@ describe('AuthManager', () => {
261286
);
262287
});
263288
});
289+
290+
describe('OAuth state CSRF extraction', () => {
291+
const RAW_CSRF = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
292+
293+
it('extracts csrf from base64 JSON state (v0.0.9+ cloud function format)', () => {
294+
const payload = { uri: 'http://localhost:54321/oauth2callback', manual: false, csrf: RAW_CSRF };
295+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
296+
expect(extractCsrfFromState(state)).toBe(RAW_CSRF);
297+
});
298+
299+
it('returns raw hex as-is when state is not base64 JSON (≤v0.0.7 cloud function format)', () => {
300+
expect(extractCsrfFromState(RAW_CSRF)).toBe(RAW_CSRF);
301+
});
302+
303+
it('returns null when state is null', () => {
304+
expect(extractCsrfFromState(null)).toBeNull();
305+
});
306+
307+
it('returns null when state is empty string', () => {
308+
expect(extractCsrfFromState('')).toBeNull();
309+
});
310+
311+
it('falls back to raw value when base64 decodes to JSON without csrf field', () => {
312+
const payload = { uri: 'http://localhost:54321/oauth2callback', manual: false };
313+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
314+
// No csrf field → falls back to treating the whole base64 string as the token
315+
expect(extractCsrfFromState(state)).toBe(state);
316+
});
317+
318+
it('falls back to raw value when base64 decodes to non-JSON', () => {
319+
const garbage = Buffer.from('not-json-at-all').toString('base64');
320+
expect(extractCsrfFromState(garbage)).toBe(garbage);
321+
});
322+
});

0 commit comments

Comments
 (0)