winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 51943ee788348553665b6131104c0da8f16ca518
parent 82f994d0692d4b9fbbcbc1913d83971f70d46414
Author: Luis Chacón <lechacon@users.noreply.github.com>
Date:   Wed,  7 Apr 2021 11:24:27 -0700

Add Group Policy E2E tests (#854)


Diffstat:
M.github/actions/spelling/allow.txt | 3+++
Msrc/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj | 10++++++++--
Msrc/AppInstallerCLIE2ETests/Constants.cs | 30+++++++++++++++++-------------
Asrc/AppInstallerCLIE2ETests/GroupPolicy.cs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/GroupPolicyHelper.cs | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/ImportCommand.cs | 6+++---
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 10++++++++--
7 files changed, 494 insertions(+), 20 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -161,6 +161,7 @@ IBuffer icu IDisposable IDX +IEnumerable ifdef ifndef ifstream @@ -511,6 +512,8 @@ xamarin xlang xml xmlns +XElement +XName XResource xsd XTOKEN diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -9,8 +9,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Msix.Utils" Version="1.0.200812001" /> - <PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" /> + <PackageReference Include="Microsoft.Msix.Utils" Version="2.0.3" /> + <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="nunit" Version="3.12.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.15.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> @@ -31,6 +31,12 @@ </ItemGroup> <ItemGroup> + <Content Include="..\..\doc\admx\DesktopAppInstaller.admx" Link="TestData\DesktopAppInstaller.admx"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + </ItemGroup> + + <ItemGroup> <ProjectReference Include="..\IndexCreationTool\IndexCreationTool.csproj" /> </ItemGroup> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -108,19 +108,23 @@ namespace AppInstallerCLIE2ETests public const int ERROR_LIBYAML_ERROR = unchecked((int)0x8A150027); public const int ERROR_MANIFEST_VALIDATION_WARNING = unchecked((int)0x8A150028); public const int ERROR_MANIFEST_VALIDATION_FAILURE = unchecked((int)0x8A150029); - public const int APPINSTALLER_CLI_ERROR_INVALID_MANIFEST = unchecked((int)0x8A15002A); - public const int APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE = unchecked((int)0x8A15002B); - public const int APPINSTALLER_CLI_ERROR_UPDATE_ALL_HAS_FAILURE = unchecked((int)0x8A15002C); - public const int APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED = unchecked((int)0x8A15002D); - public const int APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH = unchecked((int)0x8A15002E); - public const int APPINSTALLER_CLI_ERROR_NO_UNINSTALL_INFO_FOUND = unchecked((int)0x8a15002F); - public const int APPINSTALLER_CLI_ERROR_EXEC_UNINSTALL_COMMAND_FAILED = unchecked((int)0x8a150030); - public const int APPINSTALLER_CLI_ERROR_ICU_BREAK_ITERATOR_ERROR = unchecked((int)0x8A150031); - public const int APPINSTALLER_CLI_ERROR_ICU_CASEMAP_ERROR = unchecked((int)0x8A150032); - public const int APPINSTALLER_CLI_ERROR_ICU_REGEX_ERROR = unchecked((int)0x8A150033); - public const int APPINSTALLER_CLI_ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150034); - public const int APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150035); - public const int APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150036); + public const int ERROR_INVALID_MANIFEST = unchecked((int)0x8A15002A); + public const int ERROR_UPDATE_NOT_APPLICABLE = unchecked((int)0x8A15002B); + public const int ERROR_UPDATE_ALL_HAS_FAILURE = unchecked((int)0x8A15002C); + public const int ERROR_INSTALLER_SECURITY_CHECK_FAILED = unchecked((int)0x8A15002D); + public const int ERROR_DOWNLOAD_SIZE_MISMATCH = unchecked((int)0x8A15002E); + public const int ERROR_NO_UNINSTALL_INFO_FOUND = unchecked((int)0x8a15002F); + public const int ERROR_EXEC_UNINSTALL_COMMAND_FAILED = unchecked((int)0x8a150030); + public const int ERROR_ICU_BREAK_ITERATOR_ERROR = unchecked((int)0x8A150031); + public const int ERROR_ICU_CASEMAP_ERROR = unchecked((int)0x8A150032); + public const int ERROR_ICU_REGEX_ERROR = unchecked((int)0x8A150033); + public const int ERROR_IMPORT_INSTALL_FAILED = unchecked((int)0x8a150034); + public const int ERROR_NOT_ALL_PACKAGES_FOUND = unchecked((int)0x8a150035); + public const int ERROR_JSON_INVALID_FILE = unchecked((int)0x8a150036); + public const int ERROR_SOURCE_NOT_REMOTE = unchecked((int)0x8A150037); + public const int ERROR_UNSUPPORTED_RESTSOURCE = unchecked((int)0x8A150038); + public const int ERROR_RESTSOURCE_INVALID_DATA = unchecked((int)0x8A150039); + public const int ERROR_BLOCKED_BY_POLICY = unchecked((int)0x8a15003A); } } } diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using NUnit.Framework; + +namespace AppInstallerCLIE2ETests +{ + /// <summary> + /// Tests for enforcement of Group Policy. + /// Behavior is better tested in the unit tests; these tests mostly ensure match between the code and the definition. + /// </summary> + public class GroupPolicy : BaseCommand + { + [SetUp] + public void Setup() + { + InitializeAllFeatures(false); + GroupPolicyHelper.DeleteExistingPolicies(); + } + + [TearDown] + public void TearDown() + { + InitializeAllFeatures(false); + GroupPolicyHelper.DeleteExistingPolicies(); + } + + [Test] + public void PolicyEnableWinget() + { + GroupPolicyHelper.EnableWinget.Disable(); + var result = TestCommon.RunAICLICommand("search", string.Empty); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } + + [Test] + public void EnableSettings() + { + GroupPolicyHelper.EnableSettings.Disable(); + var result = TestCommon.RunAICLICommand("settings", string.Empty); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } + + [Test] + public void EnableExperimentalFeatures() + { + ConfigureFeature("experimentalCmd", true); + var result = TestCommon.RunAICLICommand("experimental", string.Empty); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + + // An experimental feature disabled by Group Policy behaves the same as one that is not enabled. + // The expected result is a command line error as the argument validation rejects this. + GroupPolicyHelper.EnableExperimentalFeatures.Disable(); + result = TestCommon.RunAICLICommand("experimental", string.Empty); + Assert.AreEqual(Constants.ErrorCode.ERROR_INVALID_CL_ARGUMENTS, result.ExitCode); + } + + [Test] + public void EnableLocalManifests() + { + GroupPolicyHelper.EnableLocalManifests.Disable(); + var result = TestCommon.RunAICLICommand("install", $"-m {TestCommon.GetTestDataFile(@"Manifests\TestExeInstaller.yaml")}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } + + [Test] + public void EnableHashOverride() + { + GroupPolicyHelper.EnableHashOverride.Disable(); + var result = TestCommon.RunAICLICommand("install", "AnyPackage --force"); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } + + [Test] + public void EnableDefaultSource() + { + // Default sources are disabled during setup so they are missing. + var result = TestCommon.RunAICLICommand("source list", "winget"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + GroupPolicyHelper.EnableDefaultSource.Enable(); + result = TestCommon.RunAICLICommand("source list", "winget"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + } + + [Test] + public void EnableMicrosoftStoreSource() + { + // Default sources are disabled during setup so they are missing. + var result = TestCommon.RunAICLICommand("source list", "msstore"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + GroupPolicyHelper.EnableMicrosoftStoreSource.Enable(); + result = TestCommon.RunAICLICommand("source list", "msstore"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + } + + [Test] + public void EnableAdditionalSources() + { + // Remove the test source, then add it with policy. + TestCommon.RunAICLICommand("source remove", "TestSource"); + var result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new string[] + { + "{\"Arg\":\"https://localhost:5001/TestKit\",\"Data\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Identifier\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Name\":\"TestSource\",\"Type\":\"Microsoft.PreIndexed.Package\"}", + }); + + result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + } + + [Test] + public void EnableAllowedSources() + { + // Try listing the test source. We should only see it if it is allowed. + // With allowed sources disabled: + GroupPolicyHelper.EnableAllowedSources.Disable(); + var result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + // With allowed sources enabled, but not listing the test source: + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new string[] + { + "{\"Arg\":\"An argument\",\"Data\":\"Some data\",\"Identifier\":\"Test id\",\"Name\":\"NotTestSource\",\"Type\":\"Microsoft.PreIndexed.Package\"}", + }); + + result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); + + // With the test source allowed: + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new string[] + { + "{\"Arg\":\"https://localhost:5001/TestKit\",\"Data\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Identifier\":\"WingetE2E.Tests_8wekyb3d8bbwe\",\"Name\":\"TestSource\",\"Type\":\"Microsoft.PreIndexed.Package\"}", + }); + + result = TestCommon.RunAICLICommand("source list", "TestSource"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + } + + [Test] + public void SourceAutoUpdateInterval() + { + // Test this policy by inspecting the result of --info + GroupPolicyHelper.SourceAutoUpdateInterval.SetEnabledValue(123); + var result = TestCommon.RunAICLICommand(string.Empty, "--info"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.IsTrue(result.StdOut.Contains("Source Auto Update Interval In Minutes 123")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Win32; + +namespace AppInstallerCLIE2ETests +{ + /// <summary> + /// Helper for setting Group Policy settings. + /// This helper reads the keys and values to use directly from the ADMX file to ensure that the names + /// used by the source code are correct. + /// </summary> + /// <remarks> + /// This helper modifies the policies for winget configured in the machine. + /// </remarks> + public class GroupPolicyHelper + { + private const string PoliciesDefinitionFileName = "DesktopAppInstaller.admx"; + private static Lazy<XElement> policyDefinitions = new Lazy<XElement>(() => + { + string filePath = TestCommon.GetTestDataFile(PoliciesDefinitionFileName); + string fileText = File.ReadAllText(filePath); + return XElement.Parse(fileText); + }); + + /// <summary> + /// Names of the XML elements and attributes that make up the definition file. + /// </summary> + private static class XmlNames + { + private const string Namespace = "http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions"; + + // Root element + public static readonly XName PolicyDefinitions = XName.Get("policyDefinitions", Namespace); + + public static readonly XName Policies = XName.Get("policies", Namespace); + public static readonly XName Policy = XName.Get("policy", Namespace); + + public static readonly XName EnabledValue = XName.Get("enabledValue", Namespace); + public static readonly XName DisabledValue = XName.Get("disabledValue", Namespace); + public static readonly XName Elements = XName.Get("elements", Namespace); + + public static readonly XName Decimal = XName.Get("decimal", Namespace); + public static readonly XName List = XName.Get("list", Namespace); + + public static class Attributes + { + public const string Name = "name"; + public const string Value = "value"; + public const string Id = "id"; + public const string Key = "key"; + public const string ValueName = "valueName"; + } + } + + /// <summary> + /// Name of the policy. Used to identify it in the file. + /// </summary> + private string name; + + /// <summary> + /// ID of the value element of this policy (if it has one). + /// This assumes that each policy has a single value element. + /// </summary> + private string elementId; + + // Policies available. + public static GroupPolicyHelper EnableWinget = new GroupPolicyHelper("EnableAppInstaller"); + public static GroupPolicyHelper EnableSettings = new GroupPolicyHelper("EnableSettings"); + public static GroupPolicyHelper EnableExperimentalFeatures = new GroupPolicyHelper("EnableExperimentalFeatures"); + public static GroupPolicyHelper EnableLocalManifests = new GroupPolicyHelper("EnableLocalManifestFiles"); + public static GroupPolicyHelper EnableHashOverride = new GroupPolicyHelper("EnableHashOverride"); + public static GroupPolicyHelper EnableDefaultSource = new GroupPolicyHelper("EnableDefaultSource"); + public static GroupPolicyHelper EnableMicrosoftStoreSource = new GroupPolicyHelper("EnableMicrosoftStoreSource"); + public static GroupPolicyHelper EnableAdditionalSources = new GroupPolicyHelper("EnableAdditionalSources", "AdditionalSources"); + public static GroupPolicyHelper EnableAllowedSources = new GroupPolicyHelper("EnableAllowedSources", "AllowedSources"); + public static GroupPolicyHelper SourceAutoUpdateInterval = new GroupPolicyHelper("SourceAutoUpdateIntervalInMinutes", "SourceAutoUpdateIntervalInMinutes"); + + private static GroupPolicyHelper[] AllPolicies = new GroupPolicyHelper[] + { + EnableWinget, + EnableSettings, + EnableExperimentalFeatures, + EnableLocalManifests, + EnableHashOverride, + EnableDefaultSource, + EnableMicrosoftStoreSource, + EnableAdditionalSources, + EnableAllowedSources, + SourceAutoUpdateInterval, + }; + + private GroupPolicyHelper(string name) + { + this.name = name; + } + + private GroupPolicyHelper(string name, string elementId) + { + this.name = name; + this.elementId = elementId; + } + + /// <summary> + /// Gets the content of the ADMX file as an XML. + /// </summary> + private static XElement PolicyDefinitions => policyDefinitions.Value; + + /// <summary> + /// Gets the XML element that defines this policy. + /// </summary> + // The XML structure is like this: + // <policyDefinitions ...> + // ... + // <policies> + // <policy name="..." ... /> + // </policies> + // </policyDefinitions> + private XElement PolicyElement => PolicyDefinitions + .Element(XmlNames.Policies) + .Elements(XmlNames.Policy) + .First(policy => policy.Attribute(XmlNames.Attributes.Name).Value == this.name); + + /// <summary> + /// Gets the path to the registry key that backs this policy. + /// </summary> + private string KeyPath => this.PolicyElement.Attribute(XmlNames.Attributes.Key).Value; + + /// <summary> + /// Gets the name of the registry value that backs this policy. + /// </summary> + private string ValueName => this.PolicyElement.Attribute(XmlNames.Attributes.ValueName)?.Value; + + /// <summary> + /// Gets the XElement that defines the single value element of this policy. + /// This only works if the policy has a single element for its value. + /// </summary> + // Looks for something like this: + // <policy> + // <elements> + // <something id="..." /> + // </elements> + // </policy> + // We use only list and decimal elements. + private XElement ValueElement => this.PolicyElement + .Element(XmlNames.Elements) + .Elements() + .First(element => element.Attribute(XmlNames.Attributes.Id).Value == this.elementId); + + /// <summary> + /// Deletes all of the existing policies from the registry. + /// </summary> + public static void DeleteExistingPolicies() + { + foreach (var policy in AllPolicies) + { + policy.SetNotConfigured(); + } + } + + /// <summary> + /// Sets the policy to the Enabled state. + /// This will fail if the policy's EnabledValue does not exist or is not exactly as expected. + /// </summary> + public void Enable() + { + // The expected format is like this: + // <enabledValue> + // <decimal value="1" /> + // </enabledValue> + // We expect the value to always be 1, but still parse it to catch errors in the ADMX. + int enabledValue = GetDecimalValue(this.PolicyElement.Element(XmlNames.EnabledValue)); + using (RegistryKey key = this.GetKey()) + { + key.SetValue(this.ValueName, enabledValue); + } + } + + /// <summary> + /// Sets the policy to the Disabled state. + /// This will fail if the policy's DisabledValue does not exist or is not exactly as expected. + /// </summary> + public void Disable() + { + // The expected format is like this: + // <enabledValue> + // <decimal value="0" /> + // </enabledValue> + // We expect the value to always be 0, but still parse it to catch errors in the ADMX. + int disabledValue = GetDecimalValue(this.PolicyElement.Element(XmlNames.DisabledValue)); + using (RegistryKey key = this.GetKey()) + { + key.SetValue(this.ValueName, disabledValue); + } + } + + /// <summary> + /// Sets the policy to the Not Configured state. + /// This deletes the value associated with the policy, including its list if it has one. + /// </summary> + public void SetNotConfigured() + { + // Delete the enabled/disabled value + if (this.ValueName != null) + { + using (RegistryKey key = this.GetKey()) + { + key.DeleteValue(this.ValueName, throwOnMissingValue: false); + } + } + + // Delete the value element + if (this.elementId != null) + { + if (this.ValueElement.Name == XmlNames.List) + { + // Lists are stored in separate keys. + Registry.LocalMachine.DeleteSubKeyTree(this.ValueElement.Attribute(XmlNames.Attributes.Key).Value, throwOnMissingSubKey: false); + } + else if (this.ValueElement.Name == XmlNames.Decimal) + { + // Decimals are stored in single values + using (RegistryKey key = this.GetKey()) + { + key.DeleteValue(this.ValueElement.Attribute(XmlNames.Attributes.ValueName).Value, throwOnMissingValue: false); + } + } + } + } + + /// <summary> + /// Sets the value of the policy when enabled. + /// This uses only the "elements" of the policy, not the "enabledValue". + /// The type used in the registry is chosen automatically. + /// </summary> + /// <param name="value">Value of the policy.</param> + public void SetEnabledValue(object value) + { + using (RegistryKey key = this.GetKey()) + { + key.SetValue( + this.ValueElement.Attribute(XmlNames.Attributes.ValueName).Value, + value); + } + } + + /// <summary> + /// Sets the list value of the policy when enabled. + /// This sets from the "elements" and also sets the enabled value as lists are also gated by a toggle. + /// This will fail if the value of the policy is not a list. + /// </summary> + /// <param name="values">Values to set in the list.</param> + public void SetEnabledList(IEnumerable<string> values) + { + this.Enable(); + + // Delete the existing list + string listKeyPath = this.ValueElement.Attribute(XmlNames.Attributes.Key).Value; + Registry.LocalMachine.DeleteSubKeyTree(listKeyPath, throwOnMissingSubKey: false); + + // Create and fill the key. + // This assumes that the values don't need to have special names or prefixes. + var listKey = Registry.LocalMachine.CreateSubKey(listKeyPath); + int index = 0; + foreach (string value in values) + { + listKey.SetValue(index++.ToString(), value); + } + + listKey.Close(); + } + + /// <summary> + /// Gets the value from a "decimal" child element. + /// </summary> + /// <param name="element">Element containing the decimal.</param> + /// <returns>Value in the element.</returns> + private static int GetDecimalValue(XElement element) + { + // Reads a child that looks like this: + // <decimal value="X" /> + return int.Parse(element.Element(XmlNames.Decimal).Attribute(XmlNames.Attributes.Value).Value); + } + + /// <summary> + /// Gets the registry key backing this policy. + /// </summary> + /// <remarks> + /// This assumes that all the policies are machine-wide. + /// If this changes, we will need to parse the class="machine|user" attribute. + /// </remarks> + private RegistryKey GetKey() + { + return Registry.LocalMachine.CreateSubKey(this.KeyPath); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/ImportCommand.cs b/src/AppInstallerCLIE2ETests/ImportCommand.cs @@ -38,7 +38,7 @@ namespace AppInstallerCLIE2ETests { // Verify failure when trying to import with an invalid file var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-Invalid.json")); - Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, result.ExitCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_JSON_INVALID_FILE, result.ExitCode); Assert.True(result.StdOut.Contains("JSON file is not valid")); } @@ -56,7 +56,7 @@ namespace AppInstallerCLIE2ETests { // Verify failure when trying to import an unavailable package var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackage.json")); - Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("Package not found for import")); } @@ -65,7 +65,7 @@ namespace AppInstallerCLIE2ETests { // Verify failure when trying to import an unavailable package var result = TestCommon.RunAICLICommand("import", GetTestImportFile("ImportFile-Bad-UnknownPackageVersion.json")); - Assert.AreEqual(Constants.ErrorCode.APPINSTALLER_CLI_ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); + Assert.AreEqual(Constants.ErrorCode.ERROR_NOT_ALL_PACKAGES_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("Package not found for import")); } diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -21,8 +21,8 @@ namespace AppInstaller::Settings if (!GroupPolicies().IsEnabled(TogglePolicy::Policy::ExperimentalFeatures)) { AICLI_LOG(Core, Info, << - "Experimental feature " << ExperimentalFeature::GetFeature(feature).Name() << - " is disabled due to group policy" << TogglePolicy::GetPolicy(TogglePolicy::Policy::ExperimentalFeatures).RegValueName()); + "Experimental feature '" << ExperimentalFeature::GetFeature(feature).Name() << + "' is disabled due to group policy: " << TogglePolicy::GetPolicy(TogglePolicy::Policy::ExperimentalFeatures).RegValueName()); return false; } @@ -35,6 +35,12 @@ namespace AppInstaller::Settings case ExperimentalFeature::Feature::ExperimentalArg: return userSettings.Get<Setting::EFExperimentalArg>(); case ExperimentalFeature::Feature::ExperimentalMSStore: + if (GroupPolicies().GetState(TogglePolicy::Policy::MSStoreSource) == PolicyState::Enabled) + { + // Force enable the feature + return true; + } + return userSettings.Get<Setting::EFExperimentalMSStore>(); case ExperimentalFeature::Feature::ExperimentalList: return userSettings.Get<Setting::EFList>();