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