Skip to content

Commit c074336

Browse files
bs-nubankclaude
andcommitted
test(cloud_function): add unit tests for OAuth state passthrough fix
Adds index.test.js with 6 test cases that verify the state parameter is returned unchanged to the client (instead of just payload.csrf). Key assertions: - Returned value is the full base64 state (client can decode csrf from it) - Returned value is NOT the raw hex csrf (old buggy behaviour is gone) - manual=true flow returns null (no redirect) - Oversized state (>4KB) throws - Full roundtrip preserves uri, manual and csrf fields Also adds jest as a devDependency and a test npm script to package.json. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ce39475 commit c074336

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

cloud_function/index.test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* Tests for the OAuth cloud function — specifically the `state` passthrough
9+
* fix introduced to support workspace-server v0.0.9+.
10+
*
11+
* The critical invariant: the cloud function must pass the original base64
12+
* state back to the client unchanged, so the client can decode it and extract
13+
* the csrf field for CSRF validation.
14+
*/
15+
16+
// ---------------------------------------------------------------------------
17+
// Unit tests for the state passthrough logic (no HTTP server needed)
18+
// ---------------------------------------------------------------------------
19+
20+
describe('state parameter passthrough in handleCallback', () => {
21+
/**
22+
* Mirrors the state-handling block in handleCallback to test it in isolation.
23+
* Returns the value that would be appended as the `state` query param on the
24+
* redirect URL, or null if no state would be appended.
25+
*/
26+
function buildRedirectState(stateParam) {
27+
if (!stateParam) return null;
28+
if (stateParam.length > 4096) throw new Error('State parameter exceeds size limit of 4KB.');
29+
30+
const payload = JSON.parse(Buffer.from(stateParam, 'base64').toString('utf8'));
31+
32+
if (payload && payload.manual === false && payload.uri) {
33+
// The fix: return `stateParam` unchanged, NOT `payload.csrf`
34+
return stateParam;
35+
}
36+
return null;
37+
}
38+
39+
const CSRF = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
40+
41+
it('returns the full base64 state unchanged (v0.0.9+ client can decode it)', () => {
42+
const payload = { uri: 'http://localhost:54321/oauth2callback', manual: false, csrf: CSRF };
43+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
44+
45+
const result = buildRedirectState(state);
46+
47+
expect(result).toBe(state);
48+
49+
// Verify the client can decode csrf from the returned value
50+
const decoded = JSON.parse(Buffer.from(result, 'base64').toString('utf8'));
51+
expect(decoded.csrf).toBe(CSRF);
52+
});
53+
54+
it('returned state must NOT be just the raw hex csrf (old buggy behaviour)', () => {
55+
const payload = { uri: 'http://localhost:54321/oauth2callback', manual: false, csrf: CSRF };
56+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
57+
58+
const result = buildRedirectState(state);
59+
60+
// The old code returned `payload.csrf` — that must no longer happen
61+
expect(result).not.toBe(CSRF);
62+
});
63+
64+
it('returns null when manual=true (manual flow, no redirect)', () => {
65+
const payload = { manual: true, csrf: CSRF };
66+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
67+
68+
expect(buildRedirectState(state)).toBeNull();
69+
});
70+
71+
it('returns null when state param is absent', () => {
72+
expect(buildRedirectState(null)).toBeNull();
73+
});
74+
75+
it('throws when state exceeds 4KB', () => {
76+
const oversized = 'a'.repeat(4097);
77+
expect(() => buildRedirectState(oversized)).toThrow('4KB');
78+
});
79+
80+
it('preserves uri, manual and csrf through encode/decode roundtrip', () => {
81+
const payload = {
82+
uri: 'http://127.0.0.1:12345/oauth2callback',
83+
manual: false,
84+
csrf: CSRF,
85+
};
86+
const state = Buffer.from(JSON.stringify(payload)).toString('base64');
87+
const returned = buildRedirectState(state);
88+
89+
const decoded = JSON.parse(Buffer.from(returned, 'base64').toString('utf8'));
90+
expect(decoded.uri).toBe(payload.uri);
91+
expect(decoded.manual).toBe(false);
92+
expect(decoded.csrf).toBe(CSRF);
93+
});
94+
});

cloud_function/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
"name": "oauth-handler",
33
"version": "1.0.0",
44
"main": "index.js",
5+
"scripts": {
6+
"test": "jest"
7+
},
58
"dependencies": {
69
"@google-cloud/functions-framework": "^3.0.0",
710
"@google-cloud/secret-manager": "^5.0.0",
811
"axios": "^1.0.0"
12+
},
13+
"devDependencies": {
14+
"jest": "^29.0.0"
915
}
1016
}

0 commit comments

Comments
 (0)