diff --git a/src/vs/platform/configuration/common/configurationRegistry.ts b/src/vs/platform/configuration/common/configurationRegistry.ts index 1f424d52cb7..11331ac829b 100644 --- a/src/vs/platform/configuration/common/configurationRegistry.ts +++ b/src/vs/platform/configuration/common/configurationRegistry.ts @@ -11,6 +11,7 @@ import * as types from 'vs/base/common/types'; import * as nls from 'vs/nls'; import { getLanguageTagSettingPlainKey } from 'vs/platform/configuration/common/configuration'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { PolicyName } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; export enum EditPresentationTypes { @@ -89,6 +90,11 @@ export interface IConfigurationRegistry { */ getConfigurationProperties(): IStringDictionary; + /** + * Return all configurations by policy name + */ + getPolicyConfigurations(): Map; + /** * Returns all excluded configurations settings of all configuration nodes contributed to this registry. */ @@ -127,12 +133,12 @@ export const enum ConfigurationScope { MACHINE_OVERRIDABLE, } -export interface PolicyConfiguration { +export interface IPolicy { /** * The policy name. */ - readonly name: string; + readonly name: PolicyName; /** * The Code version in which this policy was introduced. @@ -193,7 +199,7 @@ export interface IConfigurationPropertySchema extends IJSONSchema { * When specified, this setting's value can always be overwritten by * a system-wide policy. */ - policy?: PolicyConfiguration; + policy?: IPolicy; } export interface IExtensionInfo { @@ -245,6 +251,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { private readonly defaultLanguageConfigurationOverridesNode: IConfigurationNode; private readonly configurationContributors: IConfigurationNode[]; private readonly configurationProperties: IStringDictionary; + private readonly policyConfigurations: Map; private readonly excludedConfigurationProperties: IStringDictionary; private readonly resourceLanguageSettingsSchema: IJSONSchema; private readonly overrideIdentifiers = new Set(); @@ -265,6 +272,7 @@ class ConfigurationRegistry implements IConfigurationRegistry { this.configurationContributors = [this.defaultLanguageConfigurationOverridesNode]; this.resourceLanguageSettingsSchema = { properties: {}, patternProperties: {}, additionalProperties: false, errorMessage: 'Unknown editor configuration setting', allowTrailingCommas: true, allowComments: true }; this.configurationProperties = {}; + this.policyConfigurations = new Map(); this.excludedConfigurationProperties = {}; contributionRegistry.registerSchema(resourceLanguageSettingsSchemaId, this.resourceLanguageSettingsSchema); @@ -397,6 +405,10 @@ class ConfigurationRegistry implements IConfigurationRegistry { if (configuration.properties) { for (const key in configuration.properties) { properties.push(key); + const property = this.configurationProperties[key]; + if (property?.policy?.name) { + this.policyConfigurations.delete(property.policy.name); + } delete this.configurationProperties[key]; this.removeFromSchema(key, configuration.properties[key]); } @@ -421,12 +433,12 @@ class ConfigurationRegistry implements IConfigurationRegistry { let properties = configuration.properties; if (properties) { for (let key in properties) { - if (validate && validateProperty(key)) { + const property: IRegisteredConfigurationPropertySchema = properties[key]; + if (validate && validateProperty(key, property)) { delete properties[key]; continue; } - const property: IRegisteredConfigurationPropertySchema = properties[key]; property.source = extensionInfo; // update default value @@ -449,6 +461,9 @@ class ConfigurationRegistry implements IConfigurationRegistry { continue; } else { this.configurationProperties[key] = properties[key]; + if (properties[key].policy?.name) { + this.policyConfigurations.set(properties[key].policy!.name, key); + } } if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) { @@ -477,6 +492,10 @@ class ConfigurationRegistry implements IConfigurationRegistry { return this.configurationProperties; } + getPolicyConfigurations(): Map { + return this.policyConfigurations; + } + getExcludedConfigurationProperties(): IStringDictionary { return this.excludedConfigurationProperties; } @@ -647,7 +666,7 @@ export function getDefaultValue(type: string | string[] | undefined): any { const configurationRegistry = new ConfigurationRegistry(); Registry.add(Extensions.Configuration, configurationRegistry); -export function validateProperty(property: string): string | null { +export function validateProperty(property: string, schema: IRegisteredConfigurationPropertySchema): string | null { if (!property.trim()) { return nls.localize('config.property.empty', "Cannot register an empty property"); } @@ -657,6 +676,9 @@ export function validateProperty(property: string): string | null { if (configurationRegistry.getConfigurationProperties()[property] !== undefined) { return nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", property); } + if (schema.policy?.name && configurationRegistry.getPolicyConfigurations().get(schema.policy?.name) !== undefined) { + return nls.localize('config.policy.duplicate', "Cannot register '{0}'. The associated policy {1} is already registered with {2}.", property, schema.policy?.name, configurationRegistry.getPolicyConfigurations().get(schema.policy?.name)); + } return null; } diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index 9e9603e9c61..a9cf1f256f9 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -15,7 +15,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { FilePolicyService } from 'vs/platform/policy/common/filePolicyService'; -import { IPolicyService, NullPolicyService, PolicyName } from 'vs/platform/policy/common/policy'; +import { IPolicyService, NullPolicyService, PolicyName, PolicyValue } from 'vs/platform/policy/common/policy'; import { Registry } from 'vs/platform/registry/common/platform'; export class DefaultConfiguration extends Disposable { @@ -86,7 +86,6 @@ export class PolicyConfiguration extends Disposable { readonly onDidChangeConfiguration = this._onDidChangeConfiguration.event; private readonly policyService: IPolicyService; - private readonly policyNamesToKeys = new Map(); private _configurationModel = new ConfigurationModel(); get configurationModel() { return this._configurationModel; } @@ -103,53 +102,55 @@ export class PolicyConfiguration extends Disposable { async initialize(): Promise { await this.policyService.initialize(); - this.updateKeys(this.defaultConfiguration.configurationModel.keys, false); + this.update(this.defaultConfiguration.configurationModel.keys, false); this._register(this.policyService.onDidChange(policyNames => this.onDidChangePolicies(policyNames))); - this._register(this.defaultConfiguration.onDidChangeConfiguration(({ properties }) => this.updateKeys(properties, true))); + this._register(this.defaultConfiguration.onDidChangeConfiguration(({ properties }) => this.update(properties, true))); return this._configurationModel; } - private updateKeys(keys: string[], trigger: boolean): void { - const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const keyPolicyNamePairs: [string, PolicyName][] = coalesce(keys.map(key => { - const policyName = configurationProperties[key].policy?.name; - return policyName ? [key, policyName] : undefined; - })); - this.update(keyPolicyNamePairs, trigger); + async reload(): Promise { + await this.policyService.refresh(); + this.update(this.defaultConfiguration.configurationModel.keys, false); + return this._configurationModel; } private onDidChangePolicies(policyNames: readonly PolicyName[]): void { - const keyPolicyNamePairs: [string, PolicyName][] = coalesce(policyNames.map(policyName => { - const key = this.policyNamesToKeys.get(policyName); - return key ? [key, policyName] : undefined; - })); - this.update(keyPolicyNamePairs, true); + const policyConfigurations = Registry.as(Extensions.Configuration).getPolicyConfigurations(); + const keys = coalesce(policyNames.map(policyName => policyConfigurations.get(policyName))); + this.update(keys, true); } - private update(keyPolicyNamePairs: [string, PolicyName][], trigger: boolean): void { - if (!keyPolicyNamePairs.length) { - return; - } + private update(keys: string[], trigger: boolean): void { + const configurationProperties = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const changed: [string, PolicyValue | undefined][] = []; + const wasEmpty = this._configurationModel.isEmpty(); - const updated: string[] = []; - this._configurationModel = this._configurationModel.isFrozen() ? this._configurationModel.clone() : this._configurationModel; - const isEmpty = this._configurationModel.isEmpty(); - for (const [key, policyName] of keyPolicyNamePairs) { - this.policyNamesToKeys.set(policyName, key); - const value = this.policyService.getPolicyValue(policyName); - if (!isEmpty && equals(this._configurationModel.getValue(key), value)) { - continue; - } - if (value === undefined) { - this._configurationModel.removeValue(key); + for (const key of keys) { + const policyName = configurationProperties[key]?.policy?.name; + if (policyName) { + const policyValue = this.policyService.getPolicyValue(policyName); + if (wasEmpty ? policyValue !== undefined : !equals(this._configurationModel.getValue(key), policyValue)) { + changed.push([key, policyValue]); + } } else { - this._configurationModel.setValue(key, value); + if (this._configurationModel.getValue(key) !== undefined) { + changed.push([key, undefined]); + } } - updated.push(key); } - if (updated.length && trigger) { - this._onDidChangeConfiguration.fire(this._configurationModel); + if (changed.length) { + this._configurationModel = this._configurationModel.isFrozen() ? this._configurationModel.clone() : this._configurationModel; + for (const [key, policyValue] of changed) { + if (policyValue === undefined) { + this._configurationModel.removeValue(key); + } else { + this._configurationModel.setValue(key, policyValue); + } + } + if (trigger) { + this._onDidChangeConfiguration.fire(this._configurationModel); + } } } diff --git a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts index 83314c83ad3..8e836674dae 100644 --- a/src/vs/platform/configuration/test/common/configurationRegistry.test.ts +++ b/src/vs/platform/configuration/test/common/configurationRegistry.test.ts @@ -50,4 +50,30 @@ suite('ConfigurationRegistry', () => { assert.deepStrictEqual(configurationRegistry.getConfigurationProperties()['config'].default, { a: 2, c: 3 }); }); + + test('registering multiple settings with same policy', async () => { + configurationRegistry.registerConfiguration({ + 'id': '_test_default', + 'type': 'object', + 'properties': { + 'policy1': { + 'type': 'object', + policy: { + name: 'policy', + minimumVersion: '1.0.0' + } + }, + 'policy2': { + 'type': 'object', + policy: { + name: 'policy', + minimumVersion: '1.0.0' + } + } + } + }); + const actual = configurationRegistry.getConfigurationProperties(); + assert.ok(actual['policy1'] !== undefined); + assert.ok(actual['policy2'] === undefined); + }); }); diff --git a/src/vs/platform/configuration/test/common/policyConfiguration.test.ts b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts new file mode 100644 index 00000000000..3f6e15bdd0c --- /dev/null +++ b/src/vs/platform/configuration/test/common/policyConfiguration.test.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { DefaultConfiguration, PolicyConfiguration } from 'vs/platform/configuration/common/configurations'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { Extensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { deepClone } from 'vs/base/common/objects'; + +suite('PolicyConfiguration', () => { + + let testObject: PolicyConfiguration; + let fileService: IFileService; + const policyFile = URI.file('policyFile').with({ scheme: 'vscode-tests' }); + const disposables = new DisposableStore(); + const policyConfigurationNode: IConfigurationNode = { + 'id': 'policyConfiguration', + 'order': 1, + 'title': 'a', + 'type': 'object', + 'properties': { + 'policy.settingA': { + 'type': 'boolean', + 'default': true, + policy: { + name: 'PolicySettingA', + minimumVersion: '1.0.0', + } + }, + 'nonPolicy.setting': { + 'type': 'boolean', + 'default': true + } + } + }; + + suiteSetup(() => Registry.as(Extensions.Configuration).registerConfiguration(policyConfigurationNode)); + suiteTeardown(() => Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode])); + + setup(async () => { + const defaultConfiguration = disposables.add(new DefaultConfiguration()); + await defaultConfiguration.initialize(); + fileService = disposables.add(new FileService(new NullLogService())); + const diskFileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + fileService.registerProvider(policyFile.scheme, diskFileSystemProvider); + testObject = disposables.add(new PolicyConfiguration(defaultConfiguration, fileService, { policyFile } as IEnvironmentService, new NullLogService())); + }); + + teardown(() => disposables.clear()); + + test('initialize: with policies', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.strictEqual(acutal.getValue('policy.settingA'), false); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('initialize: no policies', async () => { + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + }); + + test('initialize: with policies but not registered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false, 'PolicySettingB': false }))); + + await testObject.initialize(); + const acutal = testObject.configurationModel; + + assert.strictEqual(acutal.getValue('policy.settingA'), false); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is added', async () => { + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false }))); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), false); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is updated', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': true }))); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), true); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingA']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy is removed', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({}))); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy setting is registered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingB': false }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + policyConfigurationNode.properties!['policy.settingB'] = { + 'type': 'boolean', + 'default': true, + policy: { + name: 'PolicySettingB', + minimumVersion: '1.0.0', + } + }; + Registry.as(Extensions.Configuration).registerConfiguration(deepClone(policyConfigurationNode)); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingB'), false); + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, ['policy.settingB']); + assert.deepStrictEqual(acutal.overrides, []); + }); + + test('change: when policy setting is deregistered', async () => { + await fileService.writeFile(policyFile, VSBuffer.fromString(JSON.stringify({ 'PolicySettingA': false }))); + await testObject.initialize(); + + const promise = Event.toPromise(testObject.onDidChangeConfiguration); + Registry.as(Extensions.Configuration).deregisterConfigurations([policyConfigurationNode]); + await promise; + + const acutal = testObject.configurationModel; + assert.strictEqual(acutal.getValue('policy.settingA'), undefined); + assert.strictEqual(acutal.getValue('nonPolicy.setting'), undefined); + assert.deepStrictEqual(acutal.keys, []); + assert.deepStrictEqual(acutal.overrides, []); + }); + +}); diff --git a/src/vs/platform/policy/common/filePolicyService.ts b/src/vs/platform/policy/common/filePolicyService.ts index 0cfa9e1588c..4779fe77c0a 100644 --- a/src/vs/platform/policy/common/filePolicyService.ts +++ b/src/vs/platform/policy/common/filePolicyService.ts @@ -50,8 +50,8 @@ export class FilePolicyService extends Disposable implements IPolicyService { await this.doRefresh(); } - private refresh(): void { - this.throttledDelayer.trigger(() => this.doRefresh()); + async refresh(): Promise { + await this.throttledDelayer.trigger(() => this.doRefresh()); } private async read(): Promise { diff --git a/src/vs/platform/policy/common/policy.ts b/src/vs/platform/policy/common/policy.ts index 4febe66904f..6963bf22081 100644 --- a/src/vs/platform/policy/common/policy.ts +++ b/src/vs/platform/policy/common/policy.ts @@ -12,12 +12,14 @@ export type Policies = Map; export interface IPolicyService { readonly onDidChange: Event; initialize(): Promise; + refresh(): Promise; getPolicyValue(name: PolicyName): PolicyValue | undefined; } export class NullPolicyService implements IPolicyService { readonly onDidChange = Event.None; async initialize() { } + async refresh() { } getPolicyValue() { return undefined; } } @@ -33,6 +35,10 @@ export class MultiPolicyService implements IPolicyService { await Promise.all(this.policyServices.map(p => p.initialize())); } + async refresh() { + await Promise.all(this.policyServices.map(p => p.refresh())); + } + getPolicyValue(name: PolicyName) { for (const policyService of this.policyServices) { const result = policyService.getPolicyValue(name); diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 406661573d2..17d42a088fa 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -204,7 +204,8 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { configuration.properties = {}; } for (let key in properties) { - const message = validateProperty(key); + const propertyConfiguration = properties[key]; + const message = validateProperty(key, propertyConfiguration); if (message) { delete properties[key]; extension.collector.warn(message); @@ -215,7 +216,6 @@ configurationExtPoint.setHandler((extensions, { added, removed }) => { extension.collector.warn(nls.localize('config.property.duplicate', "Cannot register '{0}'. This property is already registered.", key)); continue; } - const propertyConfiguration = properties[key]; if (!isObject(propertyConfiguration)) { delete properties[key]; extension.collector.error(nls.localize('invalid.property', "configuration.properties property '{0}' must be an object", key)); diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index a4c11121290..0db722acbc1 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -342,6 +342,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat async reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise { if (target === undefined) { + this.reloadDefaultConfiguration(); + await this.reloadPolicyConfiguration(); const { local, remote } = await this.reloadUserConfiguration(); await this.reloadWorkspaceConfiguration(); await this.loadConfiguration(local, remote); @@ -355,7 +357,8 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat switch (target) { case ConfigurationTarget.DEFAULT: - await this.reloadDefaultConfiguration(); + this.reloadDefaultConfiguration(); + await this.reloadPolicyConfiguration(); return; case ConfigurationTarget.USER: { @@ -596,10 +599,14 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat return { local, remote }; } - private async reloadDefaultConfiguration(): Promise { + private reloadDefaultConfiguration(): void { this.onDefaultConfigurationChanged(this.defaultConfiguration.reload()); } + private async reloadPolicyConfiguration(): Promise { + this.onPolicyConfigurationChanged(await this.policyConfiguration.reload()); + } + private async reloadUserConfiguration(): Promise<{ local: ConfigurationModel; remote: ConfigurationModel }> { const [local, remote] = await Promise.all([this.reloadLocalUserConfiguration(true), this.reloadRemoteUserConfiguration(true)]); return { local, remote }; diff --git a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts index 7d7e3cb1267..2fba810064f 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationEditingService.test.ts @@ -37,8 +37,9 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { joinPath } from 'vs/base/common/resources'; import { VSBuffer } from 'vs/base/common/buffer'; import { RemoteAgentService } from 'vs/workbench/services/remote/browser/remoteAgentService'; -import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { getSingleFolderWorkspaceIdentifier } from 'vs/workbench/services/workspaces/browser/workspaces'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { hash } from 'vs/base/common/hash'; const ROOT = URI.file('tests').with({ scheme: 'vscode-tests' }); @@ -52,7 +53,7 @@ export class ConfigurationCache implements IConfigurationCache { suite('ConfigurationEditingService', () => { let instantiationService: TestInstantiationService; - let environmentService: BrowserWorkbenchEnvironmentService; + let environmentService: IWorkbenchEnvironmentService; let fileService: IFileService; let workspaceService: WorkspaceService; let testObject: ConfigurationEditingService; @@ -76,6 +77,14 @@ suite('ConfigurationEditingService', () => { 'configurationEditing.service.testSettingThree': { 'type': 'string', 'default': 'isSet' + }, + 'configurationEditing.service.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationEditing.service.policySetting', + minimumVersion: '1.0.0', + } } } }); @@ -92,12 +101,17 @@ suite('ConfigurationEditingService', () => { instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(workspaceFolder, 'policies.json'); instantiationService.stub(IEnvironmentService, environmentService); const remoteAgentService = disposables.add(instantiationService.createInstance(RemoteAgentService, null)); disposables.add(fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, logService)))); instantiationService.stub(IFileService, fileService); instantiationService.stub(IRemoteAgentService, remoteAgentService); workspaceService = disposables.add(new WorkspaceService({ configurationCache: new ConfigurationCache() }, environmentService, fileService, remoteAgentService, new UriIdentityService(fileService), new NullLogService())); + await workspaceService.initialize({ + id: hash(workspaceFolder.toString()).toString(16), + uri: workspaceFolder + }); instantiationService.stub(IWorkspaceContextService, workspaceService); await workspaceService.initialize(getSingleFolderWorkspaceIdentifier(workspaceFolder)); @@ -180,6 +194,25 @@ suite('ConfigurationEditingService', () => { assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'); }); + test('errors cases - ERROR_POLICY_CONFIGURATION', async () => { + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationEditing.service.policySetting": "policyValue" }')); + await instantiationService.get(IConfigurationService).reloadConfiguration(); + try { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + } catch (error) { + assert.strictEqual(error.code, ConfigurationEditingErrorCode.ERROR_POLICY_CONFIGURATION); + return; + } + assert.fail('Should fail with ERROR_POLICY_CONFIGURATION'); + }); + + test('write policy setting - when not set', async () => { + await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.policySetting', value: 'value' }, { donotNotifyError: true }); + const contents = await fileService.readFile(environmentService.settingsResource); + const parsed = json.parse(contents.value.toString()); + assert.strictEqual(parsed['configurationEditing.service.policySetting'], 'value'); + }); + test('write one setting - empty file', async () => { await testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }); const contents = await fileService.readFile(environmentService.settingsResource); diff --git a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts index 352fd8e9d7e..5a207dccd6d 100644 --- a/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/browser/configurationService.test.ts @@ -669,7 +669,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: BrowserWorkbenchEnvironmentService; + let testObject: WorkspaceService, workspaceService: WorkspaceService, fileService: IFileService, environmentService: IWorkbenchEnvironmentService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); const disposables: DisposableStore = new DisposableStore(); @@ -708,6 +708,14 @@ suite('WorkspaceConfigurationService - Folder', () => { 'default': 'isSet', restricted: true }, + 'configurationService.folder.policySetting': { + 'type': 'string', + 'default': 'isSet', + policy: { + name: 'configurationService.folder.policySetting', + minimumVersion: '1.0.0', + } + }, } }); @@ -731,6 +739,7 @@ suite('WorkspaceConfigurationService - Folder', () => { const instantiationService = workbenchInstantiationService(undefined, disposables); environmentService = TestEnvironmentService; + environmentService.policyFile = joinPath(folder, 'policies.json'); const remoteAgentService = instantiationService.createInstance(RemoteAgentService, null); instantiationService.stub(IRemoteAgentService, remoteAgentService); fileService.registerProvider(Schemas.vscodeUserData, disposables.add(new FileUserDataProvider(ROOT.scheme, fileSystemProvider, Schemas.vscodeUserData, new NullLogService()))); @@ -750,7 +759,7 @@ suite('WorkspaceConfigurationService - Folder', () => { teardown(() => disposables.clear()); test('defaults', () => { - assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet' } }); + assert.deepStrictEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet', 'restrictedSetting': 'isSet', 'policySetting': 'isSet' } }); }); test('globals override defaults', async () => { @@ -956,6 +965,23 @@ suite('WorkspaceConfigurationService - Folder', () => { assert.strictEqual(testObject.getValue('configurationService.folder.machineSetting-3', { resource: workspaceService.getWorkspace().folders[0].uri }), 'userValue'); }); + test('policy value override all', async () => { + await fileService.writeFile(environmentService.policyFile!, VSBuffer.fromString('{ "configurationService.folder.policySetting": "policyValue" }')); + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policySetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.policySetting": "workspaceValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'policyValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, 'policyValue'); + }); + + test('policy settings when policy value is not set', async () => { + await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "configurationService.folder.policySetting": "userValue" }')); + await fileService.writeFile(joinPath(workspaceService.getWorkspace().folders[0].uri, '.vscode', 'settings.json'), VSBuffer.fromString('{ "configurationService.folder.policySetting": "workspaceValue" }')); + await testObject.reloadConfiguration(); + assert.strictEqual(testObject.getValue('configurationService.folder.policySetting'), 'workspaceValue'); + assert.strictEqual(testObject.inspect('configurationService.folder.policySetting').policyValue, undefined); + }); + test('reload configuration emits events after global configuraiton changes', async () => { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString('{ "testworkbench.editor.tabs": true }')); const target = sinon.spy();