From 4e4d3c05a14c5eb6a4956226151a0b7c41e3924d Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 10 Jun 2025 13:10:37 -0700 Subject: [PATCH 01/24] WIP --- src/WingetCreateCLI/Commands/BaseCommand.cs | 5 + src/WingetCreateCLI/Commands/CacheCommand.cs | 3 + src/WingetCreateCLI/Commands/DscCommand.cs | 118 ++++++++++++++++++ .../Commands/DscCommands/BaseDscCommand.cs | 36 ++++++ .../DscCommands/DscSettingsCommand.cs | 67 ++++++++++ src/WingetCreateCLI/Commands/InfoCommand.cs | 3 + .../Commands/SettingsCommand.cs | 3 + src/WingetCreateCLI/Program.cs | 5 +- src/WingetCreateCLI/UserSettings.cs | 5 + 9 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 src/WingetCreateCLI/Commands/DscCommand.cs create mode 100644 src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs create mode 100644 src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs index 61d3875e..1099dd10 100644 --- a/src/WingetCreateCLI/Commands/BaseCommand.cs +++ b/src/WingetCreateCLI/Commands/BaseCommand.cs @@ -61,6 +61,11 @@ public abstract class BaseCommand /// public static string Extension => Serialization.ManifestSerializer.AssociatedFileExtension; + /// + /// Gets a value indicating whether or not the command requires a GitHub token to be set. + /// + public virtual bool RequiresGitHubToken { get; } = true; + /// /// Gets or sets the GitHub token used to submit a pull request on behalf of the user. /// diff --git a/src/WingetCreateCLI/Commands/CacheCommand.cs b/src/WingetCreateCLI/Commands/CacheCommand.cs index b30298ad..dfbee690 100644 --- a/src/WingetCreateCLI/Commands/CacheCommand.cs +++ b/src/WingetCreateCLI/Commands/CacheCommand.cs @@ -37,6 +37,9 @@ public class CacheCommand : BaseCommand /// [Option('o', "open", Required = true, SetName = nameof(Open), HelpText = "Open_HelpText", ResourceType = typeof(Resources))] public bool Open { get; set; } + + /// + public override bool RequiresGitHubToken => false; /// /// Executes the cache command flow. diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs new file mode 100644 index 00000000..aa19dd33 --- /dev/null +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Commands; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommandLine; +using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +/// +/// Command for managin the application using dsc v3. +/// +[Verb("dsc", HelpText = "Manage the application using dsc v3.")] +public class DscCommand : BaseCommand +{ + /// + public override bool RequiresGitHubToken => false; + + /// + /// Gets or sets the unbound arguments that exist after the positional parameters. + /// + [Value(2, Hidden = true)] + public IList UnboundArgs { get; set; } = new List(); + + /// + /// Gets or sets the input for the dsc Get operation. + /// + [Option('g', "get", SetName = "GetMethod", HelpText = "Command for the Get flow.")] + public string Get { get; set; } + + /// + /// Gets or sets the input for the dsc Set operation. + /// + [Option('s', "set", SetName = "SetMethod", HelpText = "Command for the Set flow.")] + public string Set { get; set; } + + /// + /// Gets or sets the input for the dsc Test operation. + /// + [Option('t', "test", SetName = "TestMethod", HelpText = "Command for the Test flow.")] + public string Test { get; set; } + + /// + /// Gets or sets the input for the dsc Export operation. + /// + [Option('e', "export", SetName = "ExportMethod", HelpText = "Command for the Export flow.")] + public string Export { get; set; } + + /// + /// Executes the dsc command flow. + /// + /// Boolean representing success or fail of the command. + public override async Task Execute() + { + BaseDscCommand dscCommand; + var dscScope = this.UnboundArgs.FirstOrDefault()?.ToLowerInvariant() ?? string.Empty; + if (dscScope == DscSettingsCommand.CommandName) + { + dscCommand = new DscSettingsCommand(); + } + else + { + Console.WriteLine($"Unknown DSC scope: {dscScope}"); + return false; + } + + JToken input; + if (this.TryParse(this.Get, out input)) + { + dscCommand.Get(input); + } + else if (this.TryParse(this.Set, out input)) + { + dscCommand.Set(input); + } + else if (this.TryParse(this.Test, out input)) + { + dscCommand.Test(input); + } + else if (this.TryParse(this.Export, out input)) + { + dscCommand.Export(input); + } + else + { + Console.WriteLine("No valid DSC command provided. Use -g, -s, -t, or -e to specify a command."); + return false; + } + + return true; + } + + private bool TryParse(string json, out JToken token) + { + if (string.IsNullOrWhiteSpace(json)) + { + token = null; + return false; + } + + try + { + token = JToken.Parse(json); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error parsing JSON: {ex.Message}"); + token = null; + return false; + } + } +} diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs new file mode 100644 index 00000000..c7c7507d --- /dev/null +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Commands.DscCommands; + +using Newtonsoft.Json.Linq; + +/// +/// Base class for DSC commands. +/// +public abstract class BaseDscCommand +{ + /// + /// DSC Get command. + /// + /// Input for the Get command. + public abstract void Get(JToken input); + + /// + /// DSC Set command. + /// + /// Input for the Set command. + public abstract void Set(JToken input); + + /// + /// DSC Test command. + /// + /// Input for the Test command. + public abstract void Test(JToken input); + + /// + /// DSC Export command. + /// + /// Input for the Export command. + public abstract void Export(JToken input); +} diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs new file mode 100644 index 00000000..7585335d --- /dev/null +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Commands.DscCommands; + +using System; +using Microsoft.WingetCreateCLI.Models.Settings; +using Newtonsoft.Json.Linq; + +/// +/// Command for managing the settings using dsc v3. +/// +public class DscSettingsCommand : BaseDscCommand +{ + /// + /// Represents the name of the command used to access settings functionality. + /// + public const string CommandName = "settings"; + + /// + public override void Get(JToken input) + { + Console.WriteLine(UserSettings.ToJson()); + } + + /// + public override void Set(JToken input) + { + // No-op + } + + /// + public override void Test(JToken input) + { + // No-op + } + + /// + public override void Export(JToken input) + { + // No-op + } + + private class UserSettingsFunctionData + { + public enum ActionType + { + Partial, + Full, + } + + public UserSettingsFunctionData(JToken token) + { + + } + + /// + /// Gets or sets the action type for the settings command. + /// + public ActionType Action { get; set; } = ActionType.Partial; + + /// + /// Gets or sets the settings manifest to be used for the command. + /// + public SettingsManifest Settings { get; set; } + } +} diff --git a/src/WingetCreateCLI/Commands/InfoCommand.cs b/src/WingetCreateCLI/Commands/InfoCommand.cs index b4441e72..2fe5f8fb 100644 --- a/src/WingetCreateCLI/Commands/InfoCommand.cs +++ b/src/WingetCreateCLI/Commands/InfoCommand.cs @@ -20,6 +20,9 @@ namespace Microsoft.WingetCreateCLI.Commands [Verb("info", HelpText = "InfoCommand_HelpText", ResourceType = typeof(Resources))] public class InfoCommand : BaseCommand { + /// + public override bool RequiresGitHubToken => false; + /// /// Executes the info command flow. /// diff --git a/src/WingetCreateCLI/Commands/SettingsCommand.cs b/src/WingetCreateCLI/Commands/SettingsCommand.cs index 821de4aa..28025699 100644 --- a/src/WingetCreateCLI/Commands/SettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/SettingsCommand.cs @@ -22,6 +22,9 @@ namespace Microsoft.WingetCreateCLI.Commands [Verb("settings", HelpText = "SettingsCommand_HelpText", ResourceType = typeof(Resources))] public class SettingsCommand : BaseCommand { + /// + public override bool RequiresGitHubToken => false; + /// /// Executes the token command flow. /// diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs index 9cb7e5d5..f2871bdf 100644 --- a/src/WingetCreateCLI/Program.cs +++ b/src/WingetCreateCLI/Program.cs @@ -52,6 +52,7 @@ private static async Task Main(string[] args) typeof(CacheCommand), typeof(ShowCommand), typeof(InfoCommand), + typeof(DscCommand), }; var parserResult = myParser.ParseArguments(args, types); @@ -70,10 +71,8 @@ private static async Task Main(string[] args) Logger.WarnLocalized(nameof(Resources.GitHubTokenWarning_Message)); } - bool commandHandlesToken = command is not CacheCommand and not InfoCommand and not SettingsCommand; - // Do not load github client for commands that do not deal with a GitHub token. - if (commandHandlesToken) + if (command.RequiresGitHubToken) { if (await command.LoadGitHubClient()) { diff --git a/src/WingetCreateCLI/UserSettings.cs b/src/WingetCreateCLI/UserSettings.cs index 0ff37970..cc5b38c4 100644 --- a/src/WingetCreateCLI/UserSettings.cs +++ b/src/WingetCreateCLI/UserSettings.cs @@ -230,6 +230,11 @@ private static void LoadSettings() Visual = new Visual(), }; } + } + + public static string ToJson() + { + return JsonConvert.SerializeObject(Settings, Formatting.None); } } } From f10d9bbcb41c907c497488eb2f80f99a9b76fd03 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:41:40 -0700 Subject: [PATCH 02/24] Init draft --- src/WingetCreateCLI/Commands/DscCommand.cs | 1 - .../Commands/DscCommands/BaseDscCommand.cs | 11 ++ .../DscCommands/DscSettingsCommand.cs | 135 ++++++++++++++++-- src/WingetCreateCLI/UserSettings.cs | 37 +++-- 4 files changed, 159 insertions(+), 25 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index aa19dd33..259a96f3 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -9,7 +9,6 @@ namespace Microsoft.WingetCreateCLI.Commands; using System.Threading.Tasks; using CommandLine; using Microsoft.WingetCreateCLI.Commands.DscCommands; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; /// diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs index c7c7507d..f8840336 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -3,6 +3,8 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; /// @@ -33,4 +35,13 @@ public abstract class BaseDscCommand /// /// Input for the Export command. public abstract void Export(JToken input); + + /// + /// Writes a JSON output line to the console. + /// + /// The JSON token to be written. + protected void WriteJsonOutputLine(JToken token) + { + Console.WriteLine(token.ToString(Formatting.None)); + } } diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 7585335d..8b98874c 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -4,7 +4,9 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; using System; +using System.Diagnostics; using Microsoft.WingetCreateCLI.Models.Settings; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; /// @@ -20,48 +22,157 @@ public class DscSettingsCommand : BaseDscCommand /// public override void Get(JToken input) { - Console.WriteLine(UserSettings.ToJson()); + this.Export(input); } /// public override void Set(JToken input) { - // No-op + var data = new UserSettingsFunctionData(input); + data.Get(); + + // Capture the diff before updating the output + var diff = data.DiffJson(); + + if (!data.Test()) + { + data.Output.Settings = data.GetResolvedInput(); + data.WriteOutput(); + } + + this.WriteJsonOutputLine(data.Output.ToJson()); + this.WriteJsonOutputLine(diff); } /// public override void Test(JToken input) { - // No-op + var data = new UserSettingsFunctionData(input); + + data.Get(); + data.Output.InDesiredState = data.Test(); + + this.WriteJsonOutputLine(data.Output.ToJson()); + this.WriteJsonOutputLine(data.DiffJson()); } /// public override void Export(JToken input) { - // No-op + var data = new UserSettingsFunctionData(input); + + data.Get(); + + this.WriteJsonOutputLine(data.Output.ToJson()); } private class UserSettingsFunctionData { - public enum ActionType + private JObject resolvedInputUserSettings; + private JObject userSettings; + + public UserSettingsFunctionData(JToken json = null) + { + this.Input = json == null ? new() : json.ToObject(); + this.Output = new(); + } + + public UserSettingsResourceObject Input { get; } + + public UserSettingsResourceObject Output { get; } + + public void Get() + { + this.Output.Settings = this.GetUserSettings(); + } + + public bool Test() + { + return JToken.DeepEquals(this.GetResolvedInput(), this.GetValidSettings(this.Output.Settings)); + } + + public JArray DiffJson() + { + var diff = new JArray(); + if (!this.Test()) + { + diff.Add("settings"); + } + + return diff; + } + + public JObject GetResolvedInput() { - Partial, - Full, + Debug.Assert(this.Input.Settings != null, "Input settings should not be null."); + if (this.resolvedInputUserSettings == null) + { + if (UserSettingsResourceObject.ActionFull.Equals(this.Input.Action, StringComparison.OrdinalIgnoreCase)) + { + this.Output.Action = UserSettingsResourceObject.ActionFull; + this.resolvedInputUserSettings = this.GetValidSettings(this.Input.Settings); + } + else + { + this.Output.Action = UserSettingsResourceObject.ActionPartial; + var mergedSettings = this.GetUserSettings(); + mergedSettings.Merge(this.Input.Settings); + this.resolvedInputUserSettings = this.GetValidSettings(mergedSettings); + } + } + + return this.resolvedInputUserSettings; } - public UserSettingsFunctionData(JToken token) + public JObject GetUserSettings() { + this.userSettings ??= UserSettings.ToJson(); + return (JObject)this.userSettings.DeepClone(); + } + public void WriteOutput() + { + Debug.Assert(this.Output.Settings != null, "Output settings should not be null."); + UserSettings.SaveSettings(this.Output.Settings.ToObject()); } /// - /// Gets or sets the action type for the settings command. + /// Validates and converts the provided settings into a structured format. /// - public ActionType Action { get; set; } = ActionType.Partial; + /// An object containing settings to be validated. + /// An object representing the validated settings. + public JObject GetValidSettings(JObject settings) + { + var settingsManifest = settings.ToObject(); + return JObject.FromObject(settingsManifest); + } + } + + private class UserSettingsResourceObject + { + public const string ActionFull = "Full"; + public const string ActionPartial = "Partial"; + + // TODO Make this required + [JsonProperty("settings")] + public JObject Settings { get; set; } + + [JsonProperty("action", NullValueHandling = NullValueHandling.Ignore)] + public string Action { get; set; } + + [JsonProperty("_inDesiredState", NullValueHandling = NullValueHandling.Ignore)] + public bool? InDesiredState { get; set; } /// - /// Gets or sets the settings manifest to be used for the command. + /// Converts the current object to a JSON representation. /// - public SettingsManifest Settings { get; set; } + /// A Json object representing the current object. + public JObject ToJson() + { + return JObject.FromObject(this, new JsonSerializer + { + NullValueHandling = NullValueHandling.Ignore, + }); + } } } diff --git a/src/WingetCreateCLI/UserSettings.cs b/src/WingetCreateCLI/UserSettings.cs index cc5b38c4..5af01e26 100644 --- a/src/WingetCreateCLI/UserSettings.cs +++ b/src/WingetCreateCLI/UserSettings.cs @@ -4,13 +4,15 @@ namespace Microsoft.WingetCreateCLI { using System; - using System.Collections.Generic; + using System.Collections.Generic; + using System.Diagnostics; using System.IO; using Microsoft.WingetCreateCLI.Logging; using Microsoft.WingetCreateCLI.Models.Settings; using Microsoft.WingetCreateCLI.Properties; - using Newtonsoft.Json; - + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + /// /// UserSettings configuration class for WingetCreate. /// @@ -185,12 +187,28 @@ public static void FirstRunTelemetryConsent() /// /// Saves the current settings configurations to the settings.json file. - /// - public static void SaveSettings() - { + /// + /// Optional settings manifest to save. If null, the current settings will be saved. + public static void SaveSettings(SettingsManifest settings = null) + { + if (settings != null) + { + Settings = settings; + } + + Debug.Assert(Settings != null, "Settings should not be null when saving settings."); string output = JsonConvert.SerializeObject(Settings, Formatting.Indented); File.WriteAllText(SettingsJsonPath, output); - } + } + + /// + /// Gets the current settings as a Json object. + /// + /// A Json object representing the current settings. + public static JObject ToJson() + { + return JObject.FromObject(Settings); + } /// /// Loads the correct settings file based on the following order. @@ -231,10 +249,5 @@ private static void LoadSettings() }; } } - - public static string ToJson() - { - return JsonConvert.SerializeObject(Settings, Formatting.None); - } } } From 9113d1e714a4415c1ca9e6657d1ea45b011b0ba0 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:26:58 -0700 Subject: [PATCH 03/24] Move files --- .../DscCommands/DscSettingsCommand.cs | 121 +--------------- .../Models/DscModels/BaseResourceObject.cs | 18 +++ .../Models/DscModels/SettingsFunctionData.cs | 134 ++++++++++++++++++ .../DscModels/SettingsResourceObject.cs | 57 ++++++++ src/WingetCreateCLI/WingetCreateCLI.csproj | 4 - 5 files changed, 213 insertions(+), 121 deletions(-) create mode 100644 src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs create mode 100644 src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs create mode 100644 src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 8b98874c..98ee9a71 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -3,10 +3,7 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; -using System; -using System.Diagnostics; -using Microsoft.WingetCreateCLI.Models.Settings; -using Newtonsoft.Json; +using Microsoft.WingetCreateCLI.Models.DscModels; using Newtonsoft.Json.Linq; /// @@ -28,7 +25,7 @@ public override void Get(JToken input) /// public override void Set(JToken input) { - var data = new UserSettingsFunctionData(input); + var data = new SettingsFunctionData(input); data.Get(); // Capture the diff before updating the output @@ -47,7 +44,7 @@ public override void Set(JToken input) /// public override void Test(JToken input) { - var data = new UserSettingsFunctionData(input); + var data = new SettingsFunctionData(input); data.Get(); data.Output.InDesiredState = data.Test(); @@ -59,120 +56,10 @@ public override void Test(JToken input) /// public override void Export(JToken input) { - var data = new UserSettingsFunctionData(input); + var data = new SettingsFunctionData(); data.Get(); this.WriteJsonOutputLine(data.Output.ToJson()); } - - private class UserSettingsFunctionData - { - private JObject resolvedInputUserSettings; - private JObject userSettings; - - public UserSettingsFunctionData(JToken json = null) - { - this.Input = json == null ? new() : json.ToObject(); - this.Output = new(); - } - - public UserSettingsResourceObject Input { get; } - - public UserSettingsResourceObject Output { get; } - - public void Get() - { - this.Output.Settings = this.GetUserSettings(); - } - - public bool Test() - { - return JToken.DeepEquals(this.GetResolvedInput(), this.GetValidSettings(this.Output.Settings)); - } - - public JArray DiffJson() - { - var diff = new JArray(); - if (!this.Test()) - { - diff.Add("settings"); - } - - return diff; - } - - public JObject GetResolvedInput() - { - Debug.Assert(this.Input.Settings != null, "Input settings should not be null."); - if (this.resolvedInputUserSettings == null) - { - if (UserSettingsResourceObject.ActionFull.Equals(this.Input.Action, StringComparison.OrdinalIgnoreCase)) - { - this.Output.Action = UserSettingsResourceObject.ActionFull; - this.resolvedInputUserSettings = this.GetValidSettings(this.Input.Settings); - } - else - { - this.Output.Action = UserSettingsResourceObject.ActionPartial; - var mergedSettings = this.GetUserSettings(); - mergedSettings.Merge(this.Input.Settings); - this.resolvedInputUserSettings = this.GetValidSettings(mergedSettings); - } - } - - return this.resolvedInputUserSettings; - } - - public JObject GetUserSettings() - { - this.userSettings ??= UserSettings.ToJson(); - return (JObject)this.userSettings.DeepClone(); - } - - public void WriteOutput() - { - Debug.Assert(this.Output.Settings != null, "Output settings should not be null."); - UserSettings.SaveSettings(this.Output.Settings.ToObject()); - } - - /// - /// Validates and converts the provided settings into a structured format. - /// - /// An object containing settings to be validated. - /// An object representing the validated settings. - public JObject GetValidSettings(JObject settings) - { - var settingsManifest = settings.ToObject(); - return JObject.FromObject(settingsManifest); - } - } - - private class UserSettingsResourceObject - { - public const string ActionFull = "Full"; - public const string ActionPartial = "Partial"; - - // TODO Make this required - [JsonProperty("settings")] - public JObject Settings { get; set; } - - [JsonProperty("action", NullValueHandling = NullValueHandling.Ignore)] - public string Action { get; set; } - - [JsonProperty("_inDesiredState", NullValueHandling = NullValueHandling.Ignore)] - public bool? InDesiredState { get; set; } - - /// - /// Converts the current object to a JSON representation. - /// - /// A Json object representing the current object. - public JObject ToJson() - { - return JObject.FromObject(this, new JsonSerializer - { - NullValueHandling = NullValueHandling.Ignore, - }); - } - } } diff --git a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs new file mode 100644 index 00000000..b4bcd070 --- /dev/null +++ b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Models.DscModels; + +using Newtonsoft.Json; + +/// +/// Represents a base resource object with a property indicating whether the resource is in its desired state. +/// +public class BaseResourceObject +{ + /// + /// Gets or sets a value indicating whether the resource is in its desired state. + /// + [JsonProperty("_inDesiredState", NullValueHandling = NullValueHandling.Ignore)] + public bool? InDesiredState { get; set; } +} diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs b/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs new file mode 100644 index 00000000..eb11047b --- /dev/null +++ b/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Models.DscModels; + +using System; +using System.Diagnostics; +using Microsoft.WingetCreateCLI.Models.Settings; +using Newtonsoft.Json.Linq; + +/// +/// Represents the data structure for settings functionality in DSC operations. +/// +public class SettingsFunctionData +{ + private JObject resolvedInputUserSettings; + private JObject userSettings; + + /// + /// Initializes a new instance of the class with default settings. + /// + public SettingsFunctionData() + : this(null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// An optional JSON token used to initialize the input settings. + public SettingsFunctionData(JToken json = null) + { + this.Output = new(); + this.Input = json == null ? new() : json.ToObject(); + } + + /// + /// Gets the input settings resource object. + /// + public SettingsResourceObject Input { get; } + + /// + /// Gets the output settings resource object. + /// + public SettingsResourceObject Output { get; } + + /// + /// Loads the user settings into the output object. + /// + public void Get() + { + this.Output.Settings = this.GetUserSettings(); + } + + /// + /// Gets whether the resolved input settings and output settings are equivalent. + /// + /// >true if the settings are equivalent; otherwise, false. + public bool Test() + { + return JToken.DeepEquals(this.GetResolvedInput(), this.GetValidSettings(this.Output.Settings)); + } + + /// + /// Gets the differences between the current settings and the input settings. + /// + /// A Json array containing the differences. + public JArray DiffJson() + { + var diff = new JArray(); + if (!this.Test()) + { + diff.Add("settings"); + } + + return diff; + } + + /// + /// Gets the resolved input settings based on the action specified in the input. + /// + /// >A Json object representing the resolved input settings. + public JObject GetResolvedInput() + { + Debug.Assert(this.Input.Settings != null, "Input settings should not be null."); + if (this.resolvedInputUserSettings == null) + { + if (SettingsResourceObject.ActionFull.Equals(this.Input.Action, StringComparison.OrdinalIgnoreCase)) + { + this.Output.Action = SettingsResourceObject.ActionFull; + this.resolvedInputUserSettings = this.GetValidSettings(this.Input.Settings); + } + else + { + this.Output.Action = SettingsResourceObject.ActionPartial; + var mergedSettings = this.GetUserSettings(); + mergedSettings.Merge(this.Input.Settings); + this.resolvedInputUserSettings = this.GetValidSettings(mergedSettings); + } + } + + return this.resolvedInputUserSettings; + } + + /// + /// Retrieves a deep-cloned JSON representation of the current user settings. + /// + /// A Json object representing the user settings. + public JObject GetUserSettings() + { + this.userSettings ??= UserSettings.ToJson(); + return (JObject)this.userSettings.DeepClone(); + } + + /// + /// Writes the current output settings to persistent storage. + /// + public void WriteOutput() + { + Debug.Assert(this.Output.Settings != null, "Output settings should not be null."); + UserSettings.SaveSettings(this.Output.Settings.ToObject()); + } + + /// + /// Validates and converts the provided settings into a structured format. + /// + /// An object containing settings to be validated. + /// An object representing the validated settings. + public JObject GetValidSettings(JObject settings) + { + var settingsManifest = settings.ToObject(); + return JObject.FromObject(settingsManifest); + } +} diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs new file mode 100644 index 00000000..d03b291e --- /dev/null +++ b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Models.DscModels; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +/// +/// Represents a settings resource object used in DSC operations. +/// +public class SettingsResourceObject : BaseResourceObject +{ + /// + /// Defines the action type for full settings application. + /// + public const string ActionFull = "Full"; + + /// + /// Defines the action type for partial settings application. + /// + public const string ActionPartial = "Partial"; + + /// + /// Gets or sets the settings for the resource object. + /// + [JsonProperty("settings", Required = Required.Always)] + public JObject Settings { get; set; } + + /// + /// Gets or sets the action to be performed on the settings resource object. + /// + [JsonProperty("action", NullValueHandling = NullValueHandling.Ignore)] + public string Action { get; set; } + + /// + /// Creates a settings resource object with the specified settings and action. + /// + /// The JSON representation of the settings resource object. + /// A settings resource object. + public static SettingsResourceObject FromJson(JToken json) + { + return json.ToObject(); + } + + /// + /// Converts the current object to a JSON representation. + /// + /// A Json object representing the current object. + public JObject ToJson() + { + return JObject.FromObject(this, new JsonSerializer + { + NullValueHandling = NullValueHandling.Ignore, + }); + } +} diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index ed20a01c..3cdc473a 100644 --- a/src/WingetCreateCLI/WingetCreateCLI.csproj +++ b/src/WingetCreateCLI/WingetCreateCLI.csproj @@ -99,10 +99,6 @@ - - - - From bc9cd4af8b21d64ed279690442afb42b6e79a253 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 11 Jun 2025 19:34:23 -0700 Subject: [PATCH 04/24] Added schema --- src/WingetCreateCLI/Commands/DscCommand.cs | 12 +++++- .../Commands/DscCommands/BaseDscCommand.cs | 30 +++++++++++++++ .../DscCommands/DscSettingsCommand.cs | 12 ++++-- .../Models/DscModels/BaseResourceObject.cs | 37 ++++++++++++++++++- .../DscModels/SettingsResourceObject.cs | 35 ++++++++++-------- 5 files changed, 104 insertions(+), 22 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index 259a96f3..af2f84eb 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -50,6 +50,12 @@ public class DscCommand : BaseCommand [Option('e', "export", SetName = "ExportMethod", HelpText = "Command for the Export flow.")] public string Export { get; set; } + /// + /// Gets or sets a value indicating whether to execute the schema command. + /// + [Option("schema", SetName = "SchemaMethod", HelpText = "Command for the Schema flow.")] + public bool Schema { get; set; } + /// /// Executes the dsc command flow. /// @@ -58,7 +64,7 @@ public override async Task Execute() { BaseDscCommand dscCommand; var dscScope = this.UnboundArgs.FirstOrDefault()?.ToLowerInvariant() ?? string.Empty; - if (dscScope == DscSettingsCommand.CommandName) + if (dscScope == "settings") { dscCommand = new DscSettingsCommand(); } @@ -85,6 +91,10 @@ public override async Task Execute() { dscCommand.Export(input); } + else if (this.Schema) + { + dscCommand.Schema(); + } else { Console.WriteLine("No valid DSC command provided. Use -g, -s, -t, or -e to specify a command."); diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs index f8840336..56631d17 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -4,6 +4,7 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; using System; +using Microsoft.WingetCreateCLI.Models.DscModels; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -12,6 +13,11 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; /// public abstract class BaseDscCommand { + /// + /// Gets the name of the command used to access the DSC functionality. + /// + public virtual string CommandName { get; } + /// /// DSC Get command. /// @@ -36,6 +42,30 @@ public abstract class BaseDscCommand /// Input for the Export command. public abstract void Export(JToken input); + /// + /// DSC Schema command. + /// + public abstract void Schema(); + + /// + /// Creates a Json schema for a DSC resource object. + /// + /// A Json object representing the schema. + protected JObject CreateSchema() + where T : BaseResourceObject, new() + { + var resourceObject = new T(); + return new JObject + { + ["$schema"] = "http://json-schema.org/draft-07/schema#", + ["title"] = this.CommandName, + ["type"] = "object", + ["properties"] = resourceObject.GetProperties(), + ["required"] = resourceObject.GetRequiredProperties(), + ["additionalProperties"] = false, + }; + } + /// /// Writes a JSON output line to the console. /// diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 98ee9a71..d086b0b7 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -11,10 +11,8 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; /// public class DscSettingsCommand : BaseDscCommand { - /// - /// Represents the name of the command used to access settings functionality. - /// - public const string CommandName = "settings"; + /// + public override string CommandName => "settings"; /// public override void Get(JToken input) @@ -62,4 +60,10 @@ public override void Export(JToken input) this.WriteJsonOutputLine(data.Output.ToJson()); } + + /// + public override void Schema() + { + this.WriteJsonOutputLine(this.CreateSchema()); + } } diff --git a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs index b4bcd070..fb432384 100644 --- a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs @@ -4,15 +4,50 @@ namespace Microsoft.WingetCreateCLI.Models.DscModels; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; /// /// Represents a base resource object with a property indicating whether the resource is in its desired state. /// -public class BaseResourceObject +public abstract class BaseResourceObject { /// /// Gets or sets a value indicating whether the resource is in its desired state. /// [JsonProperty("_inDesiredState", NullValueHandling = NullValueHandling.Ignore)] public bool? InDesiredState { get; set; } + + /// + /// Gets the properties of the resource object. + /// + /// A Json object containing the properties of the resource. + public virtual JObject GetProperties() + { + return new JObject + { + ["_inDesiredState"] = new JObject + { + ["description"] = "Indicates whether an instance is in the desired state.", + ["type"] = "boolean", + }, + }; + } + + /// + /// Gets the required properties of the resource object. + /// + /// A Json array containing the required properties. + public abstract JArray GetRequiredProperties(); + + /// + /// Converts the current object to a JSON representation. + /// + /// A Json object representing the current object. + public JObject ToJson() + { + return JObject.FromObject(this, new JsonSerializer + { + NullValueHandling = NullValueHandling.Ignore, + }); + } } diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs index d03b291e..c8aa48ff 100644 --- a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs @@ -33,25 +33,28 @@ public class SettingsResourceObject : BaseResourceObject [JsonProperty("action", NullValueHandling = NullValueHandling.Ignore)] public string Action { get; set; } - /// - /// Creates a settings resource object with the specified settings and action. - /// - /// The JSON representation of the settings resource object. - /// A settings resource object. - public static SettingsResourceObject FromJson(JToken json) + /// + public override JObject GetProperties() { - return json.ToObject(); + var baseProperties = base.GetProperties(); + baseProperties["settings"] = new JObject + { + ["description"] = "The settings.", + ["type"] = "object", + }; + baseProperties["action"] = new JObject + { + ["default"] = ActionPartial, + ["description"] = "The action used to apply the settings.", + ["type"] = "string", + ["enum"] = new JArray(ActionFull, ActionPartial), + }; + return baseProperties; ; } - /// - /// Converts the current object to a JSON representation. - /// - /// A Json object representing the current object. - public JObject ToJson() + /// + public override JArray GetRequiredProperties() { - return JObject.FromObject(this, new JsonSerializer - { - NullValueHandling = NullValueHandling.Ignore, - }); + return ["settings"]; } } From 956357f535f3e0dd3b9b57b107917d95085648e8 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:14:26 -0700 Subject: [PATCH 05/24] Init resource file --- src/WingetCreateCLI/Commands/DscCommand.cs | 77 ++++++++++--------- .../Commands/DscCommands/BaseDscCommand.cs | 15 ++-- .../DscCommands/DscSettingsCommand.cs | 28 +++++-- ...t.winget-create.settings.dsc.resource.json | 60 +++++++++++++++ src/WingetCreateCLI/WingetCreateCLI.csproj | 39 ++++++---- 5 files changed, 156 insertions(+), 63 deletions(-) create mode 100644 src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index af2f84eb..d43ad2e7 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -9,6 +9,7 @@ namespace Microsoft.WingetCreateCLI.Commands; using System.Threading.Tasks; using CommandLine; using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Microsoft.WingetCreateCLI.Logging; using Newtonsoft.Json.Linq; /// @@ -62,66 +63,72 @@ public class DscCommand : BaseCommand /// Boolean representing success or fail of the command. public override async Task Execute() { - BaseDscCommand dscCommand; - var dscScope = this.UnboundArgs.FirstOrDefault()?.ToLowerInvariant() ?? string.Empty; - if (dscScope == "settings") - { - dscCommand = new DscSettingsCommand(); - } - else + await Task.CompletedTask; + + List dscCommands = [new DscSettingsCommand()]; + var argCommandName = this.UnboundArgs.FirstOrDefault(); + var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(argCommandName, StringComparison.OrdinalIgnoreCase)); + + if (dscCommand == null) { - Console.WriteLine($"Unknown DSC scope: {dscScope}"); + Logger.Error($"Unknown DSC resource {argCommandName}"); return false; } - JToken input; - if (this.TryParse(this.Get, out input)) - { - dscCommand.Get(input); - } - else if (this.TryParse(this.Set, out input)) + if (this.HandleOperation("Get", this.Get, (input) => dscCommand.Get(input)) || + this.HandleOperation("Set", this.Set, (input) => dscCommand.Set(input)) || + this.HandleOperation("Test", this.Test, (input) => dscCommand.Test(input)) || + this.HandleOperation("Export", this.Export, (input) => dscCommand.Export(input)) || + (this.Schema && dscCommand.Schema())) { - dscCommand.Set(input); - } - else if (this.TryParse(this.Test, out input)) - { - dscCommand.Test(input); + return true; } - else if (this.TryParse(this.Export, out input)) + + Logger.Error("No valid DSC command provided."); + return false; + } + + private bool HandleOperation(string name, string arg, Func op) + { + if (arg == null) { - dscCommand.Export(input); + // If no argument is provided, then we assume another operation is being requested. + return false; } - else if (this.Schema) + + try { - dscCommand.Schema(); + var input = this.GetJsonOrNull(arg); + if (!op(input)) + { + Logger.Error($"Invalid input for {name} command."); + return false; + } + + return true; } - else + catch (Exception ex) { - Console.WriteLine("No valid DSC command provided. Use -g, -s, -t, or -e to specify a command."); + Logger.Error(ex.Message); return false; } - - return true; } - private bool TryParse(string json, out JToken token) + private JToken GetJsonOrNull(string json) { if (string.IsNullOrWhiteSpace(json)) { - token = null; - return false; + return null; } try { - token = JToken.Parse(json); - return true; + return JToken.Parse(json); } catch (Exception ex) { - Console.WriteLine($"Error parsing JSON: {ex.Message}"); - token = null; - return false; + Logger.Error(ex.Message); + return null; } } } diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs index 56631d17..89288706 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -22,30 +22,35 @@ public abstract class BaseDscCommand /// DSC Get command. /// /// Input for the Get command. - public abstract void Get(JToken input); + /// True if the command was successful; otherwise, false. + public abstract bool Get(JToken input); /// /// DSC Set command. /// /// Input for the Set command. - public abstract void Set(JToken input); + /// True if the command was successful; otherwise, false. + public abstract bool Set(JToken input); /// /// DSC Test command. /// /// Input for the Test command. - public abstract void Test(JToken input); + /// True if the command was successful; otherwise, false. + public abstract bool Test(JToken input); /// /// DSC Export command. /// /// Input for the Export command. - public abstract void Export(JToken input); + /// True if the command was successful; otherwise, false. + public abstract bool Export(JToken input); /// /// DSC Schema command. /// - public abstract void Schema(); + /// True if the command was successful; otherwise, false. + public abstract bool Schema(); /// /// Creates a Json schema for a DSC resource object. diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index d086b0b7..1241be4f 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -15,14 +15,19 @@ public class DscSettingsCommand : BaseDscCommand public override string CommandName => "settings"; /// - public override void Get(JToken input) + public override bool Get(JToken input) { - this.Export(input); + return this.Export(input); } /// - public override void Set(JToken input) + public override bool Set(JToken input) { + if (input == null) + { + return false; + } + var data = new SettingsFunctionData(input); data.Get(); @@ -37,11 +42,17 @@ public override void Set(JToken input) this.WriteJsonOutputLine(data.Output.ToJson()); this.WriteJsonOutputLine(diff); + return true; } /// - public override void Test(JToken input) + public override bool Test(JToken input) { + if ( input == null) + { + return false; + } + var data = new SettingsFunctionData(input); data.Get(); @@ -49,21 +60,22 @@ public override void Test(JToken input) this.WriteJsonOutputLine(data.Output.ToJson()); this.WriteJsonOutputLine(data.DiffJson()); + return true; } /// - public override void Export(JToken input) + public override bool Export(JToken input) { var data = new SettingsFunctionData(); - data.Get(); - this.WriteJsonOutputLine(data.Output.ToJson()); + return true; } /// - public override void Schema() + public override bool Schema() { this.WriteJsonOutputLine(this.CreateSchema()); + return true; } } diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json new file mode 100644 index 00000000..cd18bbea --- /dev/null +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", + "description": "Allows management of settings state via the DSC v3 command line interface protocol. See the help link for details.", + "export": { + "executable": "wingetcreate", + "input": "stdin", + "args": [ + "dsc", + "settings", + "--export", + "'{}'" + ] + }, + "get": { + "executable": "wingetcreate", + "input": "stdin", + "args": [ + "dsc", + "settings", + "--get", + "'{}'" + ] + }, + "schema": { + "command": { + "executable": "wingetcreate", + "args": [ + "dsc", + "settings", + "--schema" + ] + } + }, + "set": { + "executable": "wingetcreate", + "implementsPretest": true, + "return": "stateAndDiff", + "input": "stdin", + "args": [ + "dsc", + "settings", + "--set" + ] + }, + "test": { + "executable": "wingetcreate", + "return": "stateAndDiff", + "input": "stdin", + "args": [ + "dsc", + "settings", + "--test" + ] + }, + "tags": [ + "WinGetCreate" + ], + "type": "Microsoft.WinGetCreate/Settings", + "version": "1.9.0" +} \ No newline at end of file diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index 3cdc473a..75214c1d 100644 --- a/src/WingetCreateCLI/WingetCreateCLI.csproj +++ b/src/WingetCreateCLI/WingetCreateCLI.csproj @@ -41,17 +41,17 @@ - @@ -69,7 +69,7 @@ - %(LinkBase)\%(Filename)%(Extension) @@ -81,7 +81,7 @@ PublicResXFileCodeGenerator - + @@ -96,8 +96,17 @@ - - + + + + + + %(Filename)%(Extension) +    Always +    Always + From 65b30f4882ebd7f60231c247f3a6f3278d811503 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:41:42 -0700 Subject: [PATCH 06/24] Resx --- src/WingetCreateCLI/Commands/DscCommand.cs | 39 +++------ .../Properties/Resources.Designer.cs | 81 +++++++++++++++++++ src/WingetCreateCLI/Properties/Resources.resx | 28 +++++++ 3 files changed, 120 insertions(+), 28 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index d43ad2e7..6f1f00d2 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -10,12 +10,13 @@ namespace Microsoft.WingetCreateCLI.Commands; using CommandLine; using Microsoft.WingetCreateCLI.Commands.DscCommands; using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Properties; using Newtonsoft.Json.Linq; /// /// Command for managin the application using dsc v3. /// -[Verb("dsc", HelpText = "Manage the application using dsc v3.")] +[Verb("dsc", HelpText = "DscCommand_HelpText", ResourceType = typeof(Resources))] public class DscCommand : BaseCommand { /// @@ -30,31 +31,31 @@ public class DscCommand : BaseCommand /// /// Gets or sets the input for the dsc Get operation. /// - [Option('g', "get", SetName = "GetMethod", HelpText = "Command for the Get flow.")] + [Option('g', "get", SetName = "GetMethod", HelpText = "DscGet_HelpText", ResourceType = typeof(Resources))] public string Get { get; set; } /// /// Gets or sets the input for the dsc Set operation. /// - [Option('s', "set", SetName = "SetMethod", HelpText = "Command for the Set flow.")] + [Option('s', "set", SetName = "SetMethod", HelpText = "DscSet_HelpText", ResourceType = typeof(Resources))] public string Set { get; set; } /// /// Gets or sets the input for the dsc Test operation. /// - [Option('t', "test", SetName = "TestMethod", HelpText = "Command for the Test flow.")] + [Option('t', "test", SetName = "TestMethod", HelpText = "DscTest_HelpText", ResourceType = typeof(Resources))] public string Test { get; set; } /// /// Gets or sets the input for the dsc Export operation. /// - [Option('e', "export", SetName = "ExportMethod", HelpText = "Command for the Export flow.")] + [Option('e', "export", SetName = "ExportMethod", HelpText = "DscExport_HelpText", ResourceType = typeof(Resources))] public string Export { get; set; } /// /// Gets or sets a value indicating whether to execute the schema command. /// - [Option("schema", SetName = "SchemaMethod", HelpText = "Command for the Schema flow.")] + [Option("schema", SetName = "SchemaMethod", HelpText = "DscSchema_HelpText", ResourceType = typeof(Resources))] public bool Schema { get; set; } /// @@ -71,7 +72,7 @@ public override async Task Execute() if (dscCommand == null) { - Logger.Error($"Unknown DSC resource {argCommandName}"); + Logger.ErrorLocalized(Resources.DscResourceNotFound_Message, argCommandName); return false; } @@ -84,7 +85,7 @@ public override async Task Execute() return true; } - Logger.Error("No valid DSC command provided."); + Logger.ErrorLocalized(Resources.DscResourceOperationInvalid_Message); return false; } @@ -98,10 +99,10 @@ private bool HandleOperation(string name, string arg, Func op) try { - var input = this.GetJsonOrNull(arg); + var input = string.IsNullOrWhiteSpace(arg) ? null : JToken.Parse(arg); if (!op(input)) { - Logger.Error($"Invalid input for {name} command."); + Logger.ErrorLocalized(Resources.DscResourceOperationFailed_Message); return false; } @@ -113,22 +114,4 @@ private bool HandleOperation(string name, string arg, Func op) return false; } } - - private JToken GetJsonOrNull(string json) - { - if (string.IsNullOrWhiteSpace(json)) - { - return null; - } - - try - { - return JToken.Parse(json); - } - catch (Exception ex) - { - Logger.Error(ex.Message); - return null; - } - } } diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 1eb6b026..9930eb78 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -735,6 +735,87 @@ public static string DownloadInstaller_Message { } } + /// + /// Looks up a localized string similar to DSC v3 resource commands.. + /// + public static string DscCommand_HelpText { + get { + return ResourceManager.GetString("DscCommand_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Export the resource configuration. + /// + public static string DscExport_HelpText { + get { + return ResourceManager.GetString("DscExport_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get the resource state. + /// + public static string DscGet_HelpText { + get { + return ResourceManager.GetString("DscGet_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DSC resource not found: {0}. + /// + public static string DscResourceNotFound_Message { + get { + return ResourceManager.GetString("DscResourceNotFound_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DSC resource operation failed. + /// + public static string DscResourceOperationFailed_Message { + get { + return ResourceManager.GetString("DscResourceOperationFailed_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid operation for the DSC resource. + /// + public static string DscResourceOperationInvalid_Message { + get { + return ResourceManager.GetString("DscResourceOperationInvalid_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Execute the Schema command. + /// + public static string DscSchema_HelpText { + get { + return ResourceManager.GetString("DscSchema_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set the resource state. + /// + public static string DscSet_HelpText { + get { + return ResourceManager.GetString("DscSet_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Test the resource state. + /// + public static string DscTest_HelpText { + get { + return ResourceManager.GetString("DscTest_HelpText", resourceCulture); + } + } + /// /// Looks up a localized string similar to Would you like to edit your manifests?. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 14180ffb..87bbcb66 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1406,4 +1406,32 @@ Warning: Using this argument may result in the token being logged. Consider an a The forked repository could not be synced with the upstream commits due to a merge conflict. Resolve conflicts manually and try again. + + DSC v3 resource commands. + + + Get the resource state + + + Set the resource state + + + Test the resource state + + + Export the resource configuration + + + Execute the Schema command + + + DSC resource not found: {0} + {Locked='{0}'} {0} is replaced by the DSC resource name + + + Invalid operation for the DSC resource + + + DSC resource operation failed + \ No newline at end of file From 888ec82741df999b2aebdbf0f6555fb1a80c9112 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:45:09 -0700 Subject: [PATCH 07/24] Fix resource file --- .../microsoft.winget-create.settings.dsc.resource.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json index cd18bbea..1cf97b12 100644 --- a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -39,7 +39,10 @@ "args": [ "dsc", "settings", - "--set" + { + "jsonInputArg": "--set", + "mandatory": true + } ] }, "test": { @@ -49,7 +52,10 @@ "args": [ "dsc", "settings", - "--test" + { + "jsonInputArg": "--test", + "mandatory": true + } ] }, "tags": [ From 26d50541743b45c81bca7bf16146d0f6d6ea8a61 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Thu, 12 Jun 2025 16:06:02 -0700 Subject: [PATCH 08/24] Resx --- .../Models/DscModels/BaseResourceObject.cs | 3 +- .../DscModels/SettingsResourceObject.cs | 5 ++-- .../Properties/Resources.Designer.cs | 29 ++++++++++++++++++- src/WingetCreateCLI/Properties/Resources.resx | 11 ++++++- src/WingetCreateCLI/WingetCreateCLI.csproj | 7 +++-- 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs index fb432384..5f7f90a9 100644 --- a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs @@ -3,6 +3,7 @@ namespace Microsoft.WingetCreateCLI.Models.DscModels; +using Microsoft.WingetCreateCLI.Properties; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,7 +28,7 @@ public virtual JObject GetProperties() { ["_inDesiredState"] = new JObject { - ["description"] = "Indicates whether an instance is in the desired state.", + ["description"] = Resources.DscResourcePropertyDescriptionInDesiredState, ["type"] = "boolean", }, }; diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs index c8aa48ff..a86b1fb5 100644 --- a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs @@ -3,6 +3,7 @@ namespace Microsoft.WingetCreateCLI.Models.DscModels; +using Microsoft.WingetCreateCLI.Properties; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -39,13 +40,13 @@ public override JObject GetProperties() var baseProperties = base.GetProperties(); baseProperties["settings"] = new JObject { - ["description"] = "The settings.", + ["description"] = Resources.DscResourcePropertyDescriptionSettings, ["type"] = "object", }; baseProperties["action"] = new JObject { ["default"] = ActionPartial, - ["description"] = "The action used to apply the settings.", + ["description"] = Resources.DscResourcePropertyDescriptionAction, ["type"] = "string", ["enum"] = new JArray(ActionFull, ActionPartial), }; diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 9930eb78..1d8a6d00 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -745,7 +745,7 @@ public static string DscCommand_HelpText { } /// - /// Looks up a localized string similar to Export the resource configuration. + /// Looks up a localized string similar to Get all state instances. /// public static string DscExport_HelpText { get { @@ -789,6 +789,33 @@ public static string DscResourceOperationInvalid_Message { } } + /// + /// Looks up a localized string similar to The action used to apply the settings.. + /// + public static string DscResourcePropertyDescriptionAction { + get { + return ResourceManager.GetString("DscResourcePropertyDescriptionAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Indicates whether an instance is in the desired state.. + /// + public static string DscResourcePropertyDescriptionInDesiredState { + get { + return ResourceManager.GetString("DscResourcePropertyDescriptionInDesiredState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The settings content.. + /// + public static string DscResourcePropertyDescriptionSettings { + get { + return ResourceManager.GetString("DscResourcePropertyDescriptionSettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Execute the Schema command. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 87bbcb66..f5ef5985 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1419,7 +1419,7 @@ Warning: Using this argument may result in the token being logged. Consider an a Test the resource state - Export the resource configuration + Get all state instances Execute the Schema command @@ -1434,4 +1434,13 @@ Warning: Using this argument may result in the token being logged. Consider an a DSC resource operation failed + + Indicates whether an instance is in the desired state. + + + The settings content. + + + The action used to apply the settings. + \ No newline at end of file diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index 75214c1d..e7e59ae9 100644 --- a/src/WingetCreateCLI/WingetCreateCLI.csproj +++ b/src/WingetCreateCLI/WingetCreateCLI.csproj @@ -97,9 +97,12 @@ - + + + + + -   %(Filename)%(Extension) From c1b6c87ce1ec41600ad1e3b660fe8e667b74174f Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:42:38 -0700 Subject: [PATCH 09/24] Fix resource file --- .../microsoft.winget-create.settings.dsc.resource.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json index 1cf97b12..bcb8d6b7 100644 --- a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -35,7 +35,6 @@ "executable": "wingetcreate", "implementsPretest": true, "return": "stateAndDiff", - "input": "stdin", "args": [ "dsc", "settings", @@ -48,7 +47,6 @@ "test": { "executable": "wingetcreate", "return": "stateAndDiff", - "input": "stdin", "args": [ "dsc", "settings", From 8035d579059dd8a5fa480bd374c9b3c57cf35c7c Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Sun, 15 Jun 2025 10:06:28 -0700 Subject: [PATCH 10/24] Adding UT --- src/WingetCreateCLI/Commands/DscCommand.cs | 13 +- .../Properties/Resources.Designer.cs | 9 + src/WingetCreateCLI/Properties/Resources.resx | 4 + .../UnitTests/DscCommandTests.cs | 121 ++++++++++ .../UnitTests/DscSettingsCommandTests.cs | 213 ++++++++++++++++++ 5 files changed, 356 insertions(+), 4 deletions(-) create mode 100644 src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs create mode 100644 src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index 6f1f00d2..c7bae00c 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -68,11 +68,16 @@ public override async Task Execute() List dscCommands = [new DscSettingsCommand()]; var argCommandName = this.UnboundArgs.FirstOrDefault(); - var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(argCommandName, StringComparison.OrdinalIgnoreCase)); + if(string.IsNullOrWhiteSpace(argCommandName)) + { + Logger.ErrorLocalized(nameof(Resources.DscResourceMissing_Message), string.Join(", ", dscCommands.Select(c => c.CommandName))); + return false; + } + var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(argCommandName, StringComparison.OrdinalIgnoreCase)); if (dscCommand == null) { - Logger.ErrorLocalized(Resources.DscResourceNotFound_Message, argCommandName); + Logger.ErrorLocalized(nameof(Resources.DscResourceNotFound_Message), argCommandName); return false; } @@ -85,7 +90,7 @@ public override async Task Execute() return true; } - Logger.ErrorLocalized(Resources.DscResourceOperationInvalid_Message); + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationInvalid_Message)); return false; } @@ -102,7 +107,7 @@ private bool HandleOperation(string name, string arg, Func op) var input = string.IsNullOrWhiteSpace(arg) ? null : JToken.Parse(arg); if (!op(input)) { - Logger.ErrorLocalized(Resources.DscResourceOperationFailed_Message); + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationFailed_Message)); return false; } diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 1d8a6d00..79bf95fb 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -762,6 +762,15 @@ public static string DscGet_HelpText { } } + /// + /// Looks up a localized string similar to DSC resource is missing: {0}. + /// + public static string DscResourceMissing_Message { + get { + return ResourceManager.GetString("DscResourceMissing_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to DSC resource not found: {0}. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index f5ef5985..288268a9 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1428,6 +1428,10 @@ Warning: Using this argument may result in the token being logged. Consider an a DSC resource not found: {0} {Locked='{0}'} {0} is replaced by the DSC resource name + + DSC resource is missing: {0} + {Locked='{0}'} {0} is replaced by the available DSC resource names + Invalid operation for the DSC resource diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs new file mode 100644 index 00000000..ff58af6c --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateUnitTests; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommandLine; +using Microsoft.WingetCreateCLI.Commands; +using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Properties; +using NUnit.Framework; + +/// +/// Unit test class for the DSC Command. +/// +public class DscCommandTests +{ + /// + /// Execute the DSC command + /// + /// The arguments to pass to the DSC command. + /// Result of executing the DSC command. + public static async Task ExecuteDscCommandAsync(List args) + { + var sw = new StringWriter(); + Console.SetOut(sw); + var executeResult = await Parser.Default.ParseArguments(args).Value.Execute(); + var output = sw.ToString(); + return new(executeResult, output); + } + + /// + /// OneTimeSetup method for the DSC command unit tests. + /// + [OneTimeSetUp] + public void OneTimeSetUp() + { + Logger.Initialize(); + } + + [Test] + public async Task DscSettingsResource_Success() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + + // Assert + Assert.That(result.Success, Is.True); + } + + [Test] + public async Task DscResourceMissing_ErrorMessage() + { + // Arrange + List dscCommands = [new DscSettingsCommand()]; + + // Act + var result = await ExecuteDscCommandAsync([]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceMissing_Message, string.Join(", ", dscCommands.Select(c => c.CommandName))))); + } + + [Test] + public async Task DscResourceNotFound_ErrorMessage() + { + // Arrange + var dscResourceName = "ResourceNotFound"; + + // Act + var result = await ExecuteDscCommandAsync([dscResourceName]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNotFound_Message, dscResourceName))); + } + + [Test] + public async Task DscResourceInvalidOperation_ErrorMessage() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await ExecuteDscCommandAsync([command.CommandName]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationInvalid_Message)); + } + + [Test] + public async Task DscSettingsResourceFailedOperation_ErrorMessage() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); + } + + /// + /// Result of executing a DSC command. + /// + /// Value indicating whether the command execution was successful. + /// Value containing the output of the command execution. + public record class ExecuteResult(bool Success, string Output); +} diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs new file mode 100644 index 00000000..f02591b7 --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateUnitTests; + +using System.Threading.Tasks; +using Microsoft.WingetCreateCLI; +using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Models.DscModels; +using Microsoft.WingetCreateCLI.Models.Settings; +using Microsoft.WingetCreateCLI.Properties; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +/// +/// Unit test class for the DSC settings Command. +/// +public class DscSettingsCommandTests +{ + private const string MockName = "mock_name"; + private const string MockOwner = "mock_owner"; + private JToken rawOriginalSettings; + + /// + /// Gets the settings state with default values. + /// + private SettingsManifest DefaultSettings => new(); + + /// + /// Gets the settings state after the test is run. + /// + private SettingsManifest CurrentSettings => UserSettings.ToJson().ToObject(); + + /// + /// Gets the settings state before the test is run. + /// + private SettingsManifest OriginalSettings => this.rawOriginalSettings.ToObject(); + + /// + /// OneTimeSetup method for the DSC command unit tests. + /// + [OneTimeSetUp] + public void OneTimeSetUp() + { + Logger.Initialize(); + } + + /// + /// Setup method for the cache command unit tests. + /// + [SetUp] + public void SetUp() + { + this.rawOriginalSettings = UserSettings.ToJson(); + } + + /// + /// Teardown method for each individual test. + /// + [TearDown] + public void TearDown() + { + UserSettings.SaveSettings(this.OriginalSettings); + } + + [Test] + public async Task DscSettingsResource_Get_Success() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + + // Assert + Assert.That(result.Success, Is.True); + Assert.That(result.Output, Does.Contain(this.CreateGetResponse())); + } + + [Test] + public async Task DscSettingsResource_Export_Success() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--export", string.Empty]); + + // Assert + Assert.That(result.Success, Is.True); + Assert.That(result.Output, Does.Contain(this.CreateGetResponse())); + } + + [Test] + public async Task DscSettingsResource_SetEmpty_Fail() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_Set_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + var command = new DscSettingsCommand(); + + // Part 1: Update settings repo name only + { + // Act + var setRepoName = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + + // Assert + Assert.That(setRepoName.Success, Is.True); + this.AssertSettingsRepoHasChanged(name: MockName); + } + + // Part 2: Now update settings repo owner only + { + // Act + var setRepoOwner = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + + // Assert + Assert.That(setRepoOwner.Success, Is.True); + this.AssertSettingsRepoHasChanged(name: (isPartial ?? true) ? MockName : null, owner: MockOwner); + } + } + + [Test] + public async Task DscSettingsResource_Test_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + var command = new DscSettingsCommand(); + var currentSettings = this.CurrentSettings; + currentSettings.WindowsPackageManagerRepository.Name = MockName; + UserSettings.SaveSettings(currentSettings); + + // TODO + } + + private void ResetSettingsToDefaultValues() + { + UserSettings.SaveSettings(this.DefaultSettings); + } + + private string CreateGetResponse(bool? isPartial = null) + { + return this.CreateResourceObject(UserSettings.ToJson(), isPartial); + } + + private string CreateInput(string name = null, string owner = null, bool? isPartial = null) + { + var repo = new JObject(); + if (name != null) + { + repo["name"] = name; + } + + if (owner != null) + { + repo["owner"] = owner; + } + + var input = new JObject + { + [nameof(WindowsPackageManagerRepository)] = repo, + }; + + return this.CreateResourceObject(input, isPartial); + } + + private string CreateResourceObject(JObject settings, bool? isPartial) + { + var resourceObject = new SettingsResourceObject + { + Settings = settings, + Action = isPartial.HasValue ? (isPartial.Value ? SettingsResourceObject.ActionPartial : SettingsResourceObject.ActionFull) : null, + }; + return JObject.FromObject(resourceObject).ToString(Formatting.None); + } + + private void AssertSettingsRepoHasChanged(string name = null, string owner = null) + { + var currentSettings = this.CurrentSettings; + var defaultSettings = this.DefaultSettings; + if (name != null) + { + defaultSettings.WindowsPackageManagerRepository.Name = name; + } + + if (owner != null) + { + defaultSettings.WindowsPackageManagerRepository.Owner = owner; + } + + var currentSettingsJson = JToken.FromObject(currentSettings); + var defaultSettingsJson = JToken.FromObject(defaultSettings); + Assert.That(JToken.DeepEquals(currentSettingsJson, defaultSettingsJson), Is.True); + } +} From 12b66f4c4b636ad7de462f32dfee4560def09a05 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 16 Jun 2025 09:10:38 -0700 Subject: [PATCH 11/24] Adding more UT --- .../Models/DscExecuteResult.cs | 61 ++++ .../WingetCreateTests/TestUtils.cs | 17 ++ .../UnitTests/DscCommandTests.cs | 36 +-- .../UnitTests/DscSettingsCommandTests.cs | 281 ++++++++++++++++-- 4 files changed, 348 insertions(+), 47 deletions(-) create mode 100644 src/WingetCreateTests/WingetCreateTests/Models/DscExecuteResult.cs diff --git a/src/WingetCreateTests/WingetCreateTests/Models/DscExecuteResult.cs b/src/WingetCreateTests/WingetCreateTests/Models/DscExecuteResult.cs new file mode 100644 index 00000000..5c4fb083 --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/Models/DscExecuteResult.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateUnitTests.Models; + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.WingetCreateCLI.Models.DscModels; +using Newtonsoft.Json; + +/// +/// Result of executing a DSC command. +/// +public class DscExecuteResult +{ + /// + /// Initializes a new instance of the class. + /// + /// Value indicating whether the command execution was successful. + /// Output of the command execution. + public DscExecuteResult(bool success, string output) + { + this.Success = success; + this.Output = output; + } + + /// + /// Gets a value indicating whether the command execution was successful. + /// + public bool Success { get; } + + /// + /// Gets the output result of the operation. + /// + public string Output { get; } + + /// + /// Gets the output as settings state. + /// + /// Settings state. + public SettingsResourceObject OutputState() + { + var lines = this.Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); + Debug.Assert(lines.Length == 1, "Output should contain exactly one line."); + return JsonConvert.DeserializeObject(lines[0]); + } + + /// + /// Gets the output as settings state and diff. + /// + /// Settings state and diff. + public (SettingsResourceObject State, List Diff) OutputStateAndDiff() + { + var lines = this.Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); + Debug.Assert(lines.Length == 2, "Output should contain exactly two lines."); + var settingsObject = JsonConvert.DeserializeObject(lines[0]); + var diff = JsonConvert.DeserializeObject>(lines[1]); + return (settingsObject, diff); + } +} diff --git a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs index b503ec3a..6099f3ba 100644 --- a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs +++ b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs @@ -13,7 +13,10 @@ namespace Microsoft.WingetCreateTests using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; + using CommandLine; + using Microsoft.WingetCreateCLI.Commands; using Microsoft.WingetCreateCore; + using Microsoft.WingetCreateUnitTests.Models; using Moq; using Moq.Protected; @@ -213,5 +216,19 @@ public static void DeleteCachedFiles(List testFileNames) File.Delete(Path.Combine(PackageParser.InstallerDownloadPath, fileName)); } } + + /// + /// Execute the DSC command. + /// + /// The arguments to pass to the DSC command. + /// Result of executing the DSC command. + public static async Task ExecuteDscCommandAsync(List args) + { + var sw = new StringWriter(); + Console.SetOut(sw); + var executeResult = await Parser.Default.ParseArguments(args).Value.Execute(); + var output = sw.ToString(); + return new(executeResult, output); + } } } diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs index ff58af6c..e83c089c 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -3,16 +3,13 @@ namespace Microsoft.WingetCreateUnitTests; -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading.Tasks; -using CommandLine; -using Microsoft.WingetCreateCLI.Commands; using Microsoft.WingetCreateCLI.Commands.DscCommands; using Microsoft.WingetCreateCLI.Logging; using Microsoft.WingetCreateCLI.Properties; +using Microsoft.WingetCreateTests; using NUnit.Framework; /// @@ -20,20 +17,6 @@ namespace Microsoft.WingetCreateUnitTests; /// public class DscCommandTests { - /// - /// Execute the DSC command - /// - /// The arguments to pass to the DSC command. - /// Result of executing the DSC command. - public static async Task ExecuteDscCommandAsync(List args) - { - var sw = new StringWriter(); - Console.SetOut(sw); - var executeResult = await Parser.Default.ParseArguments(args).Value.Execute(); - var output = sw.ToString(); - return new(executeResult, output); - } - /// /// OneTimeSetup method for the DSC command unit tests. /// @@ -50,7 +33,7 @@ public async Task DscSettingsResource_Success() var command = new DscSettingsCommand(); // Act - var result = await ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); // Assert Assert.That(result.Success, Is.True); @@ -63,7 +46,7 @@ public async Task DscResourceMissing_ErrorMessage() List dscCommands = [new DscSettingsCommand()]; // Act - var result = await ExecuteDscCommandAsync([]); + var result = await TestUtils.ExecuteDscCommandAsync([]); // Assert Assert.That(result.Success, Is.False); @@ -77,7 +60,7 @@ public async Task DscResourceNotFound_ErrorMessage() var dscResourceName = "ResourceNotFound"; // Act - var result = await ExecuteDscCommandAsync([dscResourceName]); + var result = await TestUtils.ExecuteDscCommandAsync([dscResourceName]); // Assert Assert.That(result.Success, Is.False); @@ -91,7 +74,7 @@ public async Task DscResourceInvalidOperation_ErrorMessage() var command = new DscSettingsCommand(); // Act - var result = await ExecuteDscCommandAsync([command.CommandName]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName]); // Assert Assert.That(result.Success, Is.False); @@ -105,17 +88,10 @@ public async Task DscSettingsResourceFailedOperation_ErrorMessage() var command = new DscSettingsCommand(); // Act - var result = await ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); // Assert Assert.That(result.Success, Is.False); Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); } - - /// - /// Result of executing a DSC command. - /// - /// Value indicating whether the command execution was successful. - /// Value containing the output of the command execution. - public record class ExecuteResult(bool Success, string Output); } diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs index f02591b7..a31f2feb 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -3,6 +3,7 @@ namespace Microsoft.WingetCreateUnitTests; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.WingetCreateCLI; using Microsoft.WingetCreateCLI.Commands.DscCommands; @@ -10,6 +11,7 @@ namespace Microsoft.WingetCreateUnitTests; using Microsoft.WingetCreateCLI.Models.DscModels; using Microsoft.WingetCreateCLI.Models.Settings; using Microsoft.WingetCreateCLI.Properties; +using Microsoft.WingetCreateTests; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -65,6 +67,10 @@ public void TearDown() UserSettings.SaveSettings(this.OriginalSettings); } + /// + /// Tests the Get operation. + /// + /// Async task. [Test] public async Task DscSettingsResource_Get_Success() { @@ -72,13 +78,20 @@ public async Task DscSettingsResource_Get_Success() var command = new DscSettingsCommand(); // Act - var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + var state = result.OutputState(); // Assert Assert.That(result.Success, Is.True); Assert.That(result.Output, Does.Contain(this.CreateGetResponse())); + Assert.That(state.Action, Is.Null); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, state); } + /// + /// Tests the Export operation. + /// + /// Async task. [Test] public async Task DscSettingsResource_Export_Success() { @@ -86,13 +99,20 @@ public async Task DscSettingsResource_Export_Success() var command = new DscSettingsCommand(); // Act - var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--export", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--export", string.Empty]); + var state = result.OutputState(); // Assert Assert.That(result.Success, Is.True); Assert.That(result.Output, Does.Contain(this.CreateGetResponse())); + Assert.That(state.Action, Is.Null); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, state); } + /// + /// Tests the Set operation with an empty input. + /// + /// Async task. [Test] public async Task DscSettingsResource_SetEmpty_Fail() { @@ -100,18 +120,22 @@ public async Task DscSettingsResource_SetEmpty_Fail() var command = new DscSettingsCommand(); // Act - var result = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); // Assert Assert.That(result.Success, Is.False); Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); } + /// + /// Tests the Set operation with diff. + /// + /// Async task. [Test] [TestCase(true)] [TestCase(false)] [TestCase(null)] - public async Task DscSettingsResource_Set_Success(bool? isPartial) + public async Task DscSettingsResource_SetWithDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); @@ -120,47 +144,221 @@ public async Task DscSettingsResource_Set_Success(bool? isPartial) // Part 1: Update settings repo name only { // Act - var setRepoName = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + var setRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + var stateAndDiff = setRepoName.OutputStateAndDiff(); // Assert Assert.That(setRepoName.Success, Is.True); - this.AssertSettingsRepoHasChanged(name: MockName); + this.AssertSettingsHasChanged(name: MockName); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); + this.AssertStateAction(stateAndDiff.State, isPartial); } // Part 2: Now update settings repo owner only { // Act - var setRepoOwner = await DscCommandTests.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var setRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var stateAndDiff = setRepoOwner.OutputStateAndDiff(); // Assert Assert.That(setRepoOwner.Success, Is.True); - this.AssertSettingsRepoHasChanged(name: (isPartial ?? true) ? MockName : null, owner: MockOwner); + this.AssertSettingsHasChanged(name: (isPartial ?? true) ? MockName : null, owner: MockOwner); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); + this.AssertStateAction(stateAndDiff.State, isPartial); } } + /// + /// Tests the Set operation without diff. + /// + /// Async task. [Test] - public async Task DscSettingsResource_Test_Success(bool? isPartial) + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); var command = new DscSettingsCommand(); - var currentSettings = this.CurrentSettings; - currentSettings.WindowsPackageManagerRepository.Name = MockName; - UserSettings.SaveSettings(currentSettings); - // TODO + // Part 1: Update settings repo name only + { + // Arrange + this.UpdateSettings(name: MockName); + var currentSettingsBeforeExecute = this.CurrentSettings; + + // Act + var setRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + var stateAndDiff = setRepoName.OutputStateAndDiff(); + + // Assert + Assert.That(setRepoName.Success, Is.True); + this.AssertSettingsHasChanged(name: MockName); + this.AssertStateAndSettingsAreEqual(currentSettingsBeforeExecute, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.Empty); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + + // Part 2: Now update settings repo owner only + { + // Arrange + var defaultRepoName = this.DefaultSettings.WindowsPackageManagerRepository.Name; + this.UpdateSettings(name: (isPartial ?? true) ? null : defaultRepoName, owner: MockOwner); + var currentSettingsBeforeExecute = this.CurrentSettings; + + // Act + var setRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var stateAndDiff = setRepoOwner.OutputStateAndDiff(); + + // Assert + Assert.That(setRepoOwner.Success, Is.True); + this.AssertSettingsHasChanged(name: (isPartial ?? true) ? MockName : null, owner: MockOwner); + this.AssertStateAndSettingsAreEqual(currentSettingsBeforeExecute, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.Empty); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + } + + /// + /// Tests the Test operation with diff. + /// + /// Async task. + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + var command = new DscSettingsCommand(); + + // Part 1: Test settings repo name only + { + // Act + var testRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)]); + var stateAndDiff = testRepoName.OutputStateAndDiff(); + + // Assert + Assert.That(testRepoName.Success, Is.True); + this.AssertSettingsAreEqual(this.DefaultSettings, this.CurrentSettings); + this.AssertStateAndSettingsAreEqual(this.DefaultSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + + // Part 2: Now test settings repo owner only + { + // Act + var testRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var stateAndDiff = testRepoOwner.OutputStateAndDiff(); + + // Assert + Assert.That(testRepoOwner.Success, Is.True); + this.AssertSettingsAreEqual(this.DefaultSettings, this.CurrentSettings); + this.AssertStateAndSettingsAreEqual(this.DefaultSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); + this.AssertStateAction(stateAndDiff.State, isPartial); + } } + /// + /// Tests the Test operation without diff. + /// + /// Async task. + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + var command = new DscSettingsCommand(); + + // Part 1: Test settings repo name only + { + // Arrange + this.UpdateSettings(name: MockName); + + // Act + var testRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)]); + var stateAndDiff = testRepoName.OutputStateAndDiff(); + + // Assert + Assert.That(testRepoName.Success, Is.True); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.Empty); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + + // Part 2: Now test settings repo owner only + { + // Arrange + var defaultRepoName = this.DefaultSettings.WindowsPackageManagerRepository.Name; + this.UpdateSettings(name: (isPartial ?? true) ? null : defaultRepoName, owner: MockOwner); + + // Act + var testRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var stateAndDiff = testRepoOwner.OutputStateAndDiff(); + + // Assert + Assert.That(testRepoOwner.Success, Is.True); + this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.Empty); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + } + + /// + /// Resets the settings to default values. + /// private void ResetSettingsToDefaultValues() { UserSettings.SaveSettings(this.DefaultSettings); } + /// + /// Updates the settings with the provided name and owner. + /// + /// Optional name for the repository. + /// Optional owner for the repository. + private void UpdateSettings(string name = null, string owner = null) + { + var settings = this.CurrentSettings; + if (name != null) + { + settings.WindowsPackageManagerRepository.Name = name; + } + + if (owner != null) + { + settings.WindowsPackageManagerRepository.Owner = owner; + } + + UserSettings.SaveSettings(settings); + } + + /// + /// Create the response for the Get operation. + /// + /// Optional parameter to indicate if the response is partial. + /// A JSON string representing the response. private string CreateGetResponse(bool? isPartial = null) { return this.CreateResourceObject(UserSettings.ToJson(), isPartial); } + /// + /// Creates the operation input. + /// + /// Optional name for the repository. + /// Optional owner for the repository. + /// Optional parameter to indicate if the input is partial. + /// A JSON string representing the operation input. private string CreateInput(string name = null, string owner = null, bool? isPartial = null) { var repo = new JObject(); @@ -182,6 +380,12 @@ private string CreateInput(string name = null, string owner = null, bool? isPart return this.CreateResourceObject(input, isPartial); } + /// + /// Create the resource object for the operation. + /// + /// Settings to include in the resource object. + /// Optional parameter to indicate if the resource object is partial. + /// A JSON string representing the resource object. private string CreateResourceObject(JObject settings, bool? isPartial) { var resourceObject = new SettingsResourceObject @@ -192,7 +396,12 @@ private string CreateResourceObject(JObject settings, bool? isPartial) return JObject.FromObject(resourceObject).ToString(Formatting.None); } - private void AssertSettingsRepoHasChanged(string name = null, string owner = null) + /// + /// Asserts that the current settings have changed based on the provided name and owner. + /// + /// Optional name for the repository. + /// Optional owner for the repository. + private void AssertSettingsHasChanged(string name = null, string owner = null) { var currentSettings = this.CurrentSettings; var defaultSettings = this.DefaultSettings; @@ -206,8 +415,46 @@ private void AssertSettingsRepoHasChanged(string name = null, string owner = nul defaultSettings.WindowsPackageManagerRepository.Owner = owner; } - var currentSettingsJson = JToken.FromObject(currentSettings); - var defaultSettingsJson = JToken.FromObject(defaultSettings); - Assert.That(JToken.DeepEquals(currentSettingsJson, defaultSettingsJson), Is.True); + this.AssertSettingsAreEqual(defaultSettings, currentSettings); + } + + /// + /// Asserts that the state and settings are equal. + /// + /// Settings manifest to compare against. + /// Output state to compare. + private void AssertStateAndSettingsAreEqual(SettingsManifest settings, SettingsResourceObject state) + { + var stateSettings = state.Settings.ToObject(); + this.AssertSettingsAreEqual(settings, stateSettings); + } + + /// + /// Asserts that the state action is as expected. + /// + /// Output state to check. + /// Optional parameter to indicate if the state is partial. + private void AssertStateAction(SettingsResourceObject state, bool? isPartial = null) + { + if (!isPartial.HasValue || isPartial.Value) + { + Assert.That(state.Action, Is.EqualTo(SettingsResourceObject.ActionPartial)); + } + else + { + Assert.That(state.Action, Is.EqualTo(SettingsResourceObject.ActionFull)); + } + } + + /// + /// Asserts that two settings manifests are equal. + /// + /// Expected settings manifest. + /// Actual settings manifest. + private void AssertSettingsAreEqual(SettingsManifest expected, SettingsManifest actual) + { + var expectedJson = JToken.FromObject(expected); + var actualJson = JToken.FromObject(actual); + Assert.That(JToken.DeepEquals(expectedJson, actualJson), Is.True); } } From bc41cde4f0bf16e3d04c08e1a3434ddd2e450659 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:00:01 -0700 Subject: [PATCH 12/24] Assert InDesiredState for Test operation --- .../WingetCreateTests/UnitTests/DscSettingsCommandTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs index a31f2feb..fda1f662 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -248,6 +248,7 @@ public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) this.AssertStateAndSettingsAreEqual(this.DefaultSettings, stateAndDiff.State); Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); this.AssertStateAction(stateAndDiff.State, isPartial); + Assert.That(stateAndDiff.State.InDesiredState, Is.False); } // Part 2: Now test settings repo owner only @@ -262,6 +263,7 @@ public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) this.AssertStateAndSettingsAreEqual(this.DefaultSettings, stateAndDiff.State); Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); this.AssertStateAction(stateAndDiff.State, isPartial); + Assert.That(stateAndDiff.State.InDesiredState, Is.False); } } @@ -293,6 +295,7 @@ public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); Assert.That(stateAndDiff.Diff, Is.Empty); this.AssertStateAction(stateAndDiff.State, isPartial); + Assert.That(stateAndDiff.State.InDesiredState, Is.True); } // Part 2: Now test settings repo owner only @@ -310,6 +313,7 @@ public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) this.AssertStateAndSettingsAreEqual(this.CurrentSettings, stateAndDiff.State); Assert.That(stateAndDiff.Diff, Is.Empty); this.AssertStateAction(stateAndDiff.State, isPartial); + Assert.That(stateAndDiff.State.InDesiredState, Is.True); } } From 0e71f5aa4417a3edaa8f37658f52f04c564761bb Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:46:54 -0700 Subject: [PATCH 13/24] Addressed comments --- src/WingetCreateCLI/Commands/DscCommand.cs | 89 +++++++++---------- .../DscCommands/DscSettingsCommand.cs | 7 +- .../Properties/Resources.Designer.cs | 26 +++++- src/WingetCreateCLI/Properties/Resources.resx | 18 ++-- .../UnitTests/DscCommandTests.cs | 34 +++---- .../UnitTests/DscSettingsCommandTests.cs | 24 ++++- 6 files changed, 125 insertions(+), 73 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index c7bae00c..dd46c9e1 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -23,34 +23,40 @@ public class DscCommand : BaseCommand public override bool RequiresGitHubToken => false; /// - /// Gets or sets the unbound arguments that exist after the positional parameters. + /// Gets or sets the name of the resource to be managed by the dsc command. /// - [Value(2, Hidden = true)] - public IList UnboundArgs { get; set; } = new List(); + [Value(0, MetaName = "ResourceName", Required = true, HelpText = "DscResourceName_HelpText", ResourceType = typeof(Resources))] + public string ResourceName { get; set; } /// - /// Gets or sets the input for the dsc Get operation. + /// Gets or sets the input for the dsc command. + /// + [Value(1, MetaName = "Input", Required = false, HelpText = "DscInput_HelpText", ResourceType = typeof(Resources))] + public string Input { get; set; } + + /// + /// Gets or sets a value indicating whether to execute the dsc Get operation. /// [Option('g', "get", SetName = "GetMethod", HelpText = "DscGet_HelpText", ResourceType = typeof(Resources))] - public string Get { get; set; } + public bool Get { get; set; } /// - /// Gets or sets the input for the dsc Set operation. + /// Gets or sets a value indicating whether to execute the dsc Set operation. /// [Option('s', "set", SetName = "SetMethod", HelpText = "DscSet_HelpText", ResourceType = typeof(Resources))] - public string Set { get; set; } + public bool Set { get; set; } /// - /// Gets or sets the input for the dsc Test operation. + /// Gets or sets a value indicating whether to execute the dsc Test operation. /// [Option('t', "test", SetName = "TestMethod", HelpText = "DscTest_HelpText", ResourceType = typeof(Resources))] - public string Test { get; set; } + public bool Test { get; set; } /// - /// Gets or sets the input for the dsc Export operation. + /// Gets or sets a value indicating whether to execute the dsc Export operation. /// [Option('e', "export", SetName = "ExportMethod", HelpText = "DscExport_HelpText", ResourceType = typeof(Resources))] - public string Export { get; set; } + public bool Export { get; set; } /// /// Gets or sets a value indicating whether to execute the schema command. @@ -67,51 +73,42 @@ public override async Task Execute() await Task.CompletedTask; List dscCommands = [new DscSettingsCommand()]; - var argCommandName = this.UnboundArgs.FirstOrDefault(); - if(string.IsNullOrWhiteSpace(argCommandName)) - { - Logger.ErrorLocalized(nameof(Resources.DscResourceMissing_Message), string.Join(", ", dscCommands.Select(c => c.CommandName))); - return false; - } - - var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(argCommandName, StringComparison.OrdinalIgnoreCase)); + var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(this.ResourceName, StringComparison.OrdinalIgnoreCase)); if (dscCommand == null) { - Logger.ErrorLocalized(nameof(Resources.DscResourceNotFound_Message), argCommandName); - return false; - } - - if (this.HandleOperation("Get", this.Get, (input) => dscCommand.Get(input)) || - this.HandleOperation("Set", this.Set, (input) => dscCommand.Set(input)) || - this.HandleOperation("Test", this.Test, (input) => dscCommand.Test(input)) || - this.HandleOperation("Export", this.Export, (input) => dscCommand.Export(input)) || - (this.Schema && dscCommand.Schema())) - { - return true; - } - - Logger.ErrorLocalized(nameof(Resources.DscResourceOperationInvalid_Message)); - return false; - } - - private bool HandleOperation(string name, string arg, Func op) - { - if (arg == null) - { - // If no argument is provided, then we assume another operation is being requested. + var availableResources = string.Join(", ", dscCommands.Select(c => c.CommandName)); + Logger.ErrorLocalized(nameof(Resources.DscResourceNotFound_Message), this.ResourceName, availableResources); return false; } try { - var input = string.IsNullOrWhiteSpace(arg) ? null : JToken.Parse(arg); - if (!op(input)) + var input = string.IsNullOrWhiteSpace(this.Input) ? null : JToken.Parse(this.Input); + var operations = new (bool, Func)[] { - Logger.ErrorLocalized(nameof(Resources.DscResourceOperationFailed_Message)); - return false; + (this.Get, () => dscCommand.Get(input)), + (this.Set, () => dscCommand.Set(input)), + (this.Test, () => dscCommand.Test(input)), + (this.Export, () => dscCommand.Export(input)), + (this.Schema, () => dscCommand.Schema()), + }; + + foreach (var (methodFlag, methodAction) in operations) + { + if (methodFlag) + { + if (!methodAction()) + { + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationFailed_Message)); + return false; + } + + return true; + } } - return true; + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationInvalid_Message)); + return false; } catch (Exception ex) { diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 1241be4f..82eca4b6 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -3,7 +3,10 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; +using System; +using Microsoft.WingetCreateCLI.Logging; using Microsoft.WingetCreateCLI.Models.DscModels; +using Microsoft.WingetCreateCLI.Properties; using Newtonsoft.Json.Linq; /// @@ -25,6 +28,7 @@ public override bool Set(JToken input) { if (input == null) { + Logger.ErrorLocalized(nameof(Resources.DscInputRequired_Message), nameof(this.Set)); return false; } @@ -48,8 +52,9 @@ public override bool Set(JToken input) /// public override bool Test(JToken input) { - if ( input == null) + if (input == null) { + Logger.ErrorLocalized(nameof(Resources.DscInputRequired_Message), nameof(this.Test)); return false; } diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 79bf95fb..917befc3 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -763,16 +763,34 @@ public static string DscGet_HelpText { } /// - /// Looks up a localized string similar to DSC resource is missing: {0}. + /// Looks up a localized string similar to The input for the DSC resource. /// - public static string DscResourceMissing_Message { + public static string DscInput_HelpText { get { - return ResourceManager.GetString("DscResourceMissing_Message", resourceCulture); + return ResourceManager.GetString("DscInput_HelpText", resourceCulture); } } /// - /// Looks up a localized string similar to DSC resource not found: {0}. + /// Looks up a localized string similar to The input for the {0} DSC operation is required.. + /// + public static string DscInputRequired_Message { + get { + return ResourceManager.GetString("DscInputRequired_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The name of the DSC resource to manage. + /// + public static string DscResourceName_HelpText { + get { + return ResourceManager.GetString("DscResourceName_HelpText", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to DSC resource not found: {0}. Valid resources: {1}. /// public static string DscResourceNotFound_Message { get { diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 288268a9..2224df3b 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1424,13 +1424,19 @@ Warning: Using this argument may result in the token being logged. Consider an a Execute the Schema command - - DSC resource not found: {0} - {Locked='{0}'} {0} is replaced by the DSC resource name + + The name of the DSC resource to manage + + + The input for the DSC resource - - DSC resource is missing: {0} - {Locked='{0}'} {0} is replaced by the available DSC resource names + + The input for the {0} DSC operation is required. + {Locked="{0}"} {0} is replaced by the DSC operation name + + + DSC resource not found: {0}. Valid resources: {1} + {Locked="{0}", "{1}"} {0} is replaced by the DSC resource name, {1} is replaced by the list of valid resources Invalid operation for the DSC resource diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs index e83c089c..26e63bf3 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -26,6 +26,10 @@ public void OneTimeSetUp() Logger.Initialize(); } + /// + /// Tests the a successful execution of the dsc command. + /// + /// Async Task. [Test] public async Task DscSettingsResource_Success() { @@ -39,34 +43,30 @@ public async Task DscSettingsResource_Success() Assert.That(result.Success, Is.True); } - [Test] - public async Task DscResourceMissing_ErrorMessage() - { - // Arrange - List dscCommands = [new DscSettingsCommand()]; - - // Act - var result = await TestUtils.ExecuteDscCommandAsync([]); - - // Assert - Assert.That(result.Success, Is.False); - Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceMissing_Message, string.Join(", ", dscCommands.Select(c => c.CommandName))))); - } - + /// + /// Tests the error message when a DSC resource is not found. + /// + /// Async Task. [Test] public async Task DscResourceNotFound_ErrorMessage() { // Arrange var dscResourceName = "ResourceNotFound"; + List dscCommands = [new DscSettingsCommand()]; + var availableResources = string.Join(", ", dscCommands.Select(c => c.CommandName)); // Act var result = await TestUtils.ExecuteDscCommandAsync([dscResourceName]); // Assert Assert.That(result.Success, Is.False); - Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNotFound_Message, dscResourceName))); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNotFound_Message, dscResourceName, availableResources))); } + /// + /// Tests the error message when an invalid operation is attempted on a DSC resource. + /// + /// Async Task. [Test] public async Task DscResourceInvalidOperation_ErrorMessage() { @@ -81,6 +81,10 @@ public async Task DscResourceInvalidOperation_ErrorMessage() Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationInvalid_Message)); } + /// + /// Tests the error message when a DSC resource operation fails. + /// + /// Async Task. [Test] public async Task DscSettingsResourceFailedOperation_ErrorMessage() { diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs index fda1f662..9061d148 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -124,12 +124,31 @@ public async Task DscSettingsResource_SetEmpty_Fail() // Assert Assert.That(result.Success, Is.False); - Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscInputRequired_Message, nameof(DscSettingsCommand.Set)))); + } + + /// + /// Tests the Test operation with an empty input. + /// + /// Async task. + [Test] + public async Task DscSettingsResource_TestEmpty_Fail() + { + // Arrange + var command = new DscSettingsCommand(); + + // Act + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", string.Empty]); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscInputRequired_Message, nameof(DscSettingsCommand.Test)))); } /// /// Tests the Set operation with diff. /// + /// Optional parameter to indicate if the operation is partial. /// Async task. [Test] [TestCase(true)] @@ -173,6 +192,7 @@ public async Task DscSettingsResource_SetWithDiff_Success(bool? isPartial) /// /// Tests the Set operation without diff. /// + /// Optional parameter to indicate if the operation is partial. /// Async task. [Test] [TestCase(true)] @@ -225,6 +245,7 @@ public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) /// /// Tests the Test operation with diff. /// + /// Optional parameter to indicate if the operation is partial. /// Async task. [Test] [TestCase(true)] @@ -270,6 +291,7 @@ public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) /// /// Tests the Test operation without diff. /// + /// Optional parameter to indicate if the operation is partial. /// Async task. [Test] [TestCase(true)] From f44e819200bfbff77ee639514c8da7267f72735c Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:56:25 -0700 Subject: [PATCH 14/24] Addressed comments --- src/WingetCreateCLI/Commands/BaseCommand.cs | 4 ++-- src/WingetCreateCLI/Commands/CacheCommand.cs | 2 +- src/WingetCreateCLI/Commands/DscCommand.cs | 2 +- src/WingetCreateCLI/Commands/InfoCommand.cs | 2 +- src/WingetCreateCLI/Commands/SettingsCommand.cs | 2 +- .../microsoft.winget-create.settings.dsc.resource.json | 6 ++---- .../Models/DscModels/SettingsResourceObject.cs | 2 +- src/WingetCreateCLI/Program.cs | 2 +- src/WingetCreateCLI/Properties/Resources.Designer.cs | 10 +++++----- src/WingetCreateCLI/Properties/Resources.resx | 10 +++++----- 10 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs index 1099dd10..39284e4d 100644 --- a/src/WingetCreateCLI/Commands/BaseCommand.cs +++ b/src/WingetCreateCLI/Commands/BaseCommand.cs @@ -62,9 +62,9 @@ public abstract class BaseCommand public static string Extension => Serialization.ManifestSerializer.AssociatedFileExtension; /// - /// Gets a value indicating whether or not the command requires a GitHub token to be set. + /// Gets a value indicating whether or not the command accepts a GitHub token. /// - public virtual bool RequiresGitHubToken { get; } = true; + public virtual bool AcceptsGitHubToken { get; } = true; /// /// Gets or sets the GitHub token used to submit a pull request on behalf of the user. diff --git a/src/WingetCreateCLI/Commands/CacheCommand.cs b/src/WingetCreateCLI/Commands/CacheCommand.cs index dfbee690..a66f6408 100644 --- a/src/WingetCreateCLI/Commands/CacheCommand.cs +++ b/src/WingetCreateCLI/Commands/CacheCommand.cs @@ -39,7 +39,7 @@ public class CacheCommand : BaseCommand public bool Open { get; set; } /// - public override bool RequiresGitHubToken => false; + public override bool AcceptsGitHubToken => false; /// /// Executes the cache command flow. diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index dd46c9e1..88f84477 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -20,7 +20,7 @@ namespace Microsoft.WingetCreateCLI.Commands; public class DscCommand : BaseCommand { /// - public override bool RequiresGitHubToken => false; + public override bool AcceptsGitHubToken => false; /// /// Gets or sets the name of the resource to be managed by the dsc command. diff --git a/src/WingetCreateCLI/Commands/InfoCommand.cs b/src/WingetCreateCLI/Commands/InfoCommand.cs index 2fe5f8fb..0f9a9c13 100644 --- a/src/WingetCreateCLI/Commands/InfoCommand.cs +++ b/src/WingetCreateCLI/Commands/InfoCommand.cs @@ -21,7 +21,7 @@ namespace Microsoft.WingetCreateCLI.Commands public class InfoCommand : BaseCommand { /// - public override bool RequiresGitHubToken => false; + public override bool AcceptsGitHubToken => false; /// /// Executes the info command flow. diff --git a/src/WingetCreateCLI/Commands/SettingsCommand.cs b/src/WingetCreateCLI/Commands/SettingsCommand.cs index 28025699..c3465143 100644 --- a/src/WingetCreateCLI/Commands/SettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/SettingsCommand.cs @@ -23,7 +23,7 @@ namespace Microsoft.WingetCreateCLI.Commands public class SettingsCommand : BaseCommand { /// - public override bool RequiresGitHubToken => false; + public override bool AcceptsGitHubToken => false; /// /// Executes the token command flow. diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json index bcb8d6b7..ee979604 100644 --- a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -7,8 +7,7 @@ "args": [ "dsc", "settings", - "--export", - "'{}'" + "--export" ] }, "get": { @@ -17,8 +16,7 @@ "args": [ "dsc", "settings", - "--get", - "'{}'" + "--get" ] }, "schema": { diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs index a86b1fb5..47b4407d 100644 --- a/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs @@ -50,7 +50,7 @@ public override JObject GetProperties() ["type"] = "string", ["enum"] = new JArray(ActionFull, ActionPartial), }; - return baseProperties; ; + return baseProperties; } /// diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs index f2871bdf..16351c4f 100644 --- a/src/WingetCreateCLI/Program.cs +++ b/src/WingetCreateCLI/Program.cs @@ -72,7 +72,7 @@ private static async Task Main(string[] args) } // Do not load github client for commands that do not deal with a GitHub token. - if (command.RequiresGitHubToken) + if (command.AcceptsGitHubToken) { if (await command.LoadGitHubClient()) { diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 917befc3..438cb910 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -736,7 +736,7 @@ public static string DownloadInstaller_Message { } /// - /// Looks up a localized string similar to DSC v3 resource commands.. + /// Looks up a localized string similar to DSC v3 resource commands. /// public static string DscCommand_HelpText { get { @@ -772,7 +772,7 @@ public static string DscInput_HelpText { } /// - /// Looks up a localized string similar to The input for the {0} DSC operation is required.. + /// Looks up a localized string similar to The input for the {0} DSC operation is required. /// public static string DscInputRequired_Message { get { @@ -817,7 +817,7 @@ public static string DscResourceOperationInvalid_Message { } /// - /// Looks up a localized string similar to The action used to apply the settings.. + /// Looks up a localized string similar to The action used to apply the settings. /// public static string DscResourcePropertyDescriptionAction { get { @@ -826,7 +826,7 @@ public static string DscResourcePropertyDescriptionAction { } /// - /// Looks up a localized string similar to Indicates whether an instance is in the desired state.. + /// Looks up a localized string similar to Indicates whether an instance is in the desired state. /// public static string DscResourcePropertyDescriptionInDesiredState { get { @@ -835,7 +835,7 @@ public static string DscResourcePropertyDescriptionInDesiredState { } /// - /// Looks up a localized string similar to The settings content.. + /// Looks up a localized string similar to The settings content. /// public static string DscResourcePropertyDescriptionSettings { get { diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 2224df3b..213b9e06 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1407,7 +1407,7 @@ Warning: Using this argument may result in the token being logged. Consider an a The forked repository could not be synced with the upstream commits due to a merge conflict. Resolve conflicts manually and try again. - DSC v3 resource commands. + DSC v3 resource commands Get the resource state @@ -1431,7 +1431,7 @@ Warning: Using this argument may result in the token being logged. Consider an a The input for the DSC resource - The input for the {0} DSC operation is required. + The input for the {0} DSC operation is required {Locked="{0}"} {0} is replaced by the DSC operation name @@ -1445,12 +1445,12 @@ Warning: Using this argument may result in the token being logged. Consider an a DSC resource operation failed - Indicates whether an instance is in the desired state. + Indicates whether an instance is in the desired state - The settings content. + The settings content - The action used to apply the settings. + The action used to apply the settings \ No newline at end of file From 617ec32318613f789bf475aa4bba5aa7c9dbc461 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:37:29 -0700 Subject: [PATCH 15/24] Addressed comments --- src/WingetCreateCLI/Commands/DscCommand.cs | 4 ++-- src/WingetCreateCLI/Properties/Resources.Designer.cs | 10 +++++----- src/WingetCreateCLI/Properties/Resources.resx | 6 +++--- .../WingetCreateTests/UnitTests/DscCommandTests.cs | 6 +++--- .../UnitTests/DscSettingsCommandTests.cs | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index 88f84477..8080326f 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -77,7 +77,7 @@ public override async Task Execute() if (dscCommand == null) { var availableResources = string.Join(", ", dscCommands.Select(c => c.CommandName)); - Logger.ErrorLocalized(nameof(Resources.DscResourceNotFound_Message), this.ResourceName, availableResources); + Logger.ErrorLocalized(nameof(Resources.DscResourceNameNotFound_Message), this.ResourceName, availableResources); return false; } @@ -107,7 +107,7 @@ public override async Task Execute() } } - Logger.ErrorLocalized(nameof(Resources.DscResourceOperationInvalid_Message)); + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationNotSpecified_Message)); return false; } catch (Exception ex) diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 438cb910..a7d5e75f 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -792,9 +792,9 @@ public static string DscResourceName_HelpText { /// /// Looks up a localized string similar to DSC resource not found: {0}. Valid resources: {1}. /// - public static string DscResourceNotFound_Message { + public static string DscResourceNameNotFound_Message { get { - return ResourceManager.GetString("DscResourceNotFound_Message", resourceCulture); + return ResourceManager.GetString("DscResourceNameNotFound_Message", resourceCulture); } } @@ -808,11 +808,11 @@ public static string DscResourceOperationFailed_Message { } /// - /// Looks up a localized string similar to Invalid operation for the DSC resource. + /// Looks up a localized string similar to No operation specified. Use --help to see available operations.. /// - public static string DscResourceOperationInvalid_Message { + public static string DscResourceOperationNotSpecified_Message { get { - return ResourceManager.GetString("DscResourceOperationInvalid_Message", resourceCulture); + return ResourceManager.GetString("DscResourceOperationNotSpecified_Message", resourceCulture); } } diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 213b9e06..24b89f3b 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1434,12 +1434,12 @@ Warning: Using this argument may result in the token being logged. Consider an a The input for the {0} DSC operation is required {Locked="{0}"} {0} is replaced by the DSC operation name - + DSC resource not found: {0}. Valid resources: {1} {Locked="{0}", "{1}"} {0} is replaced by the DSC resource name, {1} is replaced by the list of valid resources - - Invalid operation for the DSC resource + + No operation specified. Use --help to see available operations. DSC resource operation failed diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs index 26e63bf3..36792888 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -37,7 +37,7 @@ public async Task DscSettingsResource_Success() var command = new DscSettingsCommand(); // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get"]); // Assert Assert.That(result.Success, Is.True); @@ -60,7 +60,7 @@ public async Task DscResourceNotFound_ErrorMessage() // Assert Assert.That(result.Success, Is.False); - Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNotFound_Message, dscResourceName, availableResources))); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNameNotFound_Message, dscResourceName, availableResources))); } /// @@ -78,7 +78,7 @@ public async Task DscResourceInvalidOperation_ErrorMessage() // Assert Assert.That(result.Success, Is.False); - Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationInvalid_Message)); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationNotSpecified_Message)); } /// diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs index 9061d148..0789dd6b 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -78,7 +78,7 @@ public async Task DscSettingsResource_Get_Success() var command = new DscSettingsCommand(); // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get"]); var state = result.OutputState(); // Assert @@ -99,7 +99,7 @@ public async Task DscSettingsResource_Export_Success() var command = new DscSettingsCommand(); // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--export", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--export"]); var state = result.OutputState(); // Assert From 15e6ffbaab04aaacf3ee1c7292168a44657819f3 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:39:20 -0700 Subject: [PATCH 16/24] Addressed comments --- .../WingetCreateTests/UnitTests/DscCommandTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs index 36792888..ffee0f15 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -64,11 +64,11 @@ public async Task DscResourceNotFound_ErrorMessage() } /// - /// Tests the error message when an invalid operation is attempted on a DSC resource. + /// Tests the error message when a DSC resource operation is not specified. /// /// Async Task. [Test] - public async Task DscResourceInvalidOperation_ErrorMessage() + public async Task DscResourceOperationNotSpecified_ErrorMessage() { // Arrange var command = new DscSettingsCommand(); From e8d496ed9ec3de6378a9d955b2053670d3ad2264 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 17 Jun 2025 08:57:44 -0700 Subject: [PATCH 17/24] Fix resource file --- .../microsoft.winget-create.settings.dsc.resource.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json index ee979604..b62b4c25 100644 --- a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json", - "description": "Allows management of settings state via the DSC v3 command line interface protocol. See the help link for details.", + "description": "Allows management of settings state via the DSC v3 command line interface protocol.", "export": { "executable": "wingetcreate", "input": "stdin", From d968ed705bd72875795862d96583d3c0ef1181f5 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:44:38 -0700 Subject: [PATCH 18/24] Adding docs --- README.md | 1 + doc/dsc.md | 25 +++++++++++++++++++++++ doc/dsc/settings.md | 49 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 doc/dsc.md create mode 100644 doc/dsc/settings.md diff --git a/README.md b/README.md index c6a69910..19dd4cb6 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ choco install wingetcreate | [Settings](doc/settings.md) | Command for editing the settings file configurations | | [Cache](doc/cache.md) | Command for managing downloaded installers stored in cache | [Info](doc/info.md) | Displays information about the client | +| [Dsc](doc/dsc.md) | DSC v3 resource commands | | [-?](doc/help.md) | Displays command line help | Click on the individual commands to learn more. diff --git a/doc/dsc.md b/doc/dsc.md new file mode 100644 index 00000000..c1a5a814 --- /dev/null +++ b/doc/dsc.md @@ -0,0 +1,25 @@ +# dsc command (Winget-Create) + +The **dsc** command is the entry point for using DSC (Desired State Coonfiguration) with Winget-Create. + +## Usage + +`wingetcreate.exe dsc []` + +## Resources +| Resource | Type | Description | --get | --set | --test | --export | --schema | Link | +| ---- | ------ | ------------| ----- | ----- | ----- | ----- | ----- | ----- | +| `settings` | `Microsoft.WinGetCreate/Settings` | Manage the settings for Winget-Create | ✅ | ✅ | ✅ | ✅ | ✅ | [Details](dsc/settings.md) | + +## Arguments + +The following arguments are available: + +|
Argument
| Description | +| --------------------------------------- | ------------| +| **-g, --get** | Get the resource state +| **-s, --set** | Set the resource state | +| **-t, --test** | Test the resource state | +| **e, --export** | Get all state instances | +| **--schema** | Execute the Schema command | +| **-?, --help** | Gets additional help on this command. | diff --git a/doc/dsc/settings.md b/doc/dsc/settings.md new file mode 100644 index 00000000..2f43c2bb --- /dev/null +++ b/doc/dsc/settings.md @@ -0,0 +1,49 @@ +# Settings resource +Manage the settings for Winget-Create + +## 📄 Get +```shell +PS C:\> wingetcreate dsc settings --get +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":true},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}}} +``` + +## 🖨️ Export +ℹ️ Settings resource Get and Export operation output states are identical. +```shell +PS C:\> wingetcreate dsc settings --export +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":true},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}}} +``` + +## 📝 Set +- Action `Full`: When action is set to Full, the specified settings will be update accordingly and the remaining settings will be set to their default values. +- Action `Partial`: When action is set to Partial, only the specified settings will be updated, and the remaining settings will remain unchanged. +### 🌕 Full +```shell +PS C:\> wingetcreate dsc settings --set '{"settings": { "Telemetry": { "disable": false }}, "action": "Full"}' +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":false},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}},"action":"Full"} +["settings"] +``` + +### 🌗 Partial +```shell +PS C:\> wingetcreate dsc settings --set '{"settings": { "Telemetry": { "disable": true }}, "action": "Partial"}' +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":true},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}},"action":"Partial"} +["settings"] +``` + +## 🧪 Test +- Action `Full`: When action is set to Full, the specified settings will be tested accordingly, and the remaining settings will be tested against their default values. +- Action `Partial`: When action is set to Partial, only the specified settings will be tested, and the remaining settings will be omitted from the test. +### 🌕 Full +```shell +PS C:\> wingetcreate dsc settings --test '{"settings": { "Telemetry": { "disable": false }}, "action": "Full"}' +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":true},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}},"action":"Full","_inDesiredState":false} +["settings"] +``` + +### 🌗 Partial +```shell +PS C:\> wingetcreate dsc settings --test '{"settings": { "Telemetry": { "disable": false }}, "action": "Partial"}' +{"settings":{"$schema":"https://aka.ms/wingetcreate-settings.schema.0.1.json","Telemetry":{"disable":true},"CleanUp":{"intervalInDays":7,"disable":false},"WindowsPackageManagerRepository":{"owner":"microsoft","name":"winget-pkgs"},"Manifest":{"format":"yaml"},"Visual":{"anonymizePaths":true}},"action":"Partial","_inDesiredState":false} +["settings"] +``` \ No newline at end of file From 43fbd1d79539253c097162a1462814fc05868122 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:50:05 -0700 Subject: [PATCH 19/24] No wrap --- doc/dsc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dsc.md b/doc/dsc.md index c1a5a814..d7efd522 100644 --- a/doc/dsc.md +++ b/doc/dsc.md @@ -7,7 +7,7 @@ The **dsc** command is the entry point for using DSC (Desired State Coonfigurati `wingetcreate.exe dsc []` ## Resources -| Resource | Type | Description | --get | --set | --test | --export | --schema | Link | +| Resource | Type | Description |
--get
|
--set
|
--test
|
--export
|
--schema
| Link | | ---- | ------ | ------------| ----- | ----- | ----- | ----- | ----- | ----- | | `settings` | `Microsoft.WinGetCreate/Settings` | Manage the settings for Winget-Create | ✅ | ✅ | ✅ | ✅ | ✅ | [Details](dsc/settings.md) | From 5180d132f6b796b8b2a7cd463ba1e5accd4f0ae9 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:24:45 -0700 Subject: [PATCH 20/24] Typo --- doc/dsc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/dsc.md b/doc/dsc.md index d7efd522..4fd05c64 100644 --- a/doc/dsc.md +++ b/doc/dsc.md @@ -20,6 +20,6 @@ The following arguments are available: | **-g, --get** | Get the resource state | **-s, --set** | Set the resource state | | **-t, --test** | Test the resource state | -| **e, --export** | Get all state instances | +| **-e, --export** | Get all state instances | | **--schema** | Execute the Schema command | | **-?, --help** | Gets additional help on this command. | From c6f0da22fa8a94bc1df530e1b2c30078e48dee51 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:52:18 -0700 Subject: [PATCH 21/24] Update version --- .../microsoft.winget-create.settings.dsc.resource.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json index b62b4c25..f1ee9671 100644 --- a/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -58,5 +58,5 @@ "WinGetCreate" ], "type": "Microsoft.WinGetCreate/Settings", - "version": "1.9.0" + "version": "1.10.0" } \ No newline at end of file From 90753dead6fb160da8012efcc0e63c9c2256f6f3 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:14:59 -0700 Subject: [PATCH 22/24] Addressed comments --- .../DscCommands/DscSettingsCommand.cs | 2 +- .../Models/DscModels/BaseResourceObject.cs | 5 +- .../Models/DscModels/SettingsFunctionData.cs | 46 +++++++++---------- src/WingetCreateCLI/Properties/Resources.resx | 2 +- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 82eca4b6..8d3affe9 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -41,7 +41,7 @@ public override bool Set(JToken input) if (!data.Test()) { data.Output.Settings = data.GetResolvedInput(); - data.WriteOutput(); + data.Set(); } this.WriteJsonOutputLine(data.Output.ToJson()); diff --git a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs index 5f7f90a9..9d214234 100644 --- a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs +++ b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs @@ -46,9 +46,6 @@ public virtual JObject GetProperties() /// A Json object representing the current object. public JObject ToJson() { - return JObject.FromObject(this, new JsonSerializer - { - NullValueHandling = NullValueHandling.Ignore, - }); + return JObject.FromObject(this); } } diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs b/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs index eb11047b..8a75883d 100644 --- a/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs +++ b/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs @@ -58,7 +58,16 @@ public void Get() /// >true if the settings are equivalent; otherwise, false. public bool Test() { - return JToken.DeepEquals(this.GetResolvedInput(), this.GetValidSettings(this.Output.Settings)); + return JToken.DeepEquals(this.GetResolvedInput(), GetValidSettings(this.Output.Settings)); + } + + /// + /// Writes the current output settings to persistent storage. + /// + public void Set() + { + Debug.Assert(this.Output.Settings != null, "Output settings should not be null."); + UserSettings.SaveSettings(this.Output.Settings.ToObject()); } /// @@ -88,47 +97,38 @@ public JObject GetResolvedInput() if (SettingsResourceObject.ActionFull.Equals(this.Input.Action, StringComparison.OrdinalIgnoreCase)) { this.Output.Action = SettingsResourceObject.ActionFull; - this.resolvedInputUserSettings = this.GetValidSettings(this.Input.Settings); + this.resolvedInputUserSettings = GetValidSettings(this.Input.Settings); } else { this.Output.Action = SettingsResourceObject.ActionPartial; var mergedSettings = this.GetUserSettings(); mergedSettings.Merge(this.Input.Settings); - this.resolvedInputUserSettings = this.GetValidSettings(mergedSettings); + this.resolvedInputUserSettings = GetValidSettings(mergedSettings); } } return this.resolvedInputUserSettings; } - /// - /// Retrieves a deep-cloned JSON representation of the current user settings. - /// - /// A Json object representing the user settings. - public JObject GetUserSettings() - { - this.userSettings ??= UserSettings.ToJson(); - return (JObject)this.userSettings.DeepClone(); - } - - /// - /// Writes the current output settings to persistent storage. - /// - public void WriteOutput() - { - Debug.Assert(this.Output.Settings != null, "Output settings should not be null."); - UserSettings.SaveSettings(this.Output.Settings.ToObject()); - } - /// /// Validates and converts the provided settings into a structured format. /// /// An object containing settings to be validated. /// An object representing the validated settings. - public JObject GetValidSettings(JObject settings) + private static JObject GetValidSettings(JObject settings) { var settingsManifest = settings.ToObject(); return JObject.FromObject(settingsManifest); } + + /// + /// Retrieves a deep-cloned JSON representation of the current user settings. + /// + /// A Json object representing the user settings. + private JObject GetUserSettings() + { + this.userSettings ??= UserSettings.ToJson(); + return (JObject)this.userSettings.DeepClone(); + } } diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 5609dbbb..9f58f851 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1422,7 +1422,7 @@ Warning: Using this argument may result in the token being logged. Consider an a Get all state instances - Execute the Schema command + Outputs schema of the resource The name of the DSC resource to manage From 5d35f4133c5f4ff4351adc31361e61bd60f43803 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:10:59 -0700 Subject: [PATCH 23/24] Addressed comments --- src/WingetCreateCLI/Commands/DscCommand.cs | 7 ++-- .../Commands/DscCommands/BaseDscCommand.cs | 42 +++++++++++++++++-- .../DscCommands/DscSettingsCommand.cs | 9 ++-- .../WingetCreateTests/TestUtils.cs | 2 +- .../UnitTests/DscCommandTests.cs | 20 +++------ .../UnitTests/DscSettingsCommandTests.cs | 40 ++++++------------ 6 files changed, 64 insertions(+), 56 deletions(-) diff --git a/src/WingetCreateCLI/Commands/DscCommand.cs b/src/WingetCreateCLI/Commands/DscCommand.cs index 6ecb2f76..c703acd3 100644 --- a/src/WingetCreateCLI/Commands/DscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommand.cs @@ -5,6 +5,7 @@ namespace Microsoft.WingetCreateCLI.Commands; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using CommandLine; @@ -72,11 +73,9 @@ public override async Task Execute() { await Task.CompletedTask; - List dscCommands = [new DscSettingsCommand()]; - var dscCommand = dscCommands.FirstOrDefault(c => c.CommandName.Equals(this.ResourceName, StringComparison.OrdinalIgnoreCase)); - if (dscCommand == null) + if (!BaseDscCommand.TryCreateInstance(this.ResourceName, out var dscCommand)) { - var availableResources = string.Join(", ", dscCommands.Select(c => c.CommandName)); + var availableResources = string.Join(", ", BaseDscCommand.GetAvailableCommands()); Logger.ErrorLocalized(nameof(Resources.DscResourceNameNotFound_Message), this.ResourceName, availableResources); return false; } diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs index 89288706..5f25db86 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -4,6 +4,7 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; using System; +using System.Collections.Generic; using Microsoft.WingetCreateCLI.Models.DscModels; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -14,9 +15,41 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; public abstract class BaseDscCommand { /// - /// Gets the name of the command used to access the DSC functionality. + /// Tries to create an instance of a DSC command based on the command name. /// - public virtual string CommandName { get; } + /// The name of the command to create an instance for. + /// The created command instance if successful; otherwise, null. + /// True if the command instance was created successfully; otherwise, false. + public static bool TryCreateInstance(string commandName, out BaseDscCommand commandInstance) + { + var formattedCommandName = commandName?.ToLowerInvariant() ?? string.Empty; + switch (formattedCommandName) + { + case DscSettingsCommand.CommandName: + commandInstance = new DscSettingsCommand(); + return true; + + // Add more cases here for other DSC commands as needed. + + // Return false if no matching command is found. + default: + commandInstance = null; + return false; + } + } + + /// + /// Gets the list of available command names for DSC commands. + /// + /// The list of available command names. + public static List GetAvailableCommands() + { + return [ + DscSettingsCommand.CommandName, + + // Add more command names here as needed. + ]; + } /// /// DSC Get command. @@ -55,15 +88,16 @@ public abstract class BaseDscCommand /// /// Creates a Json schema for a DSC resource object. /// + /// The type of the resource object. /// A Json object representing the schema. - protected JObject CreateSchema() + protected JObject CreateSchema(string commandName) where T : BaseResourceObject, new() { var resourceObject = new T(); return new JObject { ["$schema"] = "http://json-schema.org/draft-07/schema#", - ["title"] = this.CommandName, + ["title"] = commandName, ["type"] = "object", ["properties"] = resourceObject.GetProperties(), ["required"] = resourceObject.GetRequiredProperties(), diff --git a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs index 8d3affe9..fd6538da 100644 --- a/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -3,7 +3,6 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; -using System; using Microsoft.WingetCreateCLI.Logging; using Microsoft.WingetCreateCLI.Models.DscModels; using Microsoft.WingetCreateCLI.Properties; @@ -14,8 +13,10 @@ namespace Microsoft.WingetCreateCLI.Commands.DscCommands; /// public class DscSettingsCommand : BaseDscCommand { - /// - public override string CommandName => "settings"; + /// + /// Represents the name of the settings command used to access the DSC functionality. + /// + public const string CommandName = "settings"; /// public override bool Get(JToken input) @@ -80,7 +81,7 @@ public override bool Export(JToken input) /// public override bool Schema() { - this.WriteJsonOutputLine(this.CreateSchema()); + this.WriteJsonOutputLine(this.CreateSchema(CommandName)); return true; } } diff --git a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs index 6099f3ba..5a97585b 100644 --- a/src/WingetCreateTests/WingetCreateTests/TestUtils.cs +++ b/src/WingetCreateTests/WingetCreateTests/TestUtils.cs @@ -222,7 +222,7 @@ public static void DeleteCachedFiles(List testFileNames) /// /// The arguments to pass to the DSC command. /// Result of executing the DSC command. - public static async Task ExecuteDscCommandAsync(List args) + public static async Task ExecuteDscCommandAsync(params string[] args) { var sw = new StringWriter(); Console.SetOut(sw); diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs index ffee0f15..46783c86 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -33,11 +33,8 @@ public void OneTimeSetUp() [Test] public async Task DscSettingsResource_Success() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get"]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--get"); // Assert Assert.That(result.Success, Is.True); @@ -52,11 +49,10 @@ public async Task DscResourceNotFound_ErrorMessage() { // Arrange var dscResourceName = "ResourceNotFound"; - List dscCommands = [new DscSettingsCommand()]; - var availableResources = string.Join(", ", dscCommands.Select(c => c.CommandName)); + var availableResources = string.Join(", ", BaseDscCommand.GetAvailableCommands()); // Act - var result = await TestUtils.ExecuteDscCommandAsync([dscResourceName]); + var result = await TestUtils.ExecuteDscCommandAsync(dscResourceName); // Assert Assert.That(result.Success, Is.False); @@ -70,11 +66,8 @@ public async Task DscResourceNotFound_ErrorMessage() [Test] public async Task DscResourceOperationNotSpecified_ErrorMessage() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName); // Assert Assert.That(result.Success, Is.False); @@ -88,11 +81,8 @@ public async Task DscResourceOperationNotSpecified_ErrorMessage() [Test] public async Task DscSettingsResourceFailedOperation_ErrorMessage() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", string.Empty); // Assert Assert.That(result.Success, Is.False); diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs index 0789dd6b..b9d170c8 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -74,11 +74,8 @@ public void TearDown() [Test] public async Task DscSettingsResource_Get_Success() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--get"]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--get"); var state = result.OutputState(); // Assert @@ -95,11 +92,8 @@ public async Task DscSettingsResource_Get_Success() [Test] public async Task DscSettingsResource_Export_Success() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--export"]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--export"); var state = result.OutputState(); // Assert @@ -116,11 +110,8 @@ public async Task DscSettingsResource_Export_Success() [Test] public async Task DscSettingsResource_SetEmpty_Fail() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", string.Empty); // Assert Assert.That(result.Success, Is.False); @@ -134,11 +125,8 @@ public async Task DscSettingsResource_SetEmpty_Fail() [Test] public async Task DscSettingsResource_TestEmpty_Fail() { - // Arrange - var command = new DscSettingsCommand(); - // Act - var result = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", string.Empty]); + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--test", string.Empty); // Assert Assert.That(result.Success, Is.False); @@ -158,12 +146,11 @@ public async Task DscSettingsResource_SetWithDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); - var command = new DscSettingsCommand(); // Part 1: Update settings repo name only { // Act - var setRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + var setRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)); var stateAndDiff = setRepoName.OutputStateAndDiff(); // Assert @@ -177,7 +164,7 @@ public async Task DscSettingsResource_SetWithDiff_Success(bool? isPartial) // Part 2: Now update settings repo owner only { // Act - var setRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var setRepoOwner = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)); var stateAndDiff = setRepoOwner.OutputStateAndDiff(); // Assert @@ -202,7 +189,6 @@ public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); - var command = new DscSettingsCommand(); // Part 1: Update settings repo name only { @@ -211,7 +197,7 @@ public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) var currentSettingsBeforeExecute = this.CurrentSettings; // Act - var setRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)]); + var setRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", this.CreateInput(name: MockName, isPartial: isPartial)); var stateAndDiff = setRepoName.OutputStateAndDiff(); // Assert @@ -230,7 +216,7 @@ public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) var currentSettingsBeforeExecute = this.CurrentSettings; // Act - var setRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var setRepoOwner = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", this.CreateInput(owner: MockOwner, isPartial: isPartial)); var stateAndDiff = setRepoOwner.OutputStateAndDiff(); // Assert @@ -255,12 +241,11 @@ public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); - var command = new DscSettingsCommand(); // Part 1: Test settings repo name only { // Act - var testRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)]); + var testRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)); var stateAndDiff = testRepoName.OutputStateAndDiff(); // Assert @@ -275,7 +260,7 @@ public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) // Part 2: Now test settings repo owner only { // Act - var testRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var testRepoOwner = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)); var stateAndDiff = testRepoOwner.OutputStateAndDiff(); // Assert @@ -301,7 +286,6 @@ public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) { // Arrange this.ResetSettingsToDefaultValues(); - var command = new DscSettingsCommand(); // Part 1: Test settings repo name only { @@ -309,7 +293,7 @@ public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) this.UpdateSettings(name: MockName); // Act - var testRepoName = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)]); + var testRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--test", this.CreateInput(name: MockName, isPartial: isPartial)); var stateAndDiff = testRepoName.OutputStateAndDiff(); // Assert @@ -327,7 +311,7 @@ public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) this.UpdateSettings(name: (isPartial ?? true) ? null : defaultRepoName, owner: MockOwner); // Act - var testRepoOwner = await TestUtils.ExecuteDscCommandAsync([command.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)]); + var testRepoOwner = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--test", this.CreateInput(owner: MockOwner, isPartial: isPartial)); var stateAndDiff = testRepoOwner.OutputStateAndDiff(); // Assert From f38aa7d8b39ebb2c69ffe7e4abc1eb2c59187b47 Mon Sep 17 00:00:00 2001 From: AmirMS <104940545+AmelBawa-msft@users.noreply.github.com> Date: Wed, 25 Jun 2025 15:16:55 -0700 Subject: [PATCH 24/24] Addressed comments --- src/WingetCreatePackage/Package.appxmanifest | 2 +- src/WingetCreatePackage/WingetCreatePackage.wapproj | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/WingetCreatePackage/Package.appxmanifest b/src/WingetCreatePackage/Package.appxmanifest index 218a3cd2..540a344b 100644 --- a/src/WingetCreatePackage/Package.appxmanifest +++ b/src/WingetCreatePackage/Package.appxmanifest @@ -11,7 +11,7 @@ + Version="0.0.1.0" /> Windows Package Manager Manifest Creator diff --git a/src/WingetCreatePackage/WingetCreatePackage.wapproj b/src/WingetCreatePackage/WingetCreatePackage.wapproj index e05e55ac..57a3028b 100644 --- a/src/WingetCreatePackage/WingetCreatePackage.wapproj +++ b/src/WingetCreatePackage/WingetCreatePackage.wapproj @@ -37,8 +37,6 @@ False True 0 - SHA256 - C:\win\tmp\wingetcreate-package\