Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts
5220 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 assert from 'assert';
7
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
8
import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';
9
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
10
import { INotificationService } from '../../../../platform/notification/common/notification.js';
11
import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';
12
import { IQuickInputHideEvent, IQuickInputService, IQuickPickDidAcceptEvent, IQuickPickItem, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js';
13
import { IStorageService } from '../../../../platform/storage/common/storage.js';
14
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
15
import { NullTelemetryService } from '../../../../platform/telemetry/common/telemetryUtils.js';
16
import { MainThreadAuthentication } from '../../browser/mainThreadAuthentication.js';
17
import { ExtHostContext, MainContext } from '../../common/extHost.protocol.js';
18
import { ExtHostAuthentication } from '../../common/extHostAuthentication.js';
19
import { IActivityService } from '../../../services/activity/common/activity.js';
20
import { AuthenticationService } from '../../../services/authentication/browser/authenticationService.js';
21
import { IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
22
import { IExtensionService, nullExtensionDescription as extensionDescription } from '../../../services/extensions/common/extensions.js';
23
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
24
import { TestRPCProtocol } from '../common/testRPCProtocol.js';
25
import { TestEnvironmentService, TestHostService, TestQuickInputService, TestRemoteAgentService } from '../../../test/browser/workbenchTestServices.js';
26
import { TestActivityService, TestExtensionService, TestLoggerService, TestProductService, TestStorageService } from '../../../test/common/workbenchTestServices.js';
27
import type { AuthenticationProvider, AuthenticationSession } from 'vscode';
28
import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';
29
import { IProductService } from '../../../../platform/product/common/productService.js';
30
import { AuthenticationAccessService, IAuthenticationAccessService } from '../../../services/authentication/browser/authenticationAccessService.js';
31
import { IAccountUsage, IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js';
32
import { AuthenticationExtensionsService } from '../../../services/authentication/browser/authenticationExtensionsService.js';
33
import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';
34
import { IExtHostInitDataService } from '../../common/extHostInitDataService.js';
35
import { ExtHostWindow } from '../../common/extHostWindow.js';
36
import { MainThreadWindow } from '../../browser/mainThreadWindow.js';
37
import { IHostService } from '../../../services/host/browser/host.js';
38
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
39
import { IUserActivityService, UserActivityService } from '../../../services/userActivity/common/userActivityService.js';
40
import { ExtHostUrls } from '../../common/extHostUrls.js';
41
import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
42
import { TestSecretStorageService } from '../../../../platform/secrets/test/common/testSecretStorageService.js';
43
import { IDynamicAuthenticationProviderStorageService } from '../../../services/authentication/common/dynamicAuthenticationProviderStorage.js';
44
import { DynamicAuthenticationProviderStorageService } from '../../../services/authentication/browser/dynamicAuthenticationProviderStorageService.js';
45
import { ExtHostProgress } from '../../common/extHostProgress.js';
46
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
47
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
48
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
49
50
class AuthQuickPick {
51
private accept: ((e: IQuickPickDidAcceptEvent) => any) | undefined;
52
private hide: ((e: IQuickInputHideEvent) => any) | undefined;
53
public items = [];
54
public get selectedItems(): IQuickPickItem[] {
55
return this.items;
56
}
57
58
onDidAccept(listener: (e: IQuickPickDidAcceptEvent) => any) {
59
this.accept = listener;
60
}
61
onDidHide(listener: (e: IQuickInputHideEvent) => any) {
62
this.hide = listener;
63
}
64
65
dispose() {
66
67
}
68
show() {
69
this.accept?.({ inBackground: false });
70
this.hide?.({ reason: QuickInputHideReason.Other });
71
}
72
}
73
class AuthTestQuickInputService extends TestQuickInputService {
74
override createQuickPick() {
75
// eslint-disable-next-line local/code-no-any-casts
76
return <any>new AuthQuickPick();
77
}
78
}
79
80
class TestAuthUsageService implements IAuthenticationUsageService {
81
_serviceBrand: undefined;
82
initializeExtensionUsageCache(): Promise<void> { return Promise.resolve(); }
83
extensionUsesAuth(extensionId: string): Promise<boolean> { return Promise.resolve(false); }
84
readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { return []; }
85
removeAccountUsage(providerId: string, accountName: string): void { }
86
addAccountUsage(providerId: string, accountName: string, scopes: ReadonlyArray<string>, extensionId: string, extensionName: string): void { }
87
}
88
89
class TestAuthProvider implements AuthenticationProvider {
90
private id = 1;
91
private sessions = new Map<string, AuthenticationSession>();
92
onDidChangeSessions = () => { return { dispose() { } }; };
93
constructor(private readonly authProviderName: string) { }
94
async getSessions(scopes?: readonly string[]): Promise<AuthenticationSession[]> {
95
if (!scopes) {
96
return [...this.sessions.values()];
97
}
98
99
if (scopes[0] === 'return multiple') {
100
return [...this.sessions.values()];
101
}
102
const sessions = this.sessions.get(scopes.join(' '));
103
return sessions ? [sessions] : [];
104
}
105
async createSession(scopes: readonly string[]): Promise<AuthenticationSession> {
106
const scopesStr = scopes.join(' ');
107
const session = {
108
scopes,
109
id: `${this.id}`,
110
account: {
111
label: this.authProviderName,
112
id: `${this.id}`,
113
},
114
accessToken: Math.random() + '',
115
};
116
this.sessions.set(scopesStr, session);
117
this.id++;
118
return session;
119
}
120
async removeSession(sessionId: string): Promise<void> {
121
this.sessions.delete(sessionId);
122
}
123
124
}
125
126
suite('ExtHostAuthentication', () => {
127
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
128
129
let extHostAuthentication: ExtHostAuthentication;
130
let mainInstantiationService: TestInstantiationService;
131
132
setup(async () => {
133
// services
134
const services = new ServiceCollection();
135
services.set(ILogService, new SyncDescriptor(NullLogService));
136
services.set(IDialogService, new SyncDescriptor(TestDialogService, [{ confirmed: true }]));
137
services.set(IStorageService, new SyncDescriptor(TestStorageService));
138
services.set(ISecretStorageService, new SyncDescriptor(TestSecretStorageService));
139
services.set(IDynamicAuthenticationProviderStorageService, new SyncDescriptor(DynamicAuthenticationProviderStorageService));
140
services.set(IQuickInputService, new SyncDescriptor(AuthTestQuickInputService));
141
services.set(IExtensionService, new SyncDescriptor(TestExtensionService));
142
services.set(IActivityService, new SyncDescriptor(TestActivityService));
143
services.set(IRemoteAgentService, new SyncDescriptor(TestRemoteAgentService));
144
services.set(INotificationService, new SyncDescriptor(TestNotificationService));
145
services.set(IHostService, new SyncDescriptor(TestHostService));
146
services.set(IUserActivityService, new SyncDescriptor(UserActivityService));
147
services.set(IAuthenticationAccessService, new SyncDescriptor(AuthenticationAccessService));
148
services.set(IAuthenticationService, new SyncDescriptor(AuthenticationService));
149
services.set(IAuthenticationUsageService, new SyncDescriptor(TestAuthUsageService));
150
services.set(IAuthenticationExtensionsService, new SyncDescriptor(AuthenticationExtensionsService));
151
mainInstantiationService = disposables.add(new TestInstantiationService(services, undefined, undefined, true));
152
153
// stubs
154
// eslint-disable-next-line local/code-no-dangerous-type-assertions
155
mainInstantiationService.stub(IOpenerService, {} as Partial<IOpenerService>);
156
mainInstantiationService.stub(ITelemetryService, NullTelemetryService);
157
mainInstantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService);
158
mainInstantiationService.stub(IProductService, TestProductService);
159
160
const rpcProtocol = disposables.add(new TestRPCProtocol());
161
162
rpcProtocol.set(MainContext.MainThreadAuthentication, disposables.add(mainInstantiationService.createInstance(MainThreadAuthentication, rpcProtocol)));
163
rpcProtocol.set(MainContext.MainThreadWindow, disposables.add(mainInstantiationService.createInstance(MainThreadWindow, rpcProtocol)));
164
// eslint-disable-next-line local/code-no-any-casts
165
const initData: IExtHostInitDataService = {
166
environment: {
167
appUriScheme: 'test',
168
appName: 'Test'
169
}
170
} as any;
171
extHostAuthentication = new ExtHostAuthentication(
172
rpcProtocol,
173
// eslint-disable-next-line local/code-no-any-casts
174
{
175
environment: {
176
appUriScheme: 'test',
177
appName: 'Test'
178
}
179
} as any,
180
new ExtHostWindow(initData, rpcProtocol),
181
new ExtHostUrls(rpcProtocol),
182
new ExtHostProgress(rpcProtocol),
183
disposables.add(new TestLoggerService()),
184
new NullLogService()
185
);
186
rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication);
187
disposables.add(extHostAuthentication.registerAuthenticationProvider('test', 'test provider', new TestAuthProvider('test')));
188
disposables.add(extHostAuthentication.registerAuthenticationProvider(
189
'test-multiple',
190
'test multiple provider',
191
new TestAuthProvider('test-multiple'),
192
{ supportsMultipleAccounts: true }));
193
});
194
195
test('createIfNone - true', async () => {
196
const scopes = ['foo'];
197
const session = await extHostAuthentication.getSession(
198
extensionDescription,
199
'test',
200
scopes,
201
{
202
createIfNone: true
203
});
204
assert.strictEqual(session?.id, '1');
205
assert.strictEqual(session?.scopes[0], 'foo');
206
});
207
208
test('createIfNone - false', async () => {
209
const scopes = ['foo'];
210
const nosession = await extHostAuthentication.getSession(
211
extensionDescription,
212
'test',
213
scopes,
214
{});
215
assert.strictEqual(nosession, undefined);
216
217
// Now create the session
218
const session = await extHostAuthentication.getSession(
219
extensionDescription,
220
'test',
221
scopes,
222
{
223
createIfNone: true
224
});
225
226
assert.strictEqual(session?.id, '1');
227
assert.strictEqual(session?.scopes[0], 'foo');
228
229
const session2 = await extHostAuthentication.getSession(
230
extensionDescription,
231
'test',
232
scopes,
233
{});
234
235
assert.strictEqual(session2?.id, session.id);
236
assert.strictEqual(session2?.scopes[0], session.scopes[0]);
237
assert.strictEqual(session2?.accessToken, session.accessToken);
238
});
239
240
// should behave the same as createIfNone: false
241
test('silent - true', async () => {
242
const scopes = ['foo'];
243
const nosession = await extHostAuthentication.getSession(
244
extensionDescription,
245
'test',
246
scopes,
247
{
248
silent: true
249
});
250
assert.strictEqual(nosession, undefined);
251
252
// Now create the session
253
const session = await extHostAuthentication.getSession(
254
extensionDescription,
255
'test',
256
scopes,
257
{
258
createIfNone: true
259
});
260
261
assert.strictEqual(session?.id, '1');
262
assert.strictEqual(session?.scopes[0], 'foo');
263
264
const session2 = await extHostAuthentication.getSession(
265
extensionDescription,
266
'test',
267
scopes,
268
{
269
silent: true
270
});
271
272
assert.strictEqual(session.id, session2?.id);
273
assert.strictEqual(session.scopes[0], session2?.scopes[0]);
274
});
275
276
test('forceNewSession - true - existing session', async () => {
277
const scopes = ['foo'];
278
const session1 = await extHostAuthentication.getSession(
279
extensionDescription,
280
'test',
281
scopes,
282
{
283
createIfNone: true
284
});
285
286
// Now create the session
287
const session2 = await extHostAuthentication.getSession(
288
extensionDescription,
289
'test',
290
scopes,
291
{
292
forceNewSession: true
293
});
294
295
assert.strictEqual(session2?.id, '2');
296
assert.strictEqual(session2?.scopes[0], 'foo');
297
assert.notStrictEqual(session1.accessToken, session2?.accessToken);
298
});
299
300
// Should behave like createIfNone: true
301
test('forceNewSession - true - no existing session', async () => {
302
const scopes = ['foo'];
303
const session = await extHostAuthentication.getSession(
304
extensionDescription,
305
'test',
306
scopes,
307
{
308
forceNewSession: true
309
});
310
assert.strictEqual(session?.id, '1');
311
assert.strictEqual(session?.scopes[0], 'foo');
312
});
313
314
test('forceNewSession - detail', async () => {
315
const scopes = ['foo'];
316
const session1 = await extHostAuthentication.getSession(
317
extensionDescription,
318
'test',
319
scopes,
320
{
321
createIfNone: true
322
});
323
324
// Now create the session
325
const session2 = await extHostAuthentication.getSession(
326
extensionDescription,
327
'test',
328
scopes,
329
{
330
forceNewSession: { detail: 'bar' }
331
});
332
333
assert.strictEqual(session2?.id, '2');
334
assert.strictEqual(session2?.scopes[0], 'foo');
335
assert.notStrictEqual(session1.accessToken, session2?.accessToken);
336
});
337
338
//#region Multi-Account AuthProvider
339
340
test('clearSessionPreference - true', async () => {
341
const scopes = ['foo'];
342
// Now create the session
343
const session = await extHostAuthentication.getSession(
344
extensionDescription,
345
'test-multiple',
346
scopes,
347
{
348
createIfNone: true
349
});
350
351
assert.strictEqual(session?.id, '1');
352
assert.strictEqual(session?.scopes[0], scopes[0]);
353
354
const scopes2 = ['bar'];
355
const session2 = await extHostAuthentication.getSession(
356
extensionDescription,
357
'test-multiple',
358
scopes2,
359
{
360
createIfNone: true
361
});
362
assert.strictEqual(session2?.id, '2');
363
assert.strictEqual(session2?.scopes[0], scopes2[0]);
364
365
const session3 = await extHostAuthentication.getSession(
366
extensionDescription,
367
'test-multiple',
368
['return multiple'],
369
{
370
clearSessionPreference: true,
371
createIfNone: true
372
});
373
374
// clearing session preference causes us to get the first session
375
// because it would normally show a quick pick for the user to choose
376
assert.strictEqual(session3?.id, session.id);
377
assert.strictEqual(session3?.scopes[0], session.scopes[0]);
378
assert.strictEqual(session3?.accessToken, session.accessToken);
379
});
380
381
test('silently getting session should return a session (if any) regardless of preference - fixes #137819', async () => {
382
const scopes = ['foo'];
383
// Now create the session
384
const session = await extHostAuthentication.getSession(
385
extensionDescription,
386
'test-multiple',
387
scopes,
388
{
389
createIfNone: true
390
});
391
392
assert.strictEqual(session?.id, '1');
393
assert.strictEqual(session?.scopes[0], scopes[0]);
394
395
const scopes2 = ['bar'];
396
const session2 = await extHostAuthentication.getSession(
397
extensionDescription,
398
'test-multiple',
399
scopes2,
400
{
401
createIfNone: true
402
});
403
assert.strictEqual(session2?.id, '2');
404
assert.strictEqual(session2?.scopes[0], scopes2[0]);
405
406
const shouldBeSession1 = await extHostAuthentication.getSession(
407
extensionDescription,
408
'test-multiple',
409
scopes,
410
{});
411
assert.strictEqual(shouldBeSession1?.id, session.id);
412
assert.strictEqual(shouldBeSession1?.scopes[0], session.scopes[0]);
413
assert.strictEqual(shouldBeSession1?.accessToken, session.accessToken);
414
415
const shouldBeSession2 = await extHostAuthentication.getSession(
416
extensionDescription,
417
'test-multiple',
418
scopes2,
419
{});
420
assert.strictEqual(shouldBeSession2?.id, session2.id);
421
assert.strictEqual(shouldBeSession2?.scopes[0], session2.scopes[0]);
422
assert.strictEqual(shouldBeSession2?.accessToken, session2.accessToken);
423
});
424
425
//#endregion
426
427
//#region error cases
428
429
test('createIfNone and forceNewSession', async () => {
430
try {
431
await extHostAuthentication.getSession(
432
extensionDescription,
433
'test',
434
['foo'],
435
{
436
createIfNone: true,
437
forceNewSession: true
438
});
439
assert.fail('should have thrown an Error.');
440
} catch (e) {
441
assert.ok(e);
442
}
443
});
444
445
test('forceNewSession and silent', async () => {
446
try {
447
await extHostAuthentication.getSession(
448
extensionDescription,
449
'test',
450
['foo'],
451
{
452
forceNewSession: true,
453
silent: true
454
});
455
assert.fail('should have thrown an Error.');
456
} catch (e) {
457
assert.ok(e);
458
}
459
});
460
461
test('createIfNone and silent', async () => {
462
try {
463
await extHostAuthentication.getSession(
464
extensionDescription,
465
'test',
466
['foo'],
467
{
468
createIfNone: true,
469
silent: true
470
});
471
assert.fail('should have thrown an Error.');
472
} catch (e) {
473
assert.ok(e);
474
}
475
});
476
477
test('Can get multiple sessions (with different scopes) in one extension', async () => {
478
let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(
479
extensionDescription,
480
'test-multiple',
481
['foo'],
482
{
483
createIfNone: true
484
});
485
session = await extHostAuthentication.getSession(
486
extensionDescription,
487
'test-multiple',
488
['bar'],
489
{
490
createIfNone: true
491
});
492
assert.strictEqual(session?.id, '2');
493
assert.strictEqual(session?.scopes[0], 'bar');
494
495
session = await extHostAuthentication.getSession(
496
extensionDescription,
497
'test-multiple',
498
['foo'],
499
{
500
createIfNone: false
501
});
502
assert.strictEqual(session?.id, '1');
503
assert.strictEqual(session?.scopes[0], 'foo');
504
});
505
506
test('Can get multiple sessions (from different providers) in one extension', async () => {
507
let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(
508
extensionDescription,
509
'test-multiple',
510
['foo'],
511
{
512
createIfNone: true
513
});
514
session = await extHostAuthentication.getSession(
515
extensionDescription,
516
'test',
517
['foo'],
518
{
519
createIfNone: true
520
});
521
assert.strictEqual(session?.id, '1');
522
assert.strictEqual(session?.scopes[0], 'foo');
523
assert.strictEqual(session?.account.label, 'test');
524
525
const session2 = await extHostAuthentication.getSession(
526
extensionDescription,
527
'test-multiple',
528
['foo'],
529
{
530
createIfNone: false
531
});
532
assert.strictEqual(session2?.id, '1');
533
assert.strictEqual(session2?.scopes[0], 'foo');
534
assert.strictEqual(session2?.account.label, 'test-multiple');
535
});
536
537
test('Can get multiple sessions (from different providers) in one extension at the same time', async () => {
538
const sessionP: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(
539
extensionDescription,
540
'test',
541
['foo'],
542
{
543
createIfNone: true
544
});
545
const session2P: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(
546
extensionDescription,
547
'test-multiple',
548
['foo'],
549
{
550
createIfNone: true
551
});
552
const session = await sessionP;
553
assert.strictEqual(session?.id, '1');
554
assert.strictEqual(session?.scopes[0], 'foo');
555
assert.strictEqual(session?.account.label, 'test');
556
557
const session2 = await session2P;
558
assert.strictEqual(session2?.id, '1');
559
assert.strictEqual(session2?.scopes[0], 'foo');
560
assert.strictEqual(session2?.account.label, 'test-multiple');
561
});
562
563
564
//#endregion
565
566
//#region Race Condition and Sequencing Tests
567
568
test('concurrent operations on same provider are serialized', async () => {
569
const provider = new TestAuthProvider('concurrent-test');
570
const operationOrder: string[] = [];
571
572
// Mock the provider methods to track operation order
573
const originalCreateSession = provider.createSession.bind(provider);
574
const originalGetSessions = provider.getSessions.bind(provider);
575
576
provider.createSession = async (scopes) => {
577
operationOrder.push(`create-start-${scopes[0]}`);
578
await new Promise(resolve => setTimeout(resolve, 20)); // Simulate async work
579
const result = await originalCreateSession(scopes);
580
operationOrder.push(`create-end-${scopes[0]}`);
581
return result;
582
};
583
584
provider.getSessions = async (scopes) => {
585
const scopeKey = scopes ? scopes[0] : 'all';
586
operationOrder.push(`get-start-${scopeKey}`);
587
await new Promise(resolve => setTimeout(resolve, 10)); // Simulate async work
588
const result = await originalGetSessions(scopes);
589
operationOrder.push(`get-end-${scopeKey}`);
590
return result;
591
};
592
593
const disposable = extHostAuthentication.registerAuthenticationProvider('concurrent-test', 'Concurrent Test', provider);
594
disposables.add(disposable);
595
596
// Start multiple operations simultaneously on the same provider
597
const promises = [
598
extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], { createIfNone: true }),
599
extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope2'], { createIfNone: true }),
600
extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], {}) // This should get the existing session
601
];
602
603
await Promise.all(promises);
604
605
// Verify that operations were serialized - no overlapping operations
606
// Build a map of operation starts to their corresponding ends
607
const operationPairs: Array<{ start: number; end: number; operation: string }> = [];
608
609
for (let i = 0; i < operationOrder.length; i++) {
610
const current = operationOrder[i];
611
if (current.includes('-start-')) {
612
const scope = current.split('-start-')[1];
613
const operationType = current.split('-start-')[0];
614
const endOperation = `${operationType}-end-${scope}`;
615
const endIndex = operationOrder.indexOf(endOperation, i + 1);
616
617
if (endIndex !== -1) {
618
operationPairs.push({
619
start: i,
620
end: endIndex,
621
operation: `${operationType}-${scope}`
622
});
623
}
624
}
625
}
626
627
// Verify no operations overlap (serialization)
628
for (let i = 0; i < operationPairs.length; i++) {
629
for (let j = i + 1; j < operationPairs.length; j++) {
630
const op1 = operationPairs[i];
631
const op2 = operationPairs[j];
632
633
// Operations should not overlap - one should completely finish before the other starts
634
const op1EndsBeforeOp2Starts = op1.end < op2.start;
635
const op2EndsBeforeOp1Starts = op2.end < op1.start;
636
637
assert.ok(op1EndsBeforeOp2Starts || op2EndsBeforeOp1Starts,
638
`Operations ${op1.operation} and ${op2.operation} should not overlap. ` +
639
`Op1: ${op1.start}-${op1.end}, Op2: ${op2.start}-${op2.end}. ` +
640
`Order: [${operationOrder.join(', ')}]`);
641
}
642
}
643
644
// Verify we have the expected operations
645
assert.ok(operationOrder.includes('create-start-scope1'), 'Should have created session for scope1');
646
assert.ok(operationOrder.includes('create-end-scope1'), 'Should have completed creating session for scope1');
647
assert.ok(operationOrder.includes('create-start-scope2'), 'Should have created session for scope2');
648
assert.ok(operationOrder.includes('create-end-scope2'), 'Should have completed creating session for scope2');
649
650
// The third call should use getSessions to find the existing scope1 session
651
assert.ok(operationOrder.includes('get-start-scope1'), 'Should have called getSessions for existing scope1 session');
652
assert.ok(operationOrder.includes('get-end-scope1'), 'Should have completed getSessions for existing scope1 session');
653
});
654
655
test('provider registration and immediate disposal race condition', async () => {
656
const provider = new TestAuthProvider('race-test');
657
658
// Register and immediately dispose
659
const disposable = extHostAuthentication.registerAuthenticationProvider('race-test', 'Race Test', provider);
660
disposable.dispose();
661
662
// Try to use the provider after disposal - should fail gracefully
663
try {
664
await extHostAuthentication.getSession(extensionDescription, 'race-test', ['scope'], { createIfNone: true });
665
assert.fail('Should have thrown an error for non-existent provider');
666
} catch (error) {
667
// Expected - provider should be unavailable
668
assert.ok(error);
669
}
670
});
671
672
test('provider re-registration after proper disposal', async () => {
673
const provider1 = new TestAuthProvider('reregister-test-1');
674
const provider2 = new TestAuthProvider('reregister-test-2');
675
676
// First registration
677
const disposable1 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 1', provider1);
678
679
// Create a session with first provider
680
const session1 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });
681
assert.strictEqual(session1?.account.label, 'reregister-test-1');
682
683
// Dispose first provider
684
disposable1.dispose();
685
686
// Re-register with different provider
687
const disposable2 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 2', provider2);
688
disposables.add(disposable2);
689
690
// Create session with second provider
691
const session2 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });
692
assert.strictEqual(session2?.account.label, 'reregister-test-2');
693
assert.notStrictEqual(session1?.accessToken, session2?.accessToken);
694
});
695
696
test('operations on different providers run concurrently', async () => {
697
const provider1 = new TestAuthProvider('concurrent-1');
698
const provider2 = new TestAuthProvider('concurrent-2');
699
700
let provider1Started = false;
701
let provider2Started = false;
702
let provider1Finished = false;
703
let provider2Finished = false;
704
let concurrencyVerified = false;
705
706
// Override createSession to track timing
707
const originalCreate1 = provider1.createSession.bind(provider1);
708
const originalCreate2 = provider2.createSession.bind(provider2);
709
710
provider1.createSession = async (scopes) => {
711
provider1Started = true;
712
await new Promise(resolve => setTimeout(resolve, 20));
713
const result = await originalCreate1(scopes);
714
provider1Finished = true;
715
return result;
716
};
717
718
provider2.createSession = async (scopes) => {
719
provider2Started = true;
720
// Provider 2 should start before provider 1 finishes (concurrent execution)
721
if (provider1Started && !provider1Finished) {
722
concurrencyVerified = true;
723
}
724
await new Promise(resolve => setTimeout(resolve, 10));
725
const result = await originalCreate2(scopes);
726
provider2Finished = true;
727
return result;
728
};
729
730
const disposable1 = extHostAuthentication.registerAuthenticationProvider('concurrent-1', 'Concurrent 1', provider1);
731
const disposable2 = extHostAuthentication.registerAuthenticationProvider('concurrent-2', 'Concurrent 2', provider2);
732
disposables.add(disposable1);
733
disposables.add(disposable2);
734
735
// Start operations on both providers simultaneously
736
const [session1, session2] = await Promise.all([
737
extHostAuthentication.getSession(extensionDescription, 'concurrent-1', ['scope'], { createIfNone: true }),
738
extHostAuthentication.getSession(extensionDescription, 'concurrent-2', ['scope'], { createIfNone: true })
739
]);
740
741
// Verify both operations completed successfully
742
assert.ok(session1);
743
assert.ok(session2);
744
assert.ok(provider1Started, 'Provider 1 should have started');
745
assert.ok(provider2Started, 'Provider 2 should have started');
746
assert.ok(provider1Finished, 'Provider 1 should have finished');
747
assert.ok(provider2Finished, 'Provider 2 should have finished');
748
assert.strictEqual(session1.account.label, 'concurrent-1');
749
assert.strictEqual(session2.account.label, 'concurrent-2');
750
751
// Verify that operations ran concurrently (provider 2 started while provider 1 was still running)
752
assert.ok(concurrencyVerified, 'Operations should have run concurrently - provider 2 should start while provider 1 is still running');
753
});
754
755
//#endregion
756
});
757
758