Path: blob/main/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';7import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';8import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';9import { INotificationService } from '../../../../platform/notification/common/notification.js';10import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';11import { IQuickInputHideEvent, IQuickInputService, IQuickPickDidAcceptEvent, IQuickPickItem, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js';12import { IStorageService } from '../../../../platform/storage/common/storage.js';13import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';14import { NullTelemetryService } from '../../../../platform/telemetry/common/telemetryUtils.js';15import { MainThreadAuthentication } from '../../browser/mainThreadAuthentication.js';16import { ExtHostContext, MainContext } from '../../common/extHost.protocol.js';17import { ExtHostAuthentication } from '../../common/extHostAuthentication.js';18import { IActivityService } from '../../../services/activity/common/activity.js';19import { AuthenticationService } from '../../../services/authentication/browser/authenticationService.js';20import { IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js';21import { IExtensionService, nullExtensionDescription as extensionDescription } from '../../../services/extensions/common/extensions.js';22import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';23import { TestRPCProtocol } from '../common/testRPCProtocol.js';24import { TestEnvironmentService, TestHostService, TestQuickInputService, TestRemoteAgentService } from '../../../test/browser/workbenchTestServices.js';25import { TestActivityService, TestExtensionService, TestLoggerService, TestProductService, TestStorageService } from '../../../test/common/workbenchTestServices.js';26import type { AuthenticationProvider, AuthenticationSession } from 'vscode';27import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';28import { IProductService } from '../../../../platform/product/common/productService.js';29import { AuthenticationAccessService, IAuthenticationAccessService } from '../../../services/authentication/browser/authenticationAccessService.js';30import { IAccountUsage, IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js';31import { AuthenticationExtensionsService } from '../../../services/authentication/browser/authenticationExtensionsService.js';32import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';33import { IExtHostInitDataService } from '../../common/extHostInitDataService.js';34import { ExtHostWindow } from '../../common/extHostWindow.js';35import { MainThreadWindow } from '../../browser/mainThreadWindow.js';36import { IHostService } from '../../../services/host/browser/host.js';37import { IOpenerService } from '../../../../platform/opener/common/opener.js';38import { IUserActivityService, UserActivityService } from '../../../services/userActivity/common/userActivityService.js';39import { ExtHostUrls } from '../../common/extHostUrls.js';40import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';41import { TestSecretStorageService } from '../../../../platform/secrets/test/common/testSecretStorageService.js';42import { IDynamicAuthenticationProviderStorageService } from '../../../services/authentication/common/dynamicAuthenticationProviderStorage.js';43import { DynamicAuthenticationProviderStorageService } from '../../../services/authentication/browser/dynamicAuthenticationProviderStorageService.js';44import { ExtHostProgress } from '../../common/extHostProgress.js';45import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';46import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';47import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';4849class AuthQuickPick {50private accept: ((e: IQuickPickDidAcceptEvent) => any) | undefined;51private hide: ((e: IQuickInputHideEvent) => any) | undefined;52public items = [];53public get selectedItems(): IQuickPickItem[] {54return this.items;55}5657onDidAccept(listener: (e: IQuickPickDidAcceptEvent) => any) {58this.accept = listener;59}60onDidHide(listener: (e: IQuickInputHideEvent) => any) {61this.hide = listener;62}6364dispose() {6566}67show() {68this.accept?.({ inBackground: false });69this.hide?.({ reason: QuickInputHideReason.Other });70}71}72class AuthTestQuickInputService extends TestQuickInputService {73override createQuickPick() {74return <any>new AuthQuickPick();75}76}7778class TestAuthUsageService implements IAuthenticationUsageService {79_serviceBrand: undefined;80initializeExtensionUsageCache(): Promise<void> { return Promise.resolve(); }81extensionUsesAuth(extensionId: string): Promise<boolean> { return Promise.resolve(false); }82readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { return []; }83removeAccountUsage(providerId: string, accountName: string): void { }84addAccountUsage(providerId: string, accountName: string, scopes: ReadonlyArray<string>, extensionId: string, extensionName: string): void { }85}8687class TestAuthProvider implements AuthenticationProvider {88private id = 1;89private sessions = new Map<string, AuthenticationSession>();90onDidChangeSessions = () => { return { dispose() { } }; };91constructor(private readonly authProviderName: string) { }92async getSessions(scopes?: readonly string[]): Promise<AuthenticationSession[]> {93if (!scopes) {94return [...this.sessions.values()];95}9697if (scopes[0] === 'return multiple') {98return [...this.sessions.values()];99}100const sessions = this.sessions.get(scopes.join(' '));101return sessions ? [sessions] : [];102}103async createSession(scopes: readonly string[]): Promise<AuthenticationSession> {104const scopesStr = scopes.join(' ');105const session = {106scopes,107id: `${this.id}`,108account: {109label: this.authProviderName,110id: `${this.id}`,111},112accessToken: Math.random() + '',113};114this.sessions.set(scopesStr, session);115this.id++;116return session;117}118async removeSession(sessionId: string): Promise<void> {119this.sessions.delete(sessionId);120}121122}123124suite('ExtHostAuthentication', () => {125const disposables = ensureNoDisposablesAreLeakedInTestSuite();126127let extHostAuthentication: ExtHostAuthentication;128let mainInstantiationService: TestInstantiationService;129130setup(async () => {131// services132const services = new ServiceCollection();133services.set(ILogService, new SyncDescriptor(NullLogService));134services.set(IDialogService, new SyncDescriptor(TestDialogService, [{ confirmed: true }]));135services.set(IStorageService, new SyncDescriptor(TestStorageService));136services.set(ISecretStorageService, new SyncDescriptor(TestSecretStorageService));137services.set(IDynamicAuthenticationProviderStorageService, new SyncDescriptor(DynamicAuthenticationProviderStorageService));138services.set(IQuickInputService, new SyncDescriptor(AuthTestQuickInputService));139services.set(IExtensionService, new SyncDescriptor(TestExtensionService));140services.set(IActivityService, new SyncDescriptor(TestActivityService));141services.set(IRemoteAgentService, new SyncDescriptor(TestRemoteAgentService));142services.set(INotificationService, new SyncDescriptor(TestNotificationService));143services.set(IHostService, new SyncDescriptor(TestHostService));144services.set(IUserActivityService, new SyncDescriptor(UserActivityService));145services.set(IAuthenticationAccessService, new SyncDescriptor(AuthenticationAccessService));146services.set(IAuthenticationService, new SyncDescriptor(AuthenticationService));147services.set(IAuthenticationUsageService, new SyncDescriptor(TestAuthUsageService));148services.set(IAuthenticationExtensionsService, new SyncDescriptor(AuthenticationExtensionsService));149mainInstantiationService = disposables.add(new TestInstantiationService(services, undefined, undefined, true));150151// stubs152// eslint-disable-next-line local/code-no-dangerous-type-assertions153mainInstantiationService.stub(IOpenerService, {} as Partial<IOpenerService>);154mainInstantiationService.stub(ITelemetryService, NullTelemetryService);155mainInstantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService);156mainInstantiationService.stub(IProductService, TestProductService);157158const rpcProtocol = disposables.add(new TestRPCProtocol());159160rpcProtocol.set(MainContext.MainThreadAuthentication, disposables.add(mainInstantiationService.createInstance(MainThreadAuthentication, rpcProtocol)));161rpcProtocol.set(MainContext.MainThreadWindow, disposables.add(mainInstantiationService.createInstance(MainThreadWindow, rpcProtocol)));162const initData: IExtHostInitDataService = {163environment: {164appUriScheme: 'test',165appName: 'Test'166}167} as any;168extHostAuthentication = new ExtHostAuthentication(169rpcProtocol,170{171environment: {172appUriScheme: 'test',173appName: 'Test'174}175} as any,176new ExtHostWindow(initData, rpcProtocol),177new ExtHostUrls(rpcProtocol),178new ExtHostProgress(rpcProtocol),179disposables.add(new TestLoggerService()),180new NullLogService()181);182rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication);183disposables.add(extHostAuthentication.registerAuthenticationProvider('test', 'test provider', new TestAuthProvider('test')));184disposables.add(extHostAuthentication.registerAuthenticationProvider(185'test-multiple',186'test multiple provider',187new TestAuthProvider('test-multiple'),188{ supportsMultipleAccounts: true }));189});190191test('createIfNone - true', async () => {192const scopes = ['foo'];193const session = await extHostAuthentication.getSession(194extensionDescription,195'test',196scopes,197{198createIfNone: true199});200assert.strictEqual(session?.id, '1');201assert.strictEqual(session?.scopes[0], 'foo');202});203204test('createIfNone - false', async () => {205const scopes = ['foo'];206const nosession = await extHostAuthentication.getSession(207extensionDescription,208'test',209scopes,210{});211assert.strictEqual(nosession, undefined);212213// Now create the session214const session = await extHostAuthentication.getSession(215extensionDescription,216'test',217scopes,218{219createIfNone: true220});221222assert.strictEqual(session?.id, '1');223assert.strictEqual(session?.scopes[0], 'foo');224225const session2 = await extHostAuthentication.getSession(226extensionDescription,227'test',228scopes,229{});230231assert.strictEqual(session2?.id, session.id);232assert.strictEqual(session2?.scopes[0], session.scopes[0]);233assert.strictEqual(session2?.accessToken, session.accessToken);234});235236// should behave the same as createIfNone: false237test('silent - true', async () => {238const scopes = ['foo'];239const nosession = await extHostAuthentication.getSession(240extensionDescription,241'test',242scopes,243{244silent: true245});246assert.strictEqual(nosession, undefined);247248// Now create the session249const session = await extHostAuthentication.getSession(250extensionDescription,251'test',252scopes,253{254createIfNone: true255});256257assert.strictEqual(session?.id, '1');258assert.strictEqual(session?.scopes[0], 'foo');259260const session2 = await extHostAuthentication.getSession(261extensionDescription,262'test',263scopes,264{265silent: true266});267268assert.strictEqual(session.id, session2?.id);269assert.strictEqual(session.scopes[0], session2?.scopes[0]);270});271272test('forceNewSession - true - existing session', async () => {273const scopes = ['foo'];274const session1 = await extHostAuthentication.getSession(275extensionDescription,276'test',277scopes,278{279createIfNone: true280});281282// Now create the session283const session2 = await extHostAuthentication.getSession(284extensionDescription,285'test',286scopes,287{288forceNewSession: true289});290291assert.strictEqual(session2?.id, '2');292assert.strictEqual(session2?.scopes[0], 'foo');293assert.notStrictEqual(session1.accessToken, session2?.accessToken);294});295296// Should behave like createIfNone: true297test('forceNewSession - true - no existing session', async () => {298const scopes = ['foo'];299const session = await extHostAuthentication.getSession(300extensionDescription,301'test',302scopes,303{304forceNewSession: true305});306assert.strictEqual(session?.id, '1');307assert.strictEqual(session?.scopes[0], 'foo');308});309310test('forceNewSession - detail', async () => {311const scopes = ['foo'];312const session1 = await extHostAuthentication.getSession(313extensionDescription,314'test',315scopes,316{317createIfNone: true318});319320// Now create the session321const session2 = await extHostAuthentication.getSession(322extensionDescription,323'test',324scopes,325{326forceNewSession: { detail: 'bar' }327});328329assert.strictEqual(session2?.id, '2');330assert.strictEqual(session2?.scopes[0], 'foo');331assert.notStrictEqual(session1.accessToken, session2?.accessToken);332});333334//#region Multi-Account AuthProvider335336test('clearSessionPreference - true', async () => {337const scopes = ['foo'];338// Now create the session339const session = await extHostAuthentication.getSession(340extensionDescription,341'test-multiple',342scopes,343{344createIfNone: true345});346347assert.strictEqual(session?.id, '1');348assert.strictEqual(session?.scopes[0], scopes[0]);349350const scopes2 = ['bar'];351const session2 = await extHostAuthentication.getSession(352extensionDescription,353'test-multiple',354scopes2,355{356createIfNone: true357});358assert.strictEqual(session2?.id, '2');359assert.strictEqual(session2?.scopes[0], scopes2[0]);360361const session3 = await extHostAuthentication.getSession(362extensionDescription,363'test-multiple',364['return multiple'],365{366clearSessionPreference: true,367createIfNone: true368});369370// clearing session preference causes us to get the first session371// because it would normally show a quick pick for the user to choose372assert.strictEqual(session3?.id, session.id);373assert.strictEqual(session3?.scopes[0], session.scopes[0]);374assert.strictEqual(session3?.accessToken, session.accessToken);375});376377test('silently getting session should return a session (if any) regardless of preference - fixes #137819', async () => {378const scopes = ['foo'];379// Now create the session380const session = await extHostAuthentication.getSession(381extensionDescription,382'test-multiple',383scopes,384{385createIfNone: true386});387388assert.strictEqual(session?.id, '1');389assert.strictEqual(session?.scopes[0], scopes[0]);390391const scopes2 = ['bar'];392const session2 = await extHostAuthentication.getSession(393extensionDescription,394'test-multiple',395scopes2,396{397createIfNone: true398});399assert.strictEqual(session2?.id, '2');400assert.strictEqual(session2?.scopes[0], scopes2[0]);401402const shouldBeSession1 = await extHostAuthentication.getSession(403extensionDescription,404'test-multiple',405scopes,406{});407assert.strictEqual(shouldBeSession1?.id, session.id);408assert.strictEqual(shouldBeSession1?.scopes[0], session.scopes[0]);409assert.strictEqual(shouldBeSession1?.accessToken, session.accessToken);410411const shouldBeSession2 = await extHostAuthentication.getSession(412extensionDescription,413'test-multiple',414scopes2,415{});416assert.strictEqual(shouldBeSession2?.id, session2.id);417assert.strictEqual(shouldBeSession2?.scopes[0], session2.scopes[0]);418assert.strictEqual(shouldBeSession2?.accessToken, session2.accessToken);419});420421//#endregion422423//#region error cases424425test('createIfNone and forceNewSession', async () => {426try {427await extHostAuthentication.getSession(428extensionDescription,429'test',430['foo'],431{432createIfNone: true,433forceNewSession: true434});435assert.fail('should have thrown an Error.');436} catch (e) {437assert.ok(e);438}439});440441test('forceNewSession and silent', async () => {442try {443await extHostAuthentication.getSession(444extensionDescription,445'test',446['foo'],447{448forceNewSession: true,449silent: true450});451assert.fail('should have thrown an Error.');452} catch (e) {453assert.ok(e);454}455});456457test('createIfNone and silent', async () => {458try {459await extHostAuthentication.getSession(460extensionDescription,461'test',462['foo'],463{464createIfNone: true,465silent: true466});467assert.fail('should have thrown an Error.');468} catch (e) {469assert.ok(e);470}471});472473test('Can get multiple sessions (with different scopes) in one extension', async () => {474let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(475extensionDescription,476'test-multiple',477['foo'],478{479createIfNone: true480});481session = await extHostAuthentication.getSession(482extensionDescription,483'test-multiple',484['bar'],485{486createIfNone: true487});488assert.strictEqual(session?.id, '2');489assert.strictEqual(session?.scopes[0], 'bar');490491session = await extHostAuthentication.getSession(492extensionDescription,493'test-multiple',494['foo'],495{496createIfNone: false497});498assert.strictEqual(session?.id, '1');499assert.strictEqual(session?.scopes[0], 'foo');500});501502test('Can get multiple sessions (from different providers) in one extension', async () => {503let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(504extensionDescription,505'test-multiple',506['foo'],507{508createIfNone: true509});510session = await extHostAuthentication.getSession(511extensionDescription,512'test',513['foo'],514{515createIfNone: true516});517assert.strictEqual(session?.id, '1');518assert.strictEqual(session?.scopes[0], 'foo');519assert.strictEqual(session?.account.label, 'test');520521const session2 = await extHostAuthentication.getSession(522extensionDescription,523'test-multiple',524['foo'],525{526createIfNone: false527});528assert.strictEqual(session2?.id, '1');529assert.strictEqual(session2?.scopes[0], 'foo');530assert.strictEqual(session2?.account.label, 'test-multiple');531});532533test('Can get multiple sessions (from different providers) in one extension at the same time', async () => {534const sessionP: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(535extensionDescription,536'test',537['foo'],538{539createIfNone: true540});541const session2P: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(542extensionDescription,543'test-multiple',544['foo'],545{546createIfNone: true547});548const session = await sessionP;549assert.strictEqual(session?.id, '1');550assert.strictEqual(session?.scopes[0], 'foo');551assert.strictEqual(session?.account.label, 'test');552553const session2 = await session2P;554assert.strictEqual(session2?.id, '1');555assert.strictEqual(session2?.scopes[0], 'foo');556assert.strictEqual(session2?.account.label, 'test-multiple');557});558559560//#endregion561562//#region Race Condition and Sequencing Tests563564test('concurrent operations on same provider are serialized', async () => {565const provider = new TestAuthProvider('concurrent-test');566const operationOrder: string[] = [];567568// Mock the provider methods to track operation order569const originalCreateSession = provider.createSession.bind(provider);570const originalGetSessions = provider.getSessions.bind(provider);571572provider.createSession = async (scopes) => {573operationOrder.push(`create-start-${scopes[0]}`);574await new Promise(resolve => setTimeout(resolve, 20)); // Simulate async work575const result = await originalCreateSession(scopes);576operationOrder.push(`create-end-${scopes[0]}`);577return result;578};579580provider.getSessions = async (scopes) => {581const scopeKey = scopes ? scopes[0] : 'all';582operationOrder.push(`get-start-${scopeKey}`);583await new Promise(resolve => setTimeout(resolve, 10)); // Simulate async work584const result = await originalGetSessions(scopes);585operationOrder.push(`get-end-${scopeKey}`);586return result;587};588589const disposable = extHostAuthentication.registerAuthenticationProvider('concurrent-test', 'Concurrent Test', provider);590disposables.add(disposable);591592// Start multiple operations simultaneously on the same provider593const promises = [594extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], { createIfNone: true }),595extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope2'], { createIfNone: true }),596extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], {}) // This should get the existing session597];598599await Promise.all(promises);600601// Verify that operations were serialized - no overlapping operations602// Build a map of operation starts to their corresponding ends603const operationPairs: Array<{ start: number; end: number; operation: string }> = [];604605for (let i = 0; i < operationOrder.length; i++) {606const current = operationOrder[i];607if (current.includes('-start-')) {608const scope = current.split('-start-')[1];609const operationType = current.split('-start-')[0];610const endOperation = `${operationType}-end-${scope}`;611const endIndex = operationOrder.indexOf(endOperation, i + 1);612613if (endIndex !== -1) {614operationPairs.push({615start: i,616end: endIndex,617operation: `${operationType}-${scope}`618});619}620}621}622623// Verify no operations overlap (serialization)624for (let i = 0; i < operationPairs.length; i++) {625for (let j = i + 1; j < operationPairs.length; j++) {626const op1 = operationPairs[i];627const op2 = operationPairs[j];628629// Operations should not overlap - one should completely finish before the other starts630const op1EndsBeforeOp2Starts = op1.end < op2.start;631const op2EndsBeforeOp1Starts = op2.end < op1.start;632633assert.ok(op1EndsBeforeOp2Starts || op2EndsBeforeOp1Starts,634`Operations ${op1.operation} and ${op2.operation} should not overlap. ` +635`Op1: ${op1.start}-${op1.end}, Op2: ${op2.start}-${op2.end}. ` +636`Order: [${operationOrder.join(', ')}]`);637}638}639640// Verify we have the expected operations641assert.ok(operationOrder.includes('create-start-scope1'), 'Should have created session for scope1');642assert.ok(operationOrder.includes('create-end-scope1'), 'Should have completed creating session for scope1');643assert.ok(operationOrder.includes('create-start-scope2'), 'Should have created session for scope2');644assert.ok(operationOrder.includes('create-end-scope2'), 'Should have completed creating session for scope2');645646// The third call should use getSessions to find the existing scope1 session647assert.ok(operationOrder.includes('get-start-scope1'), 'Should have called getSessions for existing scope1 session');648assert.ok(operationOrder.includes('get-end-scope1'), 'Should have completed getSessions for existing scope1 session');649});650651test('provider registration and immediate disposal race condition', async () => {652const provider = new TestAuthProvider('race-test');653654// Register and immediately dispose655const disposable = extHostAuthentication.registerAuthenticationProvider('race-test', 'Race Test', provider);656disposable.dispose();657658// Try to use the provider after disposal - should fail gracefully659try {660await extHostAuthentication.getSession(extensionDescription, 'race-test', ['scope'], { createIfNone: true });661assert.fail('Should have thrown an error for non-existent provider');662} catch (error) {663// Expected - provider should be unavailable664assert.ok(error);665}666});667668test('provider re-registration after proper disposal', async () => {669const provider1 = new TestAuthProvider('reregister-test-1');670const provider2 = new TestAuthProvider('reregister-test-2');671672// First registration673const disposable1 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 1', provider1);674675// Create a session with first provider676const session1 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });677assert.strictEqual(session1?.account.label, 'reregister-test-1');678679// Dispose first provider680disposable1.dispose();681682// Re-register with different provider683const disposable2 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 2', provider2);684disposables.add(disposable2);685686// Create session with second provider687const session2 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });688assert.strictEqual(session2?.account.label, 'reregister-test-2');689assert.notStrictEqual(session1?.accessToken, session2?.accessToken);690});691692test('session operations during provider lifecycle changes', async () => {693const provider = new TestAuthProvider('lifecycle-test');694const disposable = extHostAuthentication.registerAuthenticationProvider('lifecycle-test', 'Lifecycle Test', provider);695696// Start a session creation697const sessionPromise = extHostAuthentication.getSession(extensionDescription, 'lifecycle-test', ['scope'], { createIfNone: true });698699// Don't dispose immediately - let the session creation start700await new Promise(resolve => setTimeout(resolve, 5));701702// Dispose the provider while the session creation is likely still in progress703disposable.dispose();704705// The session creation should complete successfully even if we dispose during the operation706const session = await sessionPromise;707assert.ok(session);708assert.strictEqual(session.account.label, 'lifecycle-test');709});710711test('operations on different providers run concurrently', async () => {712const provider1 = new TestAuthProvider('concurrent-1');713const provider2 = new TestAuthProvider('concurrent-2');714715let provider1Started = false;716let provider2Started = false;717let provider1Finished = false;718let provider2Finished = false;719let concurrencyVerified = false;720721// Override createSession to track timing722const originalCreate1 = provider1.createSession.bind(provider1);723const originalCreate2 = provider2.createSession.bind(provider2);724725provider1.createSession = async (scopes) => {726provider1Started = true;727await new Promise(resolve => setTimeout(resolve, 20));728const result = await originalCreate1(scopes);729provider1Finished = true;730return result;731};732733provider2.createSession = async (scopes) => {734provider2Started = true;735// Provider 2 should start before provider 1 finishes (concurrent execution)736if (provider1Started && !provider1Finished) {737concurrencyVerified = true;738}739await new Promise(resolve => setTimeout(resolve, 10));740const result = await originalCreate2(scopes);741provider2Finished = true;742return result;743};744745const disposable1 = extHostAuthentication.registerAuthenticationProvider('concurrent-1', 'Concurrent 1', provider1);746const disposable2 = extHostAuthentication.registerAuthenticationProvider('concurrent-2', 'Concurrent 2', provider2);747disposables.add(disposable1);748disposables.add(disposable2);749750// Start operations on both providers simultaneously751const [session1, session2] = await Promise.all([752extHostAuthentication.getSession(extensionDescription, 'concurrent-1', ['scope'], { createIfNone: true }),753extHostAuthentication.getSession(extensionDescription, 'concurrent-2', ['scope'], { createIfNone: true })754]);755756// Verify both operations completed successfully757assert.ok(session1);758assert.ok(session2);759assert.ok(provider1Started, 'Provider 1 should have started');760assert.ok(provider2Started, 'Provider 2 should have started');761assert.ok(provider1Finished, 'Provider 1 should have finished');762assert.ok(provider2Finished, 'Provider 2 should have finished');763assert.strictEqual(session1.account.label, 'concurrent-1');764assert.strictEqual(session2.account.label, 'concurrent-2');765766// Verify that operations ran concurrently (provider 2 started while provider 1 was still running)767assert.ok(concurrencyVerified, 'Operations should have run concurrently - provider 2 should start while provider 1 is still running');768});769770//#endregion771});772773774