Path: blob/main/src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts
5220 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() {74// eslint-disable-next-line local/code-no-any-casts75return <any>new AuthQuickPick();76}77}7879class TestAuthUsageService implements IAuthenticationUsageService {80_serviceBrand: undefined;81initializeExtensionUsageCache(): Promise<void> { return Promise.resolve(); }82extensionUsesAuth(extensionId: string): Promise<boolean> { return Promise.resolve(false); }83readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { return []; }84removeAccountUsage(providerId: string, accountName: string): void { }85addAccountUsage(providerId: string, accountName: string, scopes: ReadonlyArray<string>, extensionId: string, extensionName: string): void { }86}8788class TestAuthProvider implements AuthenticationProvider {89private id = 1;90private sessions = new Map<string, AuthenticationSession>();91onDidChangeSessions = () => { return { dispose() { } }; };92constructor(private readonly authProviderName: string) { }93async getSessions(scopes?: readonly string[]): Promise<AuthenticationSession[]> {94if (!scopes) {95return [...this.sessions.values()];96}9798if (scopes[0] === 'return multiple') {99return [...this.sessions.values()];100}101const sessions = this.sessions.get(scopes.join(' '));102return sessions ? [sessions] : [];103}104async createSession(scopes: readonly string[]): Promise<AuthenticationSession> {105const scopesStr = scopes.join(' ');106const session = {107scopes,108id: `${this.id}`,109account: {110label: this.authProviderName,111id: `${this.id}`,112},113accessToken: Math.random() + '',114};115this.sessions.set(scopesStr, session);116this.id++;117return session;118}119async removeSession(sessionId: string): Promise<void> {120this.sessions.delete(sessionId);121}122123}124125suite('ExtHostAuthentication', () => {126const disposables = ensureNoDisposablesAreLeakedInTestSuite();127128let extHostAuthentication: ExtHostAuthentication;129let mainInstantiationService: TestInstantiationService;130131setup(async () => {132// services133const services = new ServiceCollection();134services.set(ILogService, new SyncDescriptor(NullLogService));135services.set(IDialogService, new SyncDescriptor(TestDialogService, [{ confirmed: true }]));136services.set(IStorageService, new SyncDescriptor(TestStorageService));137services.set(ISecretStorageService, new SyncDescriptor(TestSecretStorageService));138services.set(IDynamicAuthenticationProviderStorageService, new SyncDescriptor(DynamicAuthenticationProviderStorageService));139services.set(IQuickInputService, new SyncDescriptor(AuthTestQuickInputService));140services.set(IExtensionService, new SyncDescriptor(TestExtensionService));141services.set(IActivityService, new SyncDescriptor(TestActivityService));142services.set(IRemoteAgentService, new SyncDescriptor(TestRemoteAgentService));143services.set(INotificationService, new SyncDescriptor(TestNotificationService));144services.set(IHostService, new SyncDescriptor(TestHostService));145services.set(IUserActivityService, new SyncDescriptor(UserActivityService));146services.set(IAuthenticationAccessService, new SyncDescriptor(AuthenticationAccessService));147services.set(IAuthenticationService, new SyncDescriptor(AuthenticationService));148services.set(IAuthenticationUsageService, new SyncDescriptor(TestAuthUsageService));149services.set(IAuthenticationExtensionsService, new SyncDescriptor(AuthenticationExtensionsService));150mainInstantiationService = disposables.add(new TestInstantiationService(services, undefined, undefined, true));151152// stubs153// eslint-disable-next-line local/code-no-dangerous-type-assertions154mainInstantiationService.stub(IOpenerService, {} as Partial<IOpenerService>);155mainInstantiationService.stub(ITelemetryService, NullTelemetryService);156mainInstantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService);157mainInstantiationService.stub(IProductService, TestProductService);158159const rpcProtocol = disposables.add(new TestRPCProtocol());160161rpcProtocol.set(MainContext.MainThreadAuthentication, disposables.add(mainInstantiationService.createInstance(MainThreadAuthentication, rpcProtocol)));162rpcProtocol.set(MainContext.MainThreadWindow, disposables.add(mainInstantiationService.createInstance(MainThreadWindow, rpcProtocol)));163// eslint-disable-next-line local/code-no-any-casts164const initData: IExtHostInitDataService = {165environment: {166appUriScheme: 'test',167appName: 'Test'168}169} as any;170extHostAuthentication = new ExtHostAuthentication(171rpcProtocol,172// eslint-disable-next-line local/code-no-any-casts173{174environment: {175appUriScheme: 'test',176appName: 'Test'177}178} as any,179new ExtHostWindow(initData, rpcProtocol),180new ExtHostUrls(rpcProtocol),181new ExtHostProgress(rpcProtocol),182disposables.add(new TestLoggerService()),183new NullLogService()184);185rpcProtocol.set(ExtHostContext.ExtHostAuthentication, extHostAuthentication);186disposables.add(extHostAuthentication.registerAuthenticationProvider('test', 'test provider', new TestAuthProvider('test')));187disposables.add(extHostAuthentication.registerAuthenticationProvider(188'test-multiple',189'test multiple provider',190new TestAuthProvider('test-multiple'),191{ supportsMultipleAccounts: true }));192});193194test('createIfNone - true', async () => {195const scopes = ['foo'];196const session = await extHostAuthentication.getSession(197extensionDescription,198'test',199scopes,200{201createIfNone: true202});203assert.strictEqual(session?.id, '1');204assert.strictEqual(session?.scopes[0], 'foo');205});206207test('createIfNone - false', async () => {208const scopes = ['foo'];209const nosession = await extHostAuthentication.getSession(210extensionDescription,211'test',212scopes,213{});214assert.strictEqual(nosession, undefined);215216// Now create the session217const session = await extHostAuthentication.getSession(218extensionDescription,219'test',220scopes,221{222createIfNone: true223});224225assert.strictEqual(session?.id, '1');226assert.strictEqual(session?.scopes[0], 'foo');227228const session2 = await extHostAuthentication.getSession(229extensionDescription,230'test',231scopes,232{});233234assert.strictEqual(session2?.id, session.id);235assert.strictEqual(session2?.scopes[0], session.scopes[0]);236assert.strictEqual(session2?.accessToken, session.accessToken);237});238239// should behave the same as createIfNone: false240test('silent - true', async () => {241const scopes = ['foo'];242const nosession = await extHostAuthentication.getSession(243extensionDescription,244'test',245scopes,246{247silent: true248});249assert.strictEqual(nosession, undefined);250251// Now create the session252const session = await extHostAuthentication.getSession(253extensionDescription,254'test',255scopes,256{257createIfNone: true258});259260assert.strictEqual(session?.id, '1');261assert.strictEqual(session?.scopes[0], 'foo');262263const session2 = await extHostAuthentication.getSession(264extensionDescription,265'test',266scopes,267{268silent: true269});270271assert.strictEqual(session.id, session2?.id);272assert.strictEqual(session.scopes[0], session2?.scopes[0]);273});274275test('forceNewSession - true - existing session', async () => {276const scopes = ['foo'];277const session1 = await extHostAuthentication.getSession(278extensionDescription,279'test',280scopes,281{282createIfNone: true283});284285// Now create the session286const session2 = await extHostAuthentication.getSession(287extensionDescription,288'test',289scopes,290{291forceNewSession: true292});293294assert.strictEqual(session2?.id, '2');295assert.strictEqual(session2?.scopes[0], 'foo');296assert.notStrictEqual(session1.accessToken, session2?.accessToken);297});298299// Should behave like createIfNone: true300test('forceNewSession - true - no existing session', async () => {301const scopes = ['foo'];302const session = await extHostAuthentication.getSession(303extensionDescription,304'test',305scopes,306{307forceNewSession: true308});309assert.strictEqual(session?.id, '1');310assert.strictEqual(session?.scopes[0], 'foo');311});312313test('forceNewSession - detail', async () => {314const scopes = ['foo'];315const session1 = await extHostAuthentication.getSession(316extensionDescription,317'test',318scopes,319{320createIfNone: true321});322323// Now create the session324const session2 = await extHostAuthentication.getSession(325extensionDescription,326'test',327scopes,328{329forceNewSession: { detail: 'bar' }330});331332assert.strictEqual(session2?.id, '2');333assert.strictEqual(session2?.scopes[0], 'foo');334assert.notStrictEqual(session1.accessToken, session2?.accessToken);335});336337//#region Multi-Account AuthProvider338339test('clearSessionPreference - true', async () => {340const scopes = ['foo'];341// Now create the session342const session = await extHostAuthentication.getSession(343extensionDescription,344'test-multiple',345scopes,346{347createIfNone: true348});349350assert.strictEqual(session?.id, '1');351assert.strictEqual(session?.scopes[0], scopes[0]);352353const scopes2 = ['bar'];354const session2 = await extHostAuthentication.getSession(355extensionDescription,356'test-multiple',357scopes2,358{359createIfNone: true360});361assert.strictEqual(session2?.id, '2');362assert.strictEqual(session2?.scopes[0], scopes2[0]);363364const session3 = await extHostAuthentication.getSession(365extensionDescription,366'test-multiple',367['return multiple'],368{369clearSessionPreference: true,370createIfNone: true371});372373// clearing session preference causes us to get the first session374// because it would normally show a quick pick for the user to choose375assert.strictEqual(session3?.id, session.id);376assert.strictEqual(session3?.scopes[0], session.scopes[0]);377assert.strictEqual(session3?.accessToken, session.accessToken);378});379380test('silently getting session should return a session (if any) regardless of preference - fixes #137819', async () => {381const scopes = ['foo'];382// Now create the session383const session = await extHostAuthentication.getSession(384extensionDescription,385'test-multiple',386scopes,387{388createIfNone: true389});390391assert.strictEqual(session?.id, '1');392assert.strictEqual(session?.scopes[0], scopes[0]);393394const scopes2 = ['bar'];395const session2 = await extHostAuthentication.getSession(396extensionDescription,397'test-multiple',398scopes2,399{400createIfNone: true401});402assert.strictEqual(session2?.id, '2');403assert.strictEqual(session2?.scopes[0], scopes2[0]);404405const shouldBeSession1 = await extHostAuthentication.getSession(406extensionDescription,407'test-multiple',408scopes,409{});410assert.strictEqual(shouldBeSession1?.id, session.id);411assert.strictEqual(shouldBeSession1?.scopes[0], session.scopes[0]);412assert.strictEqual(shouldBeSession1?.accessToken, session.accessToken);413414const shouldBeSession2 = await extHostAuthentication.getSession(415extensionDescription,416'test-multiple',417scopes2,418{});419assert.strictEqual(shouldBeSession2?.id, session2.id);420assert.strictEqual(shouldBeSession2?.scopes[0], session2.scopes[0]);421assert.strictEqual(shouldBeSession2?.accessToken, session2.accessToken);422});423424//#endregion425426//#region error cases427428test('createIfNone and forceNewSession', async () => {429try {430await extHostAuthentication.getSession(431extensionDescription,432'test',433['foo'],434{435createIfNone: true,436forceNewSession: true437});438assert.fail('should have thrown an Error.');439} catch (e) {440assert.ok(e);441}442});443444test('forceNewSession and silent', async () => {445try {446await extHostAuthentication.getSession(447extensionDescription,448'test',449['foo'],450{451forceNewSession: true,452silent: true453});454assert.fail('should have thrown an Error.');455} catch (e) {456assert.ok(e);457}458});459460test('createIfNone and silent', async () => {461try {462await extHostAuthentication.getSession(463extensionDescription,464'test',465['foo'],466{467createIfNone: true,468silent: true469});470assert.fail('should have thrown an Error.');471} catch (e) {472assert.ok(e);473}474});475476test('Can get multiple sessions (with different scopes) in one extension', async () => {477let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(478extensionDescription,479'test-multiple',480['foo'],481{482createIfNone: true483});484session = await extHostAuthentication.getSession(485extensionDescription,486'test-multiple',487['bar'],488{489createIfNone: true490});491assert.strictEqual(session?.id, '2');492assert.strictEqual(session?.scopes[0], 'bar');493494session = await extHostAuthentication.getSession(495extensionDescription,496'test-multiple',497['foo'],498{499createIfNone: false500});501assert.strictEqual(session?.id, '1');502assert.strictEqual(session?.scopes[0], 'foo');503});504505test('Can get multiple sessions (from different providers) in one extension', async () => {506let session: AuthenticationSession | undefined = await extHostAuthentication.getSession(507extensionDescription,508'test-multiple',509['foo'],510{511createIfNone: true512});513session = await extHostAuthentication.getSession(514extensionDescription,515'test',516['foo'],517{518createIfNone: true519});520assert.strictEqual(session?.id, '1');521assert.strictEqual(session?.scopes[0], 'foo');522assert.strictEqual(session?.account.label, 'test');523524const session2 = await extHostAuthentication.getSession(525extensionDescription,526'test-multiple',527['foo'],528{529createIfNone: false530});531assert.strictEqual(session2?.id, '1');532assert.strictEqual(session2?.scopes[0], 'foo');533assert.strictEqual(session2?.account.label, 'test-multiple');534});535536test('Can get multiple sessions (from different providers) in one extension at the same time', async () => {537const sessionP: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(538extensionDescription,539'test',540['foo'],541{542createIfNone: true543});544const session2P: Promise<AuthenticationSession | undefined> = extHostAuthentication.getSession(545extensionDescription,546'test-multiple',547['foo'],548{549createIfNone: true550});551const session = await sessionP;552assert.strictEqual(session?.id, '1');553assert.strictEqual(session?.scopes[0], 'foo');554assert.strictEqual(session?.account.label, 'test');555556const session2 = await session2P;557assert.strictEqual(session2?.id, '1');558assert.strictEqual(session2?.scopes[0], 'foo');559assert.strictEqual(session2?.account.label, 'test-multiple');560});561562563//#endregion564565//#region Race Condition and Sequencing Tests566567test('concurrent operations on same provider are serialized', async () => {568const provider = new TestAuthProvider('concurrent-test');569const operationOrder: string[] = [];570571// Mock the provider methods to track operation order572const originalCreateSession = provider.createSession.bind(provider);573const originalGetSessions = provider.getSessions.bind(provider);574575provider.createSession = async (scopes) => {576operationOrder.push(`create-start-${scopes[0]}`);577await new Promise(resolve => setTimeout(resolve, 20)); // Simulate async work578const result = await originalCreateSession(scopes);579operationOrder.push(`create-end-${scopes[0]}`);580return result;581};582583provider.getSessions = async (scopes) => {584const scopeKey = scopes ? scopes[0] : 'all';585operationOrder.push(`get-start-${scopeKey}`);586await new Promise(resolve => setTimeout(resolve, 10)); // Simulate async work587const result = await originalGetSessions(scopes);588operationOrder.push(`get-end-${scopeKey}`);589return result;590};591592const disposable = extHostAuthentication.registerAuthenticationProvider('concurrent-test', 'Concurrent Test', provider);593disposables.add(disposable);594595// Start multiple operations simultaneously on the same provider596const promises = [597extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], { createIfNone: true }),598extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope2'], { createIfNone: true }),599extHostAuthentication.getSession(extensionDescription, 'concurrent-test', ['scope1'], {}) // This should get the existing session600];601602await Promise.all(promises);603604// Verify that operations were serialized - no overlapping operations605// Build a map of operation starts to their corresponding ends606const operationPairs: Array<{ start: number; end: number; operation: string }> = [];607608for (let i = 0; i < operationOrder.length; i++) {609const current = operationOrder[i];610if (current.includes('-start-')) {611const scope = current.split('-start-')[1];612const operationType = current.split('-start-')[0];613const endOperation = `${operationType}-end-${scope}`;614const endIndex = operationOrder.indexOf(endOperation, i + 1);615616if (endIndex !== -1) {617operationPairs.push({618start: i,619end: endIndex,620operation: `${operationType}-${scope}`621});622}623}624}625626// Verify no operations overlap (serialization)627for (let i = 0; i < operationPairs.length; i++) {628for (let j = i + 1; j < operationPairs.length; j++) {629const op1 = operationPairs[i];630const op2 = operationPairs[j];631632// Operations should not overlap - one should completely finish before the other starts633const op1EndsBeforeOp2Starts = op1.end < op2.start;634const op2EndsBeforeOp1Starts = op2.end < op1.start;635636assert.ok(op1EndsBeforeOp2Starts || op2EndsBeforeOp1Starts,637`Operations ${op1.operation} and ${op2.operation} should not overlap. ` +638`Op1: ${op1.start}-${op1.end}, Op2: ${op2.start}-${op2.end}. ` +639`Order: [${operationOrder.join(', ')}]`);640}641}642643// Verify we have the expected operations644assert.ok(operationOrder.includes('create-start-scope1'), 'Should have created session for scope1');645assert.ok(operationOrder.includes('create-end-scope1'), 'Should have completed creating session for scope1');646assert.ok(operationOrder.includes('create-start-scope2'), 'Should have created session for scope2');647assert.ok(operationOrder.includes('create-end-scope2'), 'Should have completed creating session for scope2');648649// The third call should use getSessions to find the existing scope1 session650assert.ok(operationOrder.includes('get-start-scope1'), 'Should have called getSessions for existing scope1 session');651assert.ok(operationOrder.includes('get-end-scope1'), 'Should have completed getSessions for existing scope1 session');652});653654test('provider registration and immediate disposal race condition', async () => {655const provider = new TestAuthProvider('race-test');656657// Register and immediately dispose658const disposable = extHostAuthentication.registerAuthenticationProvider('race-test', 'Race Test', provider);659disposable.dispose();660661// Try to use the provider after disposal - should fail gracefully662try {663await extHostAuthentication.getSession(extensionDescription, 'race-test', ['scope'], { createIfNone: true });664assert.fail('Should have thrown an error for non-existent provider');665} catch (error) {666// Expected - provider should be unavailable667assert.ok(error);668}669});670671test('provider re-registration after proper disposal', async () => {672const provider1 = new TestAuthProvider('reregister-test-1');673const provider2 = new TestAuthProvider('reregister-test-2');674675// First registration676const disposable1 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 1', provider1);677678// Create a session with first provider679const session1 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });680assert.strictEqual(session1?.account.label, 'reregister-test-1');681682// Dispose first provider683disposable1.dispose();684685// Re-register with different provider686const disposable2 = extHostAuthentication.registerAuthenticationProvider('reregister-test', 'Provider 2', provider2);687disposables.add(disposable2);688689// Create session with second provider690const session2 = await extHostAuthentication.getSession(extensionDescription, 'reregister-test', ['scope'], { createIfNone: true });691assert.strictEqual(session2?.account.label, 'reregister-test-2');692assert.notStrictEqual(session1?.accessToken, session2?.accessToken);693});694695test('operations on different providers run concurrently', async () => {696const provider1 = new TestAuthProvider('concurrent-1');697const provider2 = new TestAuthProvider('concurrent-2');698699let provider1Started = false;700let provider2Started = false;701let provider1Finished = false;702let provider2Finished = false;703let concurrencyVerified = false;704705// Override createSession to track timing706const originalCreate1 = provider1.createSession.bind(provider1);707const originalCreate2 = provider2.createSession.bind(provider2);708709provider1.createSession = async (scopes) => {710provider1Started = true;711await new Promise(resolve => setTimeout(resolve, 20));712const result = await originalCreate1(scopes);713provider1Finished = true;714return result;715};716717provider2.createSession = async (scopes) => {718provider2Started = true;719// Provider 2 should start before provider 1 finishes (concurrent execution)720if (provider1Started && !provider1Finished) {721concurrencyVerified = true;722}723await new Promise(resolve => setTimeout(resolve, 10));724const result = await originalCreate2(scopes);725provider2Finished = true;726return result;727};728729const disposable1 = extHostAuthentication.registerAuthenticationProvider('concurrent-1', 'Concurrent 1', provider1);730const disposable2 = extHostAuthentication.registerAuthenticationProvider('concurrent-2', 'Concurrent 2', provider2);731disposables.add(disposable1);732disposables.add(disposable2);733734// Start operations on both providers simultaneously735const [session1, session2] = await Promise.all([736extHostAuthentication.getSession(extensionDescription, 'concurrent-1', ['scope'], { createIfNone: true }),737extHostAuthentication.getSession(extensionDescription, 'concurrent-2', ['scope'], { createIfNone: true })738]);739740// Verify both operations completed successfully741assert.ok(session1);742assert.ok(session2);743assert.ok(provider1Started, 'Provider 1 should have started');744assert.ok(provider2Started, 'Provider 2 should have started');745assert.ok(provider1Finished, 'Provider 1 should have finished');746assert.ok(provider2Finished, 'Provider 2 should have finished');747assert.strictEqual(session1.account.label, 'concurrent-1');748assert.strictEqual(session2.account.label, 'concurrent-2');749750// Verify that operations ran concurrently (provider 2 started while provider 1 was still running)751assert.ok(concurrencyVerified, 'Operations should have run concurrently - provider 2 should start while provider 1 is still running');752});753754//#endregion755});756757758