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:
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>