Skip to content

Commit 467ffae

Browse files
committed
feat: add unit test for auth client
1 parent 193d5d3 commit 467ffae

File tree

2 files changed

+119
-1
lines changed

2 files changed

+119
-1
lines changed

server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/mcp/mcpManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export class McpManager {
331331
// streamable http/SSE transport
332332
const base = new URL(cfg.url!)
333333
try {
334-
// Use HEAD to check if it nees OAuth
334+
// Use HEAD to check if it needs OAuth
335335
let headers: Record<string, string> = { ...(cfg.headers ?? {}) }
336336
let needsOAuth = false
337337
try {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
* All Rights Reserved. SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { expect } from 'chai'
7+
import * as sinon from 'sinon'
8+
import * as crypto from 'crypto'
9+
import * as http from 'http'
10+
import * as path from 'path'
11+
import { OAuthClient } from './mcpOauthClient'
12+
13+
const fakeLogger = {
14+
log: () => {},
15+
debug: () => {},
16+
info: () => {},
17+
warn: () => {},
18+
error: () => {},
19+
}
20+
21+
const fakeWorkspace = {
22+
fs: {
23+
exists: async (_path: string) => false,
24+
readFile: async (_path: string) => Buffer.from('{}'),
25+
writeFile: async (_path: string, _d: any) => {},
26+
mkdir: async (_dir: string, _opts: any) => {},
27+
},
28+
} as any
29+
30+
function stubFileSystem(tokenObj?: any, regObj?: any): void {
31+
const cacheDir = (OAuthClient as any).cacheDir as string
32+
const tokPath = path.join(cacheDir, 'testkey.token.json')
33+
const regPath = path.join(cacheDir, 'testkey.registration.json')
34+
35+
const existsStub = sinon.stub(fakeWorkspace.fs, 'exists')
36+
existsStub.callsFake(async (p: any) => {
37+
if (p === tokPath && tokenObj) return true
38+
if (p === regPath && regObj) return true
39+
return false
40+
})
41+
42+
const readStub = sinon.stub(fakeWorkspace.fs, 'readFile')
43+
readStub.callsFake(async (p: any) => {
44+
if (p === tokPath && tokenObj) return Buffer.from(JSON.stringify(tokenObj))
45+
if (p === regPath && regObj) return Buffer.from(JSON.stringify(regObj))
46+
return Buffer.from('{}')
47+
})
48+
49+
sinon.stub(fakeWorkspace.fs, 'writeFile').resolves()
50+
sinon.stub(fakeWorkspace.fs, 'mkdir').resolves()
51+
}
52+
53+
function stubHttpServer(): void {
54+
sinon.stub(http, 'createServer').returns({
55+
listen: (...args: any[]) => {
56+
const cb = args.find(a => typeof a === 'function')
57+
if (cb) cb()
58+
return {} as any
59+
},
60+
close: (cb?: any) => {
61+
if (cb) cb()
62+
return {} as any
63+
},
64+
on: (..._args: any[]) => {
65+
return {} as any
66+
},
67+
address: () => ({ address: '127.0.0.1', port: 12345, family: 'IPv4' }),
68+
} as unknown as http.Server)
69+
}
70+
71+
describe('OAuthClient helpers', () => {
72+
it('computeKey() generates deterministic SHA-256 hex', () => {
73+
const url = new URL('https://example.com/api')
74+
const expected = crypto
75+
.createHash('sha256')
76+
.update(url.origin + url.pathname)
77+
.digest('hex')
78+
const actual = (OAuthClient as any).computeKey(url)
79+
expect(actual).to.equal(expected)
80+
})
81+
82+
it('b64url() strips padding and is URL-safe', () => {
83+
const buf = Buffer.from('hello')
84+
const actual = (OAuthClient as any).b64url(buf)
85+
expect(actual).to.equal('aGVsbG8')
86+
})
87+
})
88+
89+
describe('OAuthClient getValidAccessToken()', () => {
90+
const now = Date.now()
91+
92+
beforeEach(() => {
93+
sinon.restore()
94+
OAuthClient.initialize(fakeWorkspace, fakeLogger as any)
95+
sinon.stub(OAuthClient as any, 'computeKey').returns('testkey')
96+
stubHttpServer()
97+
})
98+
99+
afterEach(() => sinon.restore())
100+
101+
it('returns cached token when still valid', async () => {
102+
const cachedToken = {
103+
access_token: 'cached_access',
104+
expires_in: 3600,
105+
obtained_at: now - 1_000,
106+
}
107+
const cachedReg = {
108+
client_id: 'cid',
109+
redirect_uri: 'http://localhost:12345',
110+
}
111+
112+
stubFileSystem(cachedToken, cachedReg)
113+
114+
const token = await OAuthClient.getValidAccessToken(new URL('https://api.example.com/mcp'))
115+
expect(token).to.equal('cached_access')
116+
expect((http.createServer as any).calledOnce).to.be.true
117+
})
118+
})

0 commit comments

Comments
 (0)