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..4fd05c64 --- /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 diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs index 4fd1e832..42228493 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 accepts a GitHub token. + /// + 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 b30298ad..a66f6408 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 AcceptsGitHubToken => 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..c703acd3 --- /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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using CommandLine; +using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Properties; +using Newtonsoft.Json.Linq; + +/// +/// Command for managing the application using dsc v3. +/// +[Verb("dsc", HelpText = "DscCommand_HelpText", ResourceType = typeof(Resources))] +public class DscCommand : BaseCommand +{ + /// + public override bool AcceptsGitHubToken => false; + + /// + /// Gets or sets the name of the resource to be managed by the dsc command. + /// + [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 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 bool Get { get; set; } + + /// + /// 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 bool Set { get; set; } + + /// + /// 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 bool Test { get; set; } + + /// + /// 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 bool Export { get; set; } + + /// + /// Gets or sets a value indicating whether to execute the schema command. + /// + [Option("schema", SetName = "SchemaMethod", HelpText = "DscSchema_HelpText", ResourceType = typeof(Resources))] + public bool Schema { get; set; } + + /// + /// Executes the dsc command flow. + /// + /// Boolean representing success or fail of the command. + public override async Task Execute() + { + await Task.CompletedTask; + + if (!BaseDscCommand.TryCreateInstance(this.ResourceName, out var dscCommand)) + { + var availableResources = string.Join(", ", BaseDscCommand.GetAvailableCommands()); + Logger.ErrorLocalized(nameof(Resources.DscResourceNameNotFound_Message), this.ResourceName, availableResources); + return false; + } + + try + { + var input = string.IsNullOrWhiteSpace(this.Input) ? null : JToken.Parse(this.Input); + var operations = new (bool, Func)[] + { + (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; + } + } + + Logger.ErrorLocalized(nameof(Resources.DscResourceOperationNotSpecified_Message)); + return false; + } + catch (Exception ex) + { + Logger.Error(ex.Message); + return false; + } + } +} diff --git a/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs new file mode 100644 index 00000000..5f25db86 --- /dev/null +++ b/src/WingetCreateCLI/Commands/DscCommands/BaseDscCommand.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Commands.DscCommands; + +using System; +using System.Collections.Generic; +using Microsoft.WingetCreateCLI.Models.DscModels; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +/// +/// Base class for DSC commands. +/// +public abstract class BaseDscCommand +{ + /// + /// Tries to create an instance of a DSC command based on the command name. + /// + /// 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. + /// + /// Input for the Get command. + /// True if the command was successful; otherwise, false. + public abstract bool Get(JToken input); + + /// + /// DSC Set command. + /// + /// Input for the Set command. + /// True if the command was successful; otherwise, false. + public abstract bool Set(JToken input); + + /// + /// DSC Test command. + /// + /// Input for the Test command. + /// True if the command was successful; otherwise, false. + public abstract bool Test(JToken input); + + /// + /// DSC Export command. + /// + /// Input for the Export command. + /// True if the command was successful; otherwise, false. + public abstract bool Export(JToken input); + + /// + /// DSC Schema command. + /// + /// True if the command was successful; otherwise, false. + public abstract bool Schema(); + + /// + /// Creates a Json schema for a DSC resource object. + /// + /// The type of the resource object. + /// A Json object representing the schema. + 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"] = commandName, + ["type"] = "object", + ["properties"] = resourceObject.GetProperties(), + ["required"] = resourceObject.GetRequiredProperties(), + ["additionalProperties"] = false, + }; + } + + /// + /// 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 new file mode 100644 index 00000000..fd6538da --- /dev/null +++ b/src/WingetCreateCLI/Commands/DscCommands/DscSettingsCommand.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Commands.DscCommands; + +using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Models.DscModels; +using Microsoft.WingetCreateCLI.Properties; +using Newtonsoft.Json.Linq; + +/// +/// Command for managing the settings using dsc v3. +/// +public class DscSettingsCommand : BaseDscCommand +{ + /// + /// Represents the name of the settings command used to access the DSC functionality. + /// + public const string CommandName = "settings"; + + /// + public override bool Get(JToken input) + { + return this.Export(input); + } + + /// + public override bool Set(JToken input) + { + if (input == null) + { + Logger.ErrorLocalized(nameof(Resources.DscInputRequired_Message), nameof(this.Set)); + return false; + } + + var data = new SettingsFunctionData(input); + data.Get(); + + // Capture the diff before updating the output + var diff = data.DiffJson(); + + if (!data.Test()) + { + data.Output.Settings = data.GetResolvedInput(); + data.Set(); + } + + this.WriteJsonOutputLine(data.Output.ToJson()); + this.WriteJsonOutputLine(diff); + return true; + } + + /// + public override bool Test(JToken input) + { + if (input == null) + { + Logger.ErrorLocalized(nameof(Resources.DscInputRequired_Message), nameof(this.Test)); + return false; + } + + var data = new SettingsFunctionData(input); + + data.Get(); + data.Output.InDesiredState = data.Test(); + + this.WriteJsonOutputLine(data.Output.ToJson()); + this.WriteJsonOutputLine(data.DiffJson()); + return true; + } + + /// + public override bool Export(JToken input) + { + var data = new SettingsFunctionData(); + data.Get(); + this.WriteJsonOutputLine(data.Output.ToJson()); + return true; + } + + /// + public override bool Schema() + { + this.WriteJsonOutputLine(this.CreateSchema(CommandName)); + return true; + } +} diff --git a/src/WingetCreateCLI/Commands/InfoCommand.cs b/src/WingetCreateCLI/Commands/InfoCommand.cs index b4441e72..0f9a9c13 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 AcceptsGitHubToken => false; + /// /// Executes the info command flow. /// diff --git a/src/WingetCreateCLI/Commands/SettingsCommand.cs b/src/WingetCreateCLI/Commands/SettingsCommand.cs index 821de4aa..c3465143 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 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 new file mode 100644 index 00000000..f1ee9671 --- /dev/null +++ b/src/WingetCreateCLI/DscResources/microsoft.winget-create.settings.dsc.resource.json @@ -0,0 +1,62 @@ +{ + "$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.", + "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", + "args": [ + "dsc", + "settings", + { + "jsonInputArg": "--set", + "mandatory": true + } + ] + }, + "test": { + "executable": "wingetcreate", + "return": "stateAndDiff", + "args": [ + "dsc", + "settings", + { + "jsonInputArg": "--test", + "mandatory": true + } + ] + }, + "tags": [ + "WinGetCreate" + ], + "type": "Microsoft.WinGetCreate/Settings", + "version": "1.10.0" +} \ No newline at end of file diff --git a/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs new file mode 100644 index 00000000..9d214234 --- /dev/null +++ b/src/WingetCreateCLI/Models/DscModels/BaseResourceObject.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Models.DscModels; + +using Microsoft.WingetCreateCLI.Properties; +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 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"] = Resources.DscResourcePropertyDescriptionInDesiredState, + ["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); + } +} diff --git a/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs b/src/WingetCreateCLI/Models/DscModels/SettingsFunctionData.cs new file mode 100644 index 00000000..8a75883d --- /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(), 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()); + } + + /// + /// 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 = GetValidSettings(this.Input.Settings); + } + else + { + this.Output.Action = SettingsResourceObject.ActionPartial; + var mergedSettings = this.GetUserSettings(); + mergedSettings.Merge(this.Input.Settings); + this.resolvedInputUserSettings = GetValidSettings(mergedSettings); + } + } + + return this.resolvedInputUserSettings; + } + + /// + /// Validates and converts the provided settings into a structured format. + /// + /// An object containing settings to be validated. + /// An object representing the validated 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/Models/DscModels/SettingsResourceObject.cs b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs new file mode 100644 index 00000000..47b4407d --- /dev/null +++ b/src/WingetCreateCLI/Models/DscModels/SettingsResourceObject.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI.Models.DscModels; + +using Microsoft.WingetCreateCLI.Properties; +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; } + + /// + public override JObject GetProperties() + { + var baseProperties = base.GetProperties(); + baseProperties["settings"] = new JObject + { + ["description"] = Resources.DscResourcePropertyDescriptionSettings, + ["type"] = "object", + }; + baseProperties["action"] = new JObject + { + ["default"] = ActionPartial, + ["description"] = Resources.DscResourcePropertyDescriptionAction, + ["type"] = "string", + ["enum"] = new JArray(ActionFull, ActionPartial), + }; + return baseProperties; + } + + /// + public override JArray GetRequiredProperties() + { + return ["settings"]; + } +} diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs index 9cb7e5d5..16351c4f 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.AcceptsGitHubToken) { if (await command.LoadGitHubClient()) { diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 499f6f91..5d40723a 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -753,6 +753,141 @@ 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 Get all state instances. + /// + 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 The input for the DSC resource. + /// + public static string DscInput_HelpText { + get { + return ResourceManager.GetString("DscInput_HelpText", resourceCulture); + } + } + + /// + /// 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 DscResourceNameNotFound_Message { + get { + return ResourceManager.GetString("DscResourceNameNotFound_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 No operation specified. Use --help to see available operations.. + /// + public static string DscResourceOperationNotSpecified_Message { + get { + return ResourceManager.GetString("DscResourceOperationNotSpecified_Message", resourceCulture); + } + } + + /// + /// 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. + /// + 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 ee626cb7..9f58f851 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -1406,6 +1406,53 @@ 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 + + + Get all state instances + + + Outputs schema of the resource + + + The name of the DSC resource to manage + + + The input for the DSC resource + + + 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 + + + No operation specified. Use --help to see available operations. + + + DSC resource operation failed + + + Indicates whether an instance is in the desired state + + + The settings content + + + The action used to apply the settings + The type of authentication to use diff --git a/src/WingetCreateCLI/UserSettings.cs b/src/WingetCreateCLI/UserSettings.cs index 0ff37970..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. @@ -230,6 +248,6 @@ private static void LoadSettings() Visual = new Visual(), }; } - } + } } } diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index d52213c5..6536b6bb 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,12 +96,20 @@ - - - - - - + + + + + + + + + %(Filename)%(Extension) +    Always +    Always + diff --git a/src/WingetCreatePackage/WingetCreatePackage.wapproj b/src/WingetCreatePackage/WingetCreatePackage.wapproj index 0a2b4410..57a3028b 100644 --- a/src/WingetCreatePackage/WingetCreatePackage.wapproj +++ b/src/WingetCreatePackage/WingetCreatePackage.wapproj @@ -1,70 +1,70 @@ - - - - 15.0 - - - - Debug - x86 - - - Release - x86 - - - Debug - x64 - - - Release - x64 - - - - $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ - - - - ac37dfd2-1332-4282-b373-8dcf8bb4e3ba - 10.0.22000.0 - 10.0.17763.0 - en-US - ..\WingetCreateCLI\WingetCreateCLI.csproj - True - x86|x64 - Always - False - True - 0 - - - - Designer - - - - - True - Properties\PublishProfiles\x64ReleasePublishProfile.pubxml - Properties\PublishProfiles\x86ReleasePublishProfile.pubxml - - - - - - - - - - - - - - - - - - - + + + + 15.0 + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + + + + ac37dfd2-1332-4282-b373-8dcf8bb4e3ba + 10.0.22000.0 + 10.0.17763.0 + en-US + ..\WingetCreateCLI\WingetCreateCLI.csproj + True + x86|x64 + Always + False + True + 0 + + + + Designer + + + + + True + Properties\PublishProfiles\x64ReleasePublishProfile.pubxml + Properties\PublishProfiles\x86ReleasePublishProfile.pubxml + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..5a97585b 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(params string[] 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 new file mode 100644 index 00000000..46783c86 --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscCommandTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateUnitTests; + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.WingetCreateCLI.Commands.DscCommands; +using Microsoft.WingetCreateCLI.Logging; +using Microsoft.WingetCreateCLI.Properties; +using Microsoft.WingetCreateTests; +using NUnit.Framework; + +/// +/// Unit test class for the DSC Command. +/// +public class DscCommandTests +{ + /// + /// OneTimeSetup method for the DSC command unit tests. + /// + [OneTimeSetUp] + public void OneTimeSetUp() + { + Logger.Initialize(); + } + + /// + /// Tests the a successful execution of the dsc command. + /// + /// Async Task. + [Test] + public async Task DscSettingsResource_Success() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--get"); + + // Assert + Assert.That(result.Success, Is.True); + } + + /// + /// Tests the error message when a DSC resource is not found. + /// + /// Async Task. + [Test] + public async Task DscResourceNotFound_ErrorMessage() + { + // Arrange + var dscResourceName = "ResourceNotFound"; + var availableResources = string.Join(", ", BaseDscCommand.GetAvailableCommands()); + + // Act + var result = await TestUtils.ExecuteDscCommandAsync(dscResourceName); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(string.Format(Resources.DscResourceNameNotFound_Message, dscResourceName, availableResources))); + } + + /// + /// Tests the error message when a DSC resource operation is not specified. + /// + /// Async Task. + [Test] + public async Task DscResourceOperationNotSpecified_ErrorMessage() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationNotSpecified_Message)); + } + + /// + /// Tests the error message when a DSC resource operation fails. + /// + /// Async Task. + [Test] + public async Task DscSettingsResourceFailedOperation_ErrorMessage() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", string.Empty); + + // Assert + Assert.That(result.Success, Is.False); + Assert.That(result.Output, Does.Contain(Resources.DscResourceOperationFailed_Message)); + } +} diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs new file mode 100644 index 00000000..b9d170c8 --- /dev/null +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/DscSettingsCommandTests.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateUnitTests; + +using System.Collections.Generic; +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 Microsoft.WingetCreateTests; +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); + } + + /// + /// Tests the Get operation. + /// + /// Async task. + [Test] + public async Task DscSettingsResource_Get_Success() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--get"); + 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() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--export"); + 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() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.CommandName, "--set", string.Empty); + + // Assert + Assert.That(result.Success, Is.False); + 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() + { + // Act + var result = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_SetWithDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + + // Part 1: Update settings repo name only + { + // Act + var setRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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(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 TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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(this.CurrentSettings, stateAndDiff.State); + Assert.That(stateAndDiff.Diff, Is.EqualTo(new List() { "settings" })); + this.AssertStateAction(stateAndDiff.State, isPartial); + } + } + + /// + /// Tests the Set operation without diff. + /// + /// Optional parameter to indicate if the operation is partial. + /// Async task. + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_SetWithoutDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + + // Part 1: Update settings repo name only + { + // Arrange + this.UpdateSettings(name: MockName); + var currentSettingsBeforeExecute = this.CurrentSettings; + + // Act + var setRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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(DscSettingsCommand.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. + /// + /// Optional parameter to indicate if the operation is partial. + /// Async task. + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_TestWithDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + + // Part 1: Test settings repo name only + { + // Act + var testRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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); + Assert.That(stateAndDiff.State.InDesiredState, Is.False); + } + + // Part 2: Now test settings repo owner only + { + // Act + var testRepoOwner = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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); + Assert.That(stateAndDiff.State.InDesiredState, Is.False); + } + } + + /// + /// Tests the Test operation without diff. + /// + /// Optional parameter to indicate if the operation is partial. + /// Async task. + [Test] + [TestCase(true)] + [TestCase(false)] + [TestCase(null)] + public async Task DscSettingsResource_TestWithoutDiff_Success(bool? isPartial) + { + // Arrange + this.ResetSettingsToDefaultValues(); + + // Part 1: Test settings repo name only + { + // Arrange + this.UpdateSettings(name: MockName); + + // Act + var testRepoName = await TestUtils.ExecuteDscCommandAsync(DscSettingsCommand.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); + Assert.That(stateAndDiff.State.InDesiredState, Is.True); + } + + // 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(DscSettingsCommand.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); + Assert.That(stateAndDiff.State.InDesiredState, Is.True); + } + } + + /// + /// 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(); + if (name != null) + { + repo["name"] = name; + } + + if (owner != null) + { + repo["owner"] = owner; + } + + var input = new JObject + { + [nameof(WindowsPackageManagerRepository)] = repo, + }; + + 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 + { + Settings = settings, + Action = isPartial.HasValue ? (isPartial.Value ? SettingsResourceObject.ActionPartial : SettingsResourceObject.ActionFull) : null, + }; + return JObject.FromObject(resourceObject).ToString(Formatting.None); + } + + /// + /// 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; + if (name != null) + { + defaultSettings.WindowsPackageManagerRepository.Name = name; + } + + if (owner != null) + { + defaultSettings.WindowsPackageManagerRepository.Owner = owner; + } + + 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); + } +}