Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/common/extHostMcp.test.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as assert from 'assert';
7
import * as sinon from 'sinon';
8
import { LogLevel } from '../../../../platform/log/common/log.js';
9
import { createAuthMetadata, CommonResponse, IAuthMetadata } from '../../common/extHostMcp.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
11
12
// Test constants to avoid magic strings
13
const TEST_MCP_URL = 'https://example.com/mcp';
14
const TEST_AUTH_SERVER = 'https://auth.example.com';
15
const TEST_RESOURCE_METADATA_URL = 'https://example.com/.well-known/oauth-protected-resource';
16
17
/**
18
* Creates a mock CommonResponse for testing.
19
*/
20
function createMockResponse(options: {
21
status?: number;
22
statusText?: string;
23
url?: string;
24
headers?: Record<string, string>;
25
body?: string;
26
}): CommonResponse {
27
const headers = new Headers(options.headers ?? {});
28
return {
29
status: options.status ?? 200,
30
statusText: options.statusText ?? 'OK',
31
url: options.url ?? TEST_MCP_URL,
32
headers,
33
body: null,
34
json: async () => JSON.parse(options.body ?? '{}'),
35
text: async () => options.body ?? '',
36
};
37
}
38
39
/**
40
* Helper to create an IAuthMetadata instance for testing via the factory function.
41
* Uses a mock fetch that returns the provided server metadata.
42
*/
43
async function createTestAuthMetadata(options: {
44
scopes?: string[];
45
serverMetadataIssuer?: string;
46
resourceMetadata?: { resource: string; authorization_servers?: string[]; scopes_supported?: string[] };
47
}): Promise<{ authMetadata: IAuthMetadata; logMessages: Array<{ level: LogLevel; message: string }> }> {
48
const logMessages: Array<{ level: LogLevel; message: string }> = [];
49
const mockLogger = (level: LogLevel, message: string) => logMessages.push({ level, message });
50
51
const issuer = options.serverMetadataIssuer ?? TEST_AUTH_SERVER;
52
53
const mockFetch = sinon.stub();
54
55
// Mock resource metadata fetch
56
mockFetch.onCall(0).resolves(createMockResponse({
57
status: 200,
58
url: TEST_RESOURCE_METADATA_URL,
59
body: JSON.stringify(options.resourceMetadata ?? {
60
resource: TEST_MCP_URL,
61
authorization_servers: [issuer]
62
})
63
}));
64
65
// Mock server metadata fetch
66
mockFetch.onCall(1).resolves(createMockResponse({
67
status: 200,
68
url: `${issuer}/.well-known/oauth-authorization-server`,
69
body: JSON.stringify({
70
issuer,
71
authorization_endpoint: `${issuer}/authorize`,
72
token_endpoint: `${issuer}/token`,
73
response_types_supported: ['code']
74
})
75
}));
76
77
const wwwAuthHeader = options.scopes
78
? `Bearer scope="${options.scopes.join(' ')}"`
79
: 'Bearer realm="example"';
80
81
const originalResponse = createMockResponse({
82
status: 401,
83
url: TEST_MCP_URL,
84
headers: {
85
'WWW-Authenticate': wwwAuthHeader
86
}
87
});
88
89
const authMetadata = await createAuthMetadata(
90
TEST_MCP_URL,
91
originalResponse.headers,
92
{
93
sameOriginHeaders: {},
94
fetch: mockFetch,
95
log: mockLogger
96
}
97
);
98
99
return { authMetadata, logMessages };
100
}
101
102
suite('ExtHostMcp', () => {
103
ensureNoDisposablesAreLeakedInTestSuite();
104
105
suite('IAuthMetadata', () => {
106
suite('properties', () => {
107
test('should expose readonly properties', async () => {
108
const { authMetadata } = await createTestAuthMetadata({
109
scopes: ['read', 'write'],
110
serverMetadataIssuer: TEST_AUTH_SERVER
111
});
112
113
assert.ok(authMetadata.authorizationServer.toString().startsWith(TEST_AUTH_SERVER));
114
assert.strictEqual(authMetadata.serverMetadata.issuer, TEST_AUTH_SERVER);
115
assert.deepStrictEqual(authMetadata.scopes, ['read', 'write']);
116
});
117
118
test('should allow undefined scopes', async () => {
119
const { authMetadata } = await createTestAuthMetadata({
120
scopes: undefined
121
});
122
123
assert.strictEqual(authMetadata.scopes, undefined);
124
});
125
});
126
127
suite('update()', () => {
128
test('should return true and update scopes when WWW-Authenticate header contains new scopes', async () => {
129
const { authMetadata } = await createTestAuthMetadata({
130
scopes: ['read']
131
});
132
133
const response = createMockResponse({
134
status: 401,
135
headers: {
136
'WWW-Authenticate': 'Bearer scope="read write admin"'
137
}
138
});
139
140
const result = authMetadata.update(response.headers);
141
142
assert.strictEqual(result, true);
143
assert.deepStrictEqual(authMetadata.scopes, ['read', 'write', 'admin']);
144
});
145
146
test('should return false when scopes are the same', async () => {
147
const { authMetadata } = await createTestAuthMetadata({
148
scopes: ['read', 'write']
149
});
150
151
const response = createMockResponse({
152
status: 401,
153
headers: {
154
'WWW-Authenticate': 'Bearer scope="read write"'
155
}
156
});
157
158
const result = authMetadata.update(response.headers);
159
160
assert.strictEqual(result, false);
161
assert.deepStrictEqual(authMetadata.scopes, ['read', 'write']);
162
});
163
164
test('should return false when scopes are same but in different order', async () => {
165
const { authMetadata } = await createTestAuthMetadata({
166
scopes: ['read', 'write']
167
});
168
169
const response = createMockResponse({
170
status: 401,
171
headers: {
172
'WWW-Authenticate': 'Bearer scope="write read"'
173
}
174
});
175
176
const result = authMetadata.update(response.headers);
177
178
assert.strictEqual(result, false);
179
});
180
181
test('should return true when updating from undefined scopes to defined scopes', async () => {
182
const { authMetadata } = await createTestAuthMetadata({
183
scopes: undefined
184
});
185
186
const response = createMockResponse({
187
status: 401,
188
headers: {
189
'WWW-Authenticate': 'Bearer scope="read"'
190
}
191
});
192
193
const result = authMetadata.update(response.headers);
194
195
assert.strictEqual(result, true);
196
assert.deepStrictEqual(authMetadata.scopes, ['read']);
197
});
198
199
test('should return true when updating from defined scopes to undefined (no scope in header)', async () => {
200
const { authMetadata } = await createTestAuthMetadata({
201
scopes: ['read']
202
});
203
204
const response = createMockResponse({
205
status: 401,
206
headers: {
207
'WWW-Authenticate': 'Bearer realm="example"'
208
}
209
});
210
211
const result = authMetadata.update(response.headers);
212
213
assert.strictEqual(result, true);
214
assert.strictEqual(authMetadata.scopes, undefined);
215
});
216
217
test('should return false when no WWW-Authenticate header and scopes are already undefined', async () => {
218
const { authMetadata } = await createTestAuthMetadata({
219
scopes: undefined
220
});
221
222
const response = createMockResponse({
223
status: 401,
224
headers: {}
225
});
226
227
const result = authMetadata.update(response.headers);
228
229
assert.strictEqual(result, false);
230
});
231
232
test('should handle multiple Bearer challenges and use first scope', async () => {
233
const { authMetadata } = await createTestAuthMetadata({
234
scopes: undefined
235
});
236
237
const response = createMockResponse({
238
status: 401,
239
headers: {
240
'WWW-Authenticate': 'Bearer scope="first", Bearer scope="second"'
241
}
242
});
243
244
authMetadata.update(response.headers);
245
246
assert.deepStrictEqual(authMetadata.scopes, ['first']);
247
});
248
249
test('should ignore non-Bearer schemes', async () => {
250
const { authMetadata } = await createTestAuthMetadata({
251
scopes: undefined
252
});
253
254
const response = createMockResponse({
255
status: 401,
256
headers: {
257
'WWW-Authenticate': 'Basic realm="example"'
258
}
259
});
260
261
const result = authMetadata.update(response.headers);
262
263
assert.strictEqual(result, false);
264
assert.strictEqual(authMetadata.scopes, undefined);
265
});
266
});
267
});
268
269
suite('createAuthMetadata', () => {
270
let sandbox: sinon.SinonSandbox;
271
let logMessages: Array<{ level: LogLevel; message: string }>;
272
let mockLogger: (level: LogLevel, message: string) => void;
273
274
setup(() => {
275
sandbox = sinon.createSandbox();
276
logMessages = [];
277
mockLogger = (level, message) => logMessages.push({ level, message });
278
});
279
280
teardown(() => {
281
sandbox.restore();
282
});
283
284
test('should create IAuthMetadata with fetched server metadata', async () => {
285
const mockFetch = sandbox.stub();
286
287
// Mock resource metadata fetch
288
mockFetch.onCall(0).resolves(createMockResponse({
289
status: 200,
290
url: TEST_RESOURCE_METADATA_URL,
291
body: JSON.stringify({
292
resource: TEST_MCP_URL,
293
authorization_servers: [TEST_AUTH_SERVER],
294
scopes_supported: ['read', 'write']
295
})
296
}));
297
298
// Mock server metadata fetch
299
mockFetch.onCall(1).resolves(createMockResponse({
300
status: 200,
301
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
302
body: JSON.stringify({
303
issuer: TEST_AUTH_SERVER,
304
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
305
token_endpoint: `${TEST_AUTH_SERVER}/token`,
306
response_types_supported: ['code']
307
})
308
}));
309
310
const originalResponse = createMockResponse({
311
status: 401,
312
url: TEST_MCP_URL,
313
headers: {
314
'WWW-Authenticate': 'Bearer scope="api.read"'
315
}
316
});
317
318
const authMetadata = await createAuthMetadata(
319
TEST_MCP_URL,
320
originalResponse.headers,
321
{
322
sameOriginHeaders: { 'X-Custom': 'value' },
323
fetch: mockFetch,
324
log: mockLogger
325
}
326
);
327
328
assert.ok(authMetadata.authorizationServer.toString().startsWith(TEST_AUTH_SERVER));
329
assert.strictEqual(authMetadata.serverMetadata.issuer, TEST_AUTH_SERVER);
330
assert.deepStrictEqual(authMetadata.scopes, ['api.read']);
331
});
332
333
test('should fall back to default metadata when server metadata fetch fails', async () => {
334
const mockFetch = sandbox.stub();
335
336
// Mock resource metadata fetch - fails
337
mockFetch.onCall(0).rejects(new Error('Network error'));
338
339
// Mock server metadata fetch - also fails
340
mockFetch.onCall(1).rejects(new Error('Network error'));
341
342
const originalResponse = createMockResponse({
343
status: 401,
344
url: TEST_MCP_URL,
345
headers: {}
346
});
347
348
const authMetadata = await createAuthMetadata(
349
TEST_MCP_URL,
350
originalResponse.headers,
351
{
352
sameOriginHeaders: {},
353
fetch: mockFetch,
354
log: mockLogger
355
}
356
);
357
358
// Should use default metadata based on the URL
359
assert.ok(authMetadata.authorizationServer.toString().startsWith('https://example.com'));
360
assert.ok(authMetadata.serverMetadata.issuer.startsWith('https://example.com'));
361
assert.ok(authMetadata.serverMetadata.authorization_endpoint?.startsWith('https://example.com/authorize'));
362
assert.ok(authMetadata.serverMetadata.token_endpoint?.startsWith('https://example.com/token'));
363
364
// Should log the fallback
365
assert.ok(logMessages.some(m =>
366
m.level === LogLevel.Info &&
367
m.message.includes('Using default auth metadata')
368
));
369
});
370
371
test('should use scopes from WWW-Authenticate header when resource metadata has none', async () => {
372
const mockFetch = sandbox.stub();
373
374
// Mock resource metadata fetch - no scopes_supported
375
mockFetch.onCall(0).resolves(createMockResponse({
376
status: 200,
377
url: TEST_RESOURCE_METADATA_URL,
378
body: JSON.stringify({
379
resource: TEST_MCP_URL,
380
authorization_servers: [TEST_AUTH_SERVER]
381
})
382
}));
383
384
// Mock server metadata fetch
385
mockFetch.onCall(1).resolves(createMockResponse({
386
status: 200,
387
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
388
body: JSON.stringify({
389
issuer: TEST_AUTH_SERVER,
390
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
391
token_endpoint: `${TEST_AUTH_SERVER}/token`,
392
response_types_supported: ['code']
393
})
394
}));
395
396
const originalResponse = createMockResponse({
397
status: 401,
398
url: TEST_MCP_URL,
399
headers: {
400
'WWW-Authenticate': 'Bearer scope="header.scope"'
401
}
402
});
403
404
const authMetadata = await createAuthMetadata(
405
TEST_MCP_URL,
406
originalResponse.headers,
407
{
408
sameOriginHeaders: {},
409
fetch: mockFetch,
410
log: mockLogger
411
}
412
);
413
414
assert.deepStrictEqual(authMetadata.scopes, ['header.scope']);
415
});
416
417
test('should use scopes from WWW-Authenticate header even when resource metadata has scopes_supported', async () => {
418
const mockFetch = sandbox.stub();
419
420
// Mock resource metadata fetch - has scopes_supported
421
mockFetch.onCall(0).resolves(createMockResponse({
422
status: 200,
423
url: TEST_RESOURCE_METADATA_URL,
424
body: JSON.stringify({
425
resource: TEST_MCP_URL,
426
authorization_servers: [TEST_AUTH_SERVER],
427
scopes_supported: ['resource.scope1', 'resource.scope2']
428
})
429
}));
430
431
// Mock server metadata fetch
432
mockFetch.onCall(1).resolves(createMockResponse({
433
status: 200,
434
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
435
body: JSON.stringify({
436
issuer: TEST_AUTH_SERVER,
437
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
438
token_endpoint: `${TEST_AUTH_SERVER}/token`,
439
response_types_supported: ['code']
440
})
441
}));
442
443
const originalResponse = createMockResponse({
444
status: 401,
445
url: TEST_MCP_URL,
446
headers: {
447
'WWW-Authenticate': 'Bearer scope="header.scope"'
448
}
449
});
450
451
const authMetadata = await createAuthMetadata(
452
TEST_MCP_URL,
453
originalResponse.headers,
454
{
455
sameOriginHeaders: {},
456
fetch: mockFetch,
457
log: mockLogger
458
}
459
);
460
461
// WWW-Authenticate header scopes take precedence over resource metadata scopes_supported
462
assert.deepStrictEqual(authMetadata.scopes, ['header.scope']);
463
});
464
465
test('should use resource_metadata challenge URL from WWW-Authenticate header', async () => {
466
const mockFetch = sandbox.stub();
467
468
// Mock resource metadata fetch from challenge URL
469
mockFetch.onCall(0).resolves(createMockResponse({
470
status: 200,
471
url: 'https://example.com/custom-resource-metadata',
472
body: JSON.stringify({
473
resource: TEST_MCP_URL,
474
authorization_servers: [TEST_AUTH_SERVER]
475
})
476
}));
477
478
// Mock server metadata fetch
479
mockFetch.onCall(1).resolves(createMockResponse({
480
status: 200,
481
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
482
body: JSON.stringify({
483
issuer: TEST_AUTH_SERVER,
484
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
485
token_endpoint: `${TEST_AUTH_SERVER}/token`,
486
response_types_supported: ['code']
487
})
488
}));
489
490
const originalResponse = createMockResponse({
491
status: 401,
492
url: TEST_MCP_URL,
493
headers: {
494
'WWW-Authenticate': 'Bearer resource_metadata="https://example.com/custom-resource-metadata"'
495
}
496
});
497
498
const authMetadata = await createAuthMetadata(
499
TEST_MCP_URL,
500
originalResponse.headers,
501
{
502
sameOriginHeaders: {},
503
fetch: mockFetch,
504
log: mockLogger
505
}
506
);
507
508
assert.ok(authMetadata.authorizationServer.toString().startsWith(TEST_AUTH_SERVER));
509
510
// Verify the resource_metadata URL was logged
511
assert.ok(logMessages.some(m =>
512
m.level === LogLevel.Debug &&
513
m.message.includes('resource_metadata challenge')
514
));
515
});
516
517
test('should pass launch headers when fetching metadata from same origin', async () => {
518
const mockFetch = sandbox.stub();
519
520
// Mock resource metadata fetch to succeed so we can verify headers
521
mockFetch.onCall(0).resolves(createMockResponse({
522
status: 200,
523
url: TEST_RESOURCE_METADATA_URL,
524
body: JSON.stringify({
525
resource: TEST_MCP_URL,
526
authorization_servers: [TEST_AUTH_SERVER]
527
})
528
}));
529
530
// Mock server metadata fetch
531
mockFetch.onCall(1).resolves(createMockResponse({
532
status: 200,
533
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
534
body: JSON.stringify({
535
issuer: TEST_AUTH_SERVER,
536
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
537
token_endpoint: `${TEST_AUTH_SERVER}/token`,
538
response_types_supported: ['code']
539
})
540
}));
541
542
const originalResponse = createMockResponse({
543
status: 401,
544
url: TEST_MCP_URL,
545
headers: {}
546
});
547
548
const launchHeaders = {
549
'Authorization': 'Bearer existing-token',
550
'X-Custom-Header': 'custom-value'
551
};
552
553
await createAuthMetadata(
554
TEST_MCP_URL,
555
originalResponse.headers,
556
{
557
sameOriginHeaders: launchHeaders,
558
fetch: mockFetch,
559
log: mockLogger
560
}
561
);
562
563
// Verify fetch was called
564
assert.ok(mockFetch.called, 'fetch should have been called');
565
566
// Verify the first call (resource metadata) included the launch headers
567
const firstCallArgs = mockFetch.firstCall.args;
568
assert.ok(firstCallArgs.length >= 2, 'fetch should have been called with options');
569
const fetchOptions = firstCallArgs[1] as RequestInit;
570
assert.ok(fetchOptions.headers, 'fetch options should include headers');
571
});
572
573
test('should handle empty scope string in WWW-Authenticate header', async () => {
574
const mockFetch = sandbox.stub();
575
576
// Mock resource metadata fetch
577
mockFetch.onCall(0).resolves(createMockResponse({
578
status: 200,
579
url: TEST_RESOURCE_METADATA_URL,
580
body: JSON.stringify({
581
resource: TEST_MCP_URL,
582
authorization_servers: [TEST_AUTH_SERVER]
583
})
584
}));
585
586
// Mock server metadata fetch
587
mockFetch.onCall(1).resolves(createMockResponse({
588
status: 200,
589
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
590
body: JSON.stringify({
591
issuer: TEST_AUTH_SERVER,
592
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
593
token_endpoint: `${TEST_AUTH_SERVER}/token`,
594
response_types_supported: ['code']
595
})
596
}));
597
598
const originalResponse = createMockResponse({
599
status: 401,
600
url: TEST_MCP_URL,
601
headers: {
602
'WWW-Authenticate': 'Bearer scope=""'
603
}
604
});
605
606
const authMetadata = await createAuthMetadata(
607
TEST_MCP_URL,
608
originalResponse.headers,
609
{
610
sameOriginHeaders: {},
611
fetch: mockFetch,
612
log: mockLogger
613
}
614
);
615
616
// Empty scope string should result in empty array or undefined
617
assert.ok(
618
authMetadata.scopes === undefined ||
619
(Array.isArray(authMetadata.scopes) && authMetadata.scopes.length === 0) ||
620
(Array.isArray(authMetadata.scopes) && authMetadata.scopes.every(s => s === '')),
621
'Empty scope string should be handled gracefully'
622
);
623
});
624
625
test('should handle malformed WWW-Authenticate header gracefully', async () => {
626
const mockFetch = sandbox.stub();
627
628
// Mock resource metadata fetch
629
mockFetch.onCall(0).resolves(createMockResponse({
630
status: 200,
631
url: TEST_RESOURCE_METADATA_URL,
632
body: JSON.stringify({
633
resource: TEST_MCP_URL,
634
authorization_servers: [TEST_AUTH_SERVER]
635
})
636
}));
637
638
// Mock server metadata fetch
639
mockFetch.onCall(1).resolves(createMockResponse({
640
status: 200,
641
url: `${TEST_AUTH_SERVER}/.well-known/oauth-authorization-server`,
642
body: JSON.stringify({
643
issuer: TEST_AUTH_SERVER,
644
authorization_endpoint: `${TEST_AUTH_SERVER}/authorize`,
645
token_endpoint: `${TEST_AUTH_SERVER}/token`,
646
response_types_supported: ['code']
647
})
648
}));
649
650
const originalResponse = createMockResponse({
651
status: 401,
652
url: TEST_MCP_URL,
653
headers: {
654
// Malformed header - missing closing quote
655
'WWW-Authenticate': 'Bearer scope="unclosed'
656
}
657
});
658
659
// Should not throw - should handle gracefully
660
const authMetadata = await createAuthMetadata(
661
TEST_MCP_URL,
662
originalResponse.headers,
663
{
664
sameOriginHeaders: {},
665
fetch: mockFetch,
666
log: mockLogger
667
}
668
);
669
670
// Should still create valid auth metadata
671
assert.ok(authMetadata.authorizationServer);
672
assert.ok(authMetadata.serverMetadata);
673
});
674
675
test('should handle invalid JSON in resource metadata response', async () => {
676
const mockFetch = sandbox.stub();
677
678
// Mock resource metadata fetch - returns invalid JSON
679
mockFetch.onCall(0).resolves(createMockResponse({
680
status: 200,
681
url: TEST_RESOURCE_METADATA_URL,
682
body: 'not valid json {'
683
}));
684
685
// Mock server metadata fetch - also returns invalid JSON
686
mockFetch.onCall(1).resolves(createMockResponse({
687
status: 200,
688
url: 'https://example.com/.well-known/oauth-authorization-server',
689
body: '{ invalid }'
690
}));
691
692
const originalResponse = createMockResponse({
693
status: 401,
694
url: TEST_MCP_URL,
695
headers: {}
696
});
697
698
// Should fall back to default metadata, not throw
699
const authMetadata = await createAuthMetadata(
700
TEST_MCP_URL,
701
originalResponse.headers,
702
{
703
sameOriginHeaders: {},
704
fetch: mockFetch,
705
log: mockLogger
706
}
707
);
708
709
// Should use default metadata
710
assert.ok(authMetadata.authorizationServer);
711
assert.ok(authMetadata.serverMetadata);
712
});
713
714
test('should handle non-401 status codes in update()', async () => {
715
const { authMetadata } = await createTestAuthMetadata({
716
scopes: ['read']
717
});
718
719
// Response with 403 instead of 401
720
const response = createMockResponse({
721
status: 403,
722
headers: {
723
'WWW-Authenticate': 'Bearer scope="new.scope"'
724
}
725
});
726
727
// update() should still process the WWW-Authenticate header regardless of status
728
const result = authMetadata.update(response.headers);
729
730
// The behavior depends on implementation - either it updates or ignores non-401
731
// This test documents the actual behavior
732
assert.strictEqual(typeof result, 'boolean');
733
});
734
});
735
});
736
737
738