winget-cli

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

commit 6e092a6dde38ef6a3dc80a90b8bc7ea745ad63fd
parent a31f773cb0de74d17aea6048f94670d51fcd0694
Author: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com>
Date:   Fri,  2 May 2025 14:38:34 -0700

Added dscv3 user-settings resource (#5421)


Diffstat:
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Msrc/AppInstallerCLICore/Commands/DscCommand.cpp | 2++
Asrc/AppInstallerCLICore/Commands/DscUserSettingsFileResource.cpp | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/DscUserSettingsFileResource.h | 25+++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Resources.h | 4++++
Msrc/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs | 1-
Asrc/AppInstallerCLIE2ETests/DSCv3UserSettingsFileResourceCommand.cs | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 15+++++++++++++--
9 files changed, 647 insertions(+), 3 deletions(-)

diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -309,6 +309,7 @@ <ClInclude Include="Commands\DscSourceResource.h" /> <ClInclude Include="Commands\DscTestFileResource.h" /> <ClInclude Include="Commands\DscTestJsonResource.h" /> + <ClInclude Include="Commands\DscUserSettingsFileResource.h" /> <ClInclude Include="Commands\ErrorCommand.h" /> <ClInclude Include="Commands\ExperimentalCommand.h" /> <ClInclude Include="Commands\ExportCommand.h" /> @@ -396,6 +397,7 @@ <ClCompile Include="Commands\DscSourceResource.cpp" /> <ClCompile Include="Commands\DscTestFileResource.cpp" /> <ClCompile Include="Commands\DscTestJsonResource.cpp" /> + <ClCompile Include="Commands\DscUserSettingsFileResource.cpp" /> <ClCompile Include="Commands\ErrorCommand.cpp" /> <ClCompile Include="Commands\FontCommand.cpp" /> <ClCompile Include="Commands\ImportCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -287,6 +287,9 @@ <ClInclude Include="Commands\DscSourceResource.h"> <Filter>Commands\Configuration</Filter> </ClInclude> + <ClInclude Include="Commands\DscUserSettingsFileResource.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -541,6 +544,9 @@ <ClCompile Include="Commands\DscSourceResource.cpp"> <Filter>Commands\Configuration</Filter> </ClCompile> + <ClCompile Include="Commands\DscUserSettingsFileResource.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "DscCommand.h" #include "DscPackageResource.h" +#include "DscUserSettingsFileResource.h" #include "DscSourceResource.h" #ifndef AICLI_DISABLE_TEST_HOOKS @@ -17,6 +18,7 @@ namespace AppInstaller::CLI return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ std::make_unique<DscPackageResource>(FullName()), std::make_unique<DscSourceResource>(FullName()), + std::make_unique<DscUserSettingsFileResource>(FullName()), #ifndef AICLI_DISABLE_TEST_HOOKS std::make_unique<DscTestFileResource>(FullName()), std::make_unique<DscTestJsonResource>(FullName()), diff --git a/src/AppInstallerCLICore/Commands/DscUserSettingsFileResource.cpp b/src/AppInstallerCLICore/Commands/DscUserSettingsFileResource.cpp @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscUserSettingsFileResource.h" +#include "DscComposableObject.h" +#include "Resources.h" + +using namespace AppInstaller::Utility::literals; +using namespace AppInstaller::Settings; + +#define ACTION_FULL "Full" +#define ACTION_PARTIAL "Partial" + +namespace AppInstaller::CLI +{ + namespace + { + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(SettingsProperty, Json::Value, Settings, "settings", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionUserSettingsFileSettings); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(ActionProperty, std::string, Action, "action", Resource::String::DscResourcePropertyDescriptionUserSettingsFileAction, ({ ACTION_PARTIAL, ACTION_FULL }), ACTION_FULL); + + using UserSettingsFileResourceObject = DscComposableObject<StandardInDesiredStateProperty, SettingsProperty, ActionProperty>; + + struct UserSettingsFileFunctionData + { + UserSettingsFileFunctionData() + : UserSettingsFileFunctionData(std::nullopt, true) + { + } + + UserSettingsFileFunctionData(const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) : + Input(json, ignoreFieldRequirements), + _userSettingsPath(UserSettings::SettingsFilePath()) + { + } + + const UserSettingsFileResourceObject Input; + UserSettingsFileResourceObject Output; + + void Get() + { + Output.Action(ACTION_FULL); + Output.Settings(GetUserSettings()); + } + + bool Test() + { + return GetResolvedInput() == Output.Settings(); + } + + Json::Value DiffJson() + { + Json::Value result{ Json::ValueType::arrayValue }; + + if (!Test()) + { + result.append(std::string{ SettingsProperty::Name() }); + } + + return result; + } + + const Json::Value& GetResolvedInput() + { + THROW_HR_IF(E_UNEXPECTED, !Input.Settings().has_value()); + if (!_resolvedInputUserSettings) + { + if(Input.Action() == ACTION_FULL) + { + _resolvedInputUserSettings = Input.Settings(); + } + else + { + _resolvedInputUserSettings = MergeUserSettingsFiles(*Input.Settings()); + } + } + + return *_resolvedInputUserSettings; + } + + bool WriteOutput() + { + THROW_HR_IF(E_UNEXPECTED, !Output.Settings().has_value()); + std::ofstream file(_userSettingsPath, std::ios::binary); + if (file) + { + Json::StreamWriterBuilder writer; + writer["indentation"] = " "; + file << Json::writeString(writer, *Output.Settings()); + return true; + } + + AICLI_LOG(Config, Error, << "Failed to open or create user settings file: " << _userSettingsPath); + return false; + } + + private: + std::filesystem::path _userSettingsPath; + std::optional<Json::Value> _userSettings; + std::optional<Json::Value> _resolvedInputUserSettings; + + Json::Value MergeUserSettingsFiles(const Json::Value& overlay) + { + Json::Value mergedUserSettingsFile = GetUserSettings(); + MergeUserSettingsFiles(mergedUserSettingsFile, overlay); + return mergedUserSettingsFile; + } + + // Merges the overlay settings into the target settings. + void MergeUserSettingsFiles(Json::Value& target, const Json::Value& overlay) + { + // If either is not an object, we can't merge. + if (!overlay.isObject() || !target.isObject()) + { + return; + } + + // Iterate through the overlay settings and merge them into the target. + for (const auto& overlayKey : overlay.getMemberNames()) + { + const Json::Value& overlayValue = overlay[overlayKey]; + if (target.isMember(overlayKey)) + { + Json::Value& targetValue = target[overlayKey]; + if (targetValue.isObject() && overlayValue.isObject()) + { + // Recursively merge the objects. + MergeUserSettingsFiles(targetValue, overlayValue); + } + else + { + // Replace the value in the target. + // Note: Arrays are not merged, they are replaced. + target[overlayKey] = overlayValue; + } + } + else + { + // Add the overlay key to the target. + target[overlayKey] = overlayValue; + } + } + } + + const Json::Value& GetUserSettings() + { + if (!_userSettings) + { + _userSettings = Json::objectValue; + std::ifstream file(_userSettingsPath, std::ios::binary); + if (file) + { + Json::CharReaderBuilder builder; + std::string errs; + Json::Value jsonRoot; + if (Json::parseFromStream(builder, file, &jsonRoot, &errs)) + { + _userSettings = jsonRoot; + } + else + { + AICLI_LOG(Config, Warning, << "Failed to parse user settings file: " << _userSettingsPath << ", error: " << errs); + } + } + else + { + AICLI_LOG(Config, Warning, << "Failed to open user settings file: " << _userSettingsPath); + } + } + + return *_userSettings; + } + }; + } + + DscUserSettingsFileResource::DscUserSettingsFileResource(std::string_view parent) : + DscCommandBase(parent, "user-settings-file", DscResourceKind::Resource, + DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, + DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff) + { + } + + Resource::LocString DscUserSettingsFileResource::ShortDescription() const + { + return Resource::String::DscUserSettingsFileShortDescription; + } + + Resource::LocString DscUserSettingsFileResource::LongDescription() const + { + return Resource::String::DscUserSettingsFileLongDescription; + } + + std::string DscUserSettingsFileResource::ResourceType() const + { + return "UserSettingsFile"; + } + + void DscUserSettingsFileResource::ResourceFunctionGet(Execution::Context& context) const + { + ResourceFunctionExport(context); + } + + void DscUserSettingsFileResource::ResourceFunctionSet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + UserSettingsFileFunctionData data{ json }; + + data.Get(); + + // Capture the diff before updating the output + auto diff = data.DiffJson(); + + if (!data.Test()) + { + data.Output.Settings(data.GetResolvedInput()); + if (!data.WriteOutput()) + { + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_OPEN_FAILED)); + return; + } + } + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, diff); + } + } + + void DscUserSettingsFileResource::ResourceFunctionTest(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + UserSettingsFileFunctionData data{ json }; + + data.Get(); + data.Output.InDesiredState(data.Test()); + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson()); + } + } + + void DscUserSettingsFileResource::ResourceFunctionExport(Execution::Context& context) const + { + UserSettingsFileFunctionData data; + + data.Get(); + + WriteJsonOutputLine(context, data.Output.ToJson()); + } + + void DscUserSettingsFileResource::ResourceFunctionSchema(Execution::Context& context) const + { + WriteJsonOutputLine(context, UserSettingsFileResourceObject::Schema(ResourceType())); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscUserSettingsFileResource.h b/src/AppInstallerCLICore/Commands/DscUserSettingsFileResource.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DscCommandBase.h" + +namespace AppInstaller::CLI +{ + // A resource for managing user settings file. + struct DscUserSettingsFileResource : public DscCommandBase + { + DscUserSettingsFileResource(std::string_view parent); + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + std::string ResourceType() const override; + + void ResourceFunctionGet(Execution::Context& context) const override; + void ResourceFunctionSet(Execution::Context& context) const override; + void ResourceFunctionTest(Execution::Context& context) const override; + void ResourceFunctionExport(Execution::Context& context) const override; + void ResourceFunctionSchema(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -236,6 +236,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageMatchOption); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageUseLatest); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionPackageInstallMode); + WINGET_DEFINE_RESOURCE_STRINGID(DscUserSettingsFileShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscUserSettingsFileLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionUserSettingsFileSettings); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionUserSettingsFileAction); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceName); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceArgument); WINGET_DEFINE_RESOURCE_STRINGID(DscResourcePropertyDescriptionSourceType); diff --git a/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3SourceResourceCommand.cs @@ -10,7 +10,6 @@ namespace AppInstallerCLIE2ETests using System.Text.Json.Serialization; using AppInstallerCLIE2ETests.Helpers; using NUnit.Framework; - using WinRT; /// <summary> /// `Configure` command tests. diff --git a/src/AppInstallerCLIE2ETests/DSCv3UserSettingsFileResourceCommand.cs b/src/AppInstallerCLIE2ETests/DSCv3UserSettingsFileResourceCommand.cs @@ -0,0 +1,340 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3UserSettingsFileResourceCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests; + +using AppInstallerCLIE2ETests.Helpers; +using NUnit.Framework; +using System.Collections.Generic; +using System.IO; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +/// <summary> +/// `Configure` command tests. +/// </summary> +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] +[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] +public class DSCv3UserSettingsFileResourceCommand : DSCv3ResourceTestBase +{ + private const string UserSettingsFileResource = "user-settings-file"; + private const string SettingsPropertyName = "settings"; + private const string ActionPropertyValueFull = "Full"; + private const string ActionPropertyValuePartial = "Partial"; + private const string SettingsMock = "mock"; + private const string SettingsMockObject = "mockObject"; + private const string SettingsMockNested = "mockNested"; + + /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + TestCommon.SetupTestSource(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + EnsureTestResourcePresence(UserSettingsFileResource); + } + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + WinGetSettingsHelper.InitializeWingetSettings(); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + } + + /// <summary> + /// Set up. + /// </summary> + [SetUp] + public void Setup() + { + // Reset the settings to default before each test. + WinGetSettingsHelper.InitializeWingetSettings(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + } + + /// <summary> + /// Calls `get` on the `user-settings` resource. + /// </summary> + [Test] + public void UserSettingsFile_Get() + { + var expected = GetCurrentUserSettings(); + var getOutput = Get(new ()); + + Assert.IsNotNull(getOutput); + Assert.AreEqual(ActionPropertyValueFull, getOutput.Action); + AssertSettingsAreEqual(expected, getOutput.Settings); + } + + /// <summary> + /// Calls `set` on the `user-settings` resource with no diff. + /// </summary> + /// <param name="action">The action value.</param> + [Test] + [TestCase(ActionPropertyValueFull)] + [TestCase(ActionPropertyValuePartial)] + public void UserSettingsFile_Set_NoDiff(string action) + { + var setSettings = GetSettingsArg(action); + + (var setOutput, var setDiff) = Set(new () { Action = action, Settings = setSettings }); + + var expected = GetCurrentUserSettings(); + + Assert.IsNotNull(setOutput); + Assert.AreEqual(ActionPropertyValueFull, setOutput.Action); + AssertSettingsAreEqual(expected, setOutput.Settings); + AssertDiffState(setDiff, []); + } + + /// <summary> + /// Calls `set` on the `user-settings` resource to add fields. + /// </summary> + /// <param name="action">The action value.</param> + [Test] + [TestCase(ActionPropertyValueFull)] + [TestCase(ActionPropertyValuePartial)] + public void UserSettingsFile_Set_AddFields(string action) + { + // Call `set` to add mock properties to the settings + var setSettings = GetSettingsArg(action); + AddOrModifyMockProperties(setSettings, "mock"); + + (var setOutput, var setDiff) = Set(new () { Action = action, Settings = setSettings }); + + var expected = GetCurrentUserSettings(); + + // Assert that the settings are added + Assert.IsNotNull(setOutput); + Assert.AreEqual(ActionPropertyValueFull, setOutput.Action); + AssertMockProperties(setOutput.Settings, "mock"); + AssertSettingsAreEqual(expected, setOutput.Settings); + AssertDiffState(setDiff, [ SettingsPropertyName ]); + } + + /// <summary> + /// Calls `set` on the `user-settings` resource to update fields. + /// </summary> + /// <param name="action">The action value.</param> + [Test] + [TestCase(ActionPropertyValueFull)] + [TestCase(ActionPropertyValuePartial)] + public void UserSettingsFile_Set_UpdateFields(string action) + { + // Add mock properties to the settings + var set1Settings = new JsonObject(); + AddOrModifyMockProperties(set1Settings, "mock_old"); + Set(new () { Action = ActionPropertyValuePartial, Settings = set1Settings }); + + // Call `set` to update the settings + var set2Settings = GetSettingsArg(action); + AddOrModifyMockProperties(set2Settings, "mock_new"); + + (var setOutput, var setDiff) = Set(new () { Action = action, Settings = set2Settings }); + + var expected = GetCurrentUserSettings(); + + // Assert that the settings are updated + Assert.IsNotNull(setOutput); + Assert.AreEqual(ActionPropertyValueFull, setOutput.Action); + AssertMockProperties(setOutput.Settings, "mock_new"); + AssertSettingsAreEqual(expected, setOutput.Settings); + AssertDiffState(setDiff, [ SettingsPropertyName ]); + } + + /// <summary> + /// Calls `test` on the `user-settings` resource to check if the settings are in desired state. + /// </summary> + /// <param name="action">The action value.</param> + [Test] + [TestCase(ActionPropertyValueFull)] + [TestCase(ActionPropertyValuePartial)] + public void UserSettingsFile_Test_InDesiredState(string action) + { + // Add mock properties to the settings + var setSettings = new JsonObject(); + AddOrModifyMockProperties(setSettings, "mock"); + Set(new () { Action = ActionPropertyValuePartial, Settings = setSettings }); + + // Call `test` to check the settings + var testSettings = GetSettingsArg(action); + AddOrModifyMockProperties(testSettings, "mock"); + + (var testOutput, var testDiff) = Test(new () { Action = action, Settings = testSettings }); + + var expected = GetCurrentUserSettings(); + + // Assert that the settings are in desired state + Assert.IsNotNull(testOutput); + Assert.AreEqual(ActionPropertyValueFull, testOutput.Action); + AssertMockProperties(testOutput.Settings, "mock"); + AssertSettingsAreEqual(expected, testOutput.Settings); + Assert.IsTrue(testOutput.InDesiredState); + AssertDiffState(testDiff, []); + } + + /// <summary> + /// Calls `test` on the `user-settings` resource to check if the settings are not in desired state. + /// </summary> + /// <param name="action">The action value.</param> + [Test] + [TestCase(ActionPropertyValueFull)] + [TestCase(ActionPropertyValuePartial)] + public void UserSettingsFile_Test_NotInDesiredState(string action) + { + // Add mock properties to the settings + var setSettings = new JsonObject(); + AddOrModifyMockProperties(setSettings, "mock_set"); + Set(new () { Action = ActionPropertyValuePartial, Settings = setSettings }); + + // Call `test` to check the settings + var testSettings = GetSettingsArg(action); + AddOrModifyMockProperties(testSettings, "mock_test"); + + (var testOutput, var testDiff) = Test(new () { Action = action, Settings = testSettings }); + + var expected = GetCurrentUserSettings(); + + // Assert that the settings are not in desired state + Assert.IsNotNull(testOutput); + Assert.AreEqual(ActionPropertyValueFull, testOutput.Action); + AssertMockProperties(testOutput.Settings, "mock_set"); + AssertSettingsAreEqual(expected, testOutput.Settings); + Assert.IsFalse(testOutput.InDesiredState); + AssertDiffState(testDiff, [ SettingsPropertyName ]); + } + + /// <summary> + /// Calls `export` on the `user-settings` resource to export the settings. + /// </summary> + [Test] + public void UserSettingsFile_Export() + { + var expected = GetCurrentUserSettings(); + var exportOutput = Export(new ()); + + Assert.IsNotNull(exportOutput); + Assert.AreEqual(ActionPropertyValueFull, exportOutput.Action); + AssertSettingsAreEqual(expected, exportOutput.Settings); + } + + /// <summary> + /// Calls `get` on the `user-settings` resource. + /// </summary> + /// <param name="resourceData">The input resource data.</param> + /// <returns>The output resource data.</returns> + private static UserSettingsFileResourceData Get(UserSettingsFileResourceData resourceData) + { + var result = RunDSCv3Command(UserSettingsFileResource, GetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + return GetSingleOutputLineAs<UserSettingsFileResourceData>(result.StdOut); + } + + /// <summary> + /// Calls `set` on the `user-settings` resource. + /// </summary> + /// <param name="resourceData">The input resource data.</param> + /// <returns>The output resource data and the diff.</returns> + private static (UserSettingsFileResourceData, List<string>) Set(UserSettingsFileResourceData resourceData) + { + var result = RunDSCv3Command(UserSettingsFileResource, SetFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + return GetSingleOutputLineAndDiffAs<UserSettingsFileResourceData>(result.StdOut); + } + + /// <summary> + /// Calls `test` on the `user-settings` resource. + /// </summary> + /// <param name="resourceData">The input resource data.</param> + /// <returns>The output resource data and the diff.</returns> + private static (UserSettingsFileResourceData, List<string>) Test(UserSettingsFileResourceData resourceData) + { + var result = RunDSCv3Command(UserSettingsFileResource, TestFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + return GetSingleOutputLineAndDiffAs<UserSettingsFileResourceData>(result.StdOut); + } + + /// <summary> + /// Calls `export` on the `user-settings` resource. + /// </summary> + /// <param name="resourceData">The input resource data.</param> + /// <returns>The output resource data.</returns> + private static UserSettingsFileResourceData Export(UserSettingsFileResourceData resourceData) + { + var result = RunDSCv3Command(UserSettingsFileResource, ExportFunction, resourceData); + AssertSuccessfulResourceRun(ref result); + return GetSingleOutputLineAs<UserSettingsFileResourceData>(result.StdOut); + } + + /// <summary> + /// Gets the current user settings from the settings file. + /// </summary> + /// <returns>The current user settings as a JsonObject.</returns> + private static JsonObject GetCurrentUserSettings() + { + var settingsContent = File.ReadAllText(WinGetSettingsHelper.GetUserSettingsPath()); + return JsonNode.Parse(settingsContent).AsObject(); + } + + /// <summary> + /// Adds or modifies mock properties in the settings. + /// </summary> + /// <param name="settings">Target settings.</param> + /// <param name="value">The mock value.</param> + private static void AddOrModifyMockProperties(JsonObject settings, string value) + { + settings[SettingsMock] = value; + settings[SettingsMockObject] ??= new JsonObject(); + settings[SettingsMockObject][SettingsMockNested] = value; + } + + /// <summary> + /// Asserts that the settings contain the expected mock properties. + /// </summary> + /// <param name="settings">Target settings.</param> + /// <param name="value">The expected mock value.</param> + private static void AssertMockProperties(JsonObject settings, string value) + { + Assert.IsNotNull(settings); + Assert.IsTrue(settings.ContainsKey(SettingsMock)); + Assert.AreEqual(settings[SettingsMock].ToString(), value); + Assert.IsTrue(settings.ContainsKey(SettingsMockObject)); + Assert.IsTrue(settings[SettingsMockObject].AsObject().ContainsKey(SettingsMockNested)); + Assert.AreEqual(settings[SettingsMockObject][SettingsMockNested].ToString(), value); + } + + /// <summary> + /// Asserts that the diff state is as expected. + /// </summary> + /// <param name="expected">The expected settings.</param> + /// <param name="actual">The actual settings.</param> + private static void AssertSettingsAreEqual(JsonObject expected, JsonObject actual) + { + Assert.IsTrue(JsonNode.DeepEquals(expected, actual)); + } + + /// <summary> + /// Gets the settings argument based on the action. + /// </summary> + /// <param name="action">The action value.</param> + /// <returns>The settings argument as a JsonObject.</returns> + private static JsonObject GetSettingsArg(string action) => action == ActionPropertyValueFull ? GetCurrentUserSettings() : new (); + + private class UserSettingsFileResourceData + { + [JsonPropertyName(InDesiredStatePropertyName)] + public bool? InDesiredState { get; set; } + + public string Action { get; set; } + + public JsonObject Settings { get; set; } + } +} diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3283,6 +3283,18 @@ Please specify one of them using the --source option to proceed.</value> <data name="DscResourcePropertyDescriptionAcceptAgreements" xml:space="preserve"> <value>Indicates whether to accept agreements for sources and packages.</value> </data> + <data name="DscUserSettingsFileShortDescription" xml:space="preserve"> + <value>Manage user settings file</value> + </data> + <data name="DscUserSettingsFileLongDescription" xml:space="preserve"> + <value>Allows management of user settings file via the DSC v3 command line interface protocol. See the help link for details.</value> + </data> + <data name="DscResourcePropertyDescriptionUserSettingsFileSettings" xml:space="preserve"> + <value>The settings json file content.</value> + </data> + <data name="DscResourcePropertyDescriptionUserSettingsFileAction" xml:space="preserve"> + <value>The action used to apply the settings.</value> + </data> <data name="CorrelationArgumentDescription" xml:space="preserve"> <value>Value is logged for correlation</value> </data> @@ -3307,4 +3319,4 @@ Please specify one of them using the --source option to proceed.</value> <data name="DscResourcePropertyDescriptionSourceExplicit" xml:space="preserve"> <value>Whether the source is included when calls don't specify a source.</value> </data> -</root>- \ No newline at end of file +</root>