Files
vscode/src/vs/workbench/api/test/browser/mainThreadAuthentication.integrationTest.ts
T
Tyler James Leonhardt 3fe7649e8a Have the resourceUri be attached to the Auth Provider (#277540)
In order to not complicate VS Code API, the initial design of dynamic auth providers was to register one per auth server/resource server pair.

> note resource server is the same as mcp server more-or-less

that's why the auth provider id was a combo of those two urls.

However, after registration, when we wanted to see what auth provider an mcp server would use, we'd only look at the auth server uri... this caused a bug where the same auth provider was being re-used for multiple mcp servers, even though those are 2 different resources.

The fix? When fetching what auth provider to use, also pass in the resource server and look at the resource server on the auth provider to see if they match.

I still think we need an larger re-design to allow auth providers to take in `resource`s ...but this addresses the issue for now. We will need to do this once Entra supports `resource` which should happen in the new year ..,,..... maybe

Fixes https://github.com/microsoft/vscode/issues/272047
2025-11-14 22:00:03 +00:00

185 lines
10 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { TestDialogService } from '../../../../platform/dialogs/test/common/testDialogService.js';
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { TestNotificationService } from '../../../../platform/notification/test/common/testNotificationService.js';
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { NullTelemetryService } from '../../../../platform/telemetry/common/telemetryUtils.js';
import { MainThreadAuthentication } from '../../browser/mainThreadAuthentication.js';
import { ExtHostContext, MainContext } from '../../common/extHost.protocol.js';
import { IActivityService } from '../../../services/activity/common/activity.js';
import { AuthenticationService } from '../../../services/authentication/browser/authenticationService.js';
import { IAuthenticationExtensionsService, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
import { TestRPCProtocol } from '../common/testRPCProtocol.js';
import { TestEnvironmentService, TestHostService, TestQuickInputService, TestRemoteAgentService } from '../../../test/browser/workbenchTestServices.js';
import { TestActivityService, TestExtensionService, TestProductService, TestStorageService } from '../../../test/common/workbenchTestServices.js';
import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { AuthenticationAccessService, IAuthenticationAccessService } from '../../../services/authentication/browser/authenticationAccessService.js';
import { IAccountUsage, IAuthenticationUsageService } from '../../../services/authentication/browser/authenticationUsageService.js';
import { AuthenticationExtensionsService } from '../../../services/authentication/browser/authenticationExtensionsService.js';
import { ILogService, NullLogService } from '../../../../platform/log/common/log.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IUserActivityService, UserActivityService } from '../../../services/userActivity/common/userActivityService.js';
import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js';
import { TestSecretStorageService } from '../../../../platform/secrets/test/common/testSecretStorageService.js';
import { IDynamicAuthenticationProviderStorageService } from '../../../services/authentication/common/dynamicAuthenticationProviderStorage.js';
import { DynamicAuthenticationProviderStorageService } from '../../../services/authentication/browser/dynamicAuthenticationProviderStorageService.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
class TestAuthUsageService implements IAuthenticationUsageService {
_serviceBrand: undefined;
initializeExtensionUsageCache(): Promise<void> { return Promise.resolve(); }
extensionUsesAuth(extensionId: string): Promise<boolean> { return Promise.resolve(false); }
readAccountUsages(providerId: string, accountName: string): IAccountUsage[] { return []; }
removeAccountUsage(providerId: string, accountName: string): void { }
addAccountUsage(providerId: string, accountName: string, scopes: ReadonlyArray<string>, extensionId: string, extensionName: string): void { }
}
suite('MainThreadAuthentication', () => {
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
let mainThreadAuthentication: MainThreadAuthentication;
let instantiationService: TestInstantiationService;
let rpcProtocol: TestRPCProtocol;
setup(async () => {
// services
const services = new ServiceCollection();
services.set(ILogService, new SyncDescriptor(NullLogService));
services.set(IDialogService, new SyncDescriptor(TestDialogService, [{ confirmed: true }]));
services.set(IStorageService, new SyncDescriptor(TestStorageService));
services.set(ISecretStorageService, new SyncDescriptor(TestSecretStorageService));
services.set(IDynamicAuthenticationProviderStorageService, new SyncDescriptor(DynamicAuthenticationProviderStorageService));
services.set(IQuickInputService, new SyncDescriptor(TestQuickInputService));
services.set(IExtensionService, new SyncDescriptor(TestExtensionService));
services.set(IActivityService, new SyncDescriptor(TestActivityService));
services.set(IRemoteAgentService, new SyncDescriptor(TestRemoteAgentService));
services.set(INotificationService, new SyncDescriptor(TestNotificationService));
services.set(IHostService, new SyncDescriptor(TestHostService));
services.set(IUserActivityService, new SyncDescriptor(UserActivityService));
services.set(IAuthenticationAccessService, new SyncDescriptor(AuthenticationAccessService));
services.set(IAuthenticationService, new SyncDescriptor(AuthenticationService));
services.set(IAuthenticationUsageService, new SyncDescriptor(TestAuthUsageService));
services.set(IAuthenticationExtensionsService, new SyncDescriptor(AuthenticationExtensionsService));
instantiationService = disposables.add(new TestInstantiationService(services, undefined, undefined, true));
// stubs
// eslint-disable-next-line local/code-no-dangerous-type-assertions
instantiationService.stub(IOpenerService, {} as Partial<IOpenerService>);
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IBrowserWorkbenchEnvironmentService, TestEnvironmentService);
instantiationService.stub(IProductService, TestProductService);
rpcProtocol = disposables.add(new TestRPCProtocol());
mainThreadAuthentication = disposables.add(instantiationService.createInstance(MainThreadAuthentication, rpcProtocol));
rpcProtocol.set(MainContext.MainThreadAuthentication, mainThreadAuthentication);
});
test('provider registration completes without errors', async () => {
// Test basic registration - this should complete without throwing
await mainThreadAuthentication.$registerAuthenticationProvider({
id: 'test-provider',
label: 'Test Provider',
supportsMultipleAccounts: false
});
// Test unregistration - this should also complete without throwing
await mainThreadAuthentication.$unregisterAuthenticationProvider('test-provider');
// Success if we reach here without timeout
assert.ok(true, 'Registration and unregistration completed successfully');
});
test('event suppression during explicit unregistration', async () => {
let unregisterEventFired = false;
let eventProviderId: string | undefined;
// Mock the ext host to capture unregister events
const mockExtHost = {
$onDidUnregisterAuthenticationProvider: (id: string) => {
unregisterEventFired = true;
eventProviderId = id;
return Promise.resolve();
},
$getSessions: () => Promise.resolve([]),
// eslint-disable-next-line local/code-no-any-casts
$createSession: () => Promise.resolve({} as any),
$removeSession: () => Promise.resolve(),
$onDidChangeAuthenticationSessions: () => Promise.resolve(),
$registerDynamicAuthProvider: () => Promise.resolve('test'),
$onDidChangeDynamicAuthProviderTokens: () => Promise.resolve(),
$getSessionsFromChallenges: () => Promise.resolve([]),
// eslint-disable-next-line local/code-no-any-casts
$createSessionFromChallenges: () => Promise.resolve({} as any),
};
rpcProtocol.set(ExtHostContext.ExtHostAuthentication, mockExtHost);
// Register a provider
await mainThreadAuthentication.$registerAuthenticationProvider({
id: 'test-suppress',
label: 'Test Suppress',
supportsMultipleAccounts: false
});
// Reset the flag
unregisterEventFired = false;
eventProviderId = undefined;
// Unregister the provider - this should NOT fire the event due to suppression
await mainThreadAuthentication.$unregisterAuthenticationProvider('test-suppress');
// Verify the event was suppressed
assert.strictEqual(unregisterEventFired, false, 'Unregister event should be suppressed during explicit unregistration');
assert.strictEqual(eventProviderId, undefined, 'No provider ID should be captured from suppressed event');
});
test('concurrent provider registrations complete without errors', async () => {
// Register multiple providers simultaneously
const registrationPromises = [
mainThreadAuthentication.$registerAuthenticationProvider({
id: 'concurrent-1',
label: 'Concurrent 1',
supportsMultipleAccounts: false
}),
mainThreadAuthentication.$registerAuthenticationProvider({
id: 'concurrent-2',
label: 'Concurrent 2',
supportsMultipleAccounts: false
}),
mainThreadAuthentication.$registerAuthenticationProvider({
id: 'concurrent-3',
label: 'Concurrent 3',
supportsMultipleAccounts: false
})
];
await Promise.all(registrationPromises);
// Unregister all providers
const unregistrationPromises = [
mainThreadAuthentication.$unregisterAuthenticationProvider('concurrent-1'),
mainThreadAuthentication.$unregisterAuthenticationProvider('concurrent-2'),
mainThreadAuthentication.$unregisterAuthenticationProvider('concurrent-3')
];
await Promise.all(unregistrationPromises);
// Success if we reach here without timeout
assert.ok(true, 'Concurrent registrations and unregistrations completed successfully');
});
});