commit e66cbc901d87fee5386bc6b75ea2a5c2f0a697bf parent f7be7b70aa9afa3c53dc72684602db34cdfa15b9 Author: Ruben Guerrero <rubengu@microsoft.com> Date: Tue, 28 Feb 2023 18:00:35 -0800 Configuration processors (#3008) Microsoft.Management.Configuration.Process is a cswinrt component that provides implementation of `IConfigurationSetProcessor`, `IConfigurationUnitProcessor`, `IConfigurationUnitProcessorDetails`, `IConfigurationUnitSettingDetails` and `IConfigurationProcessorFactory` defined in #2942 as well as new runtime classes and an interface. `ConfigurationSetProcessorFactory` implements `IConfigurationProcessorFactory`, which takes the type of configuration and optional `IConfigurationProcessorFactoryProperties` with additional properties that need to be defined for the processor set. An implementation of `IConfigurationProcessorFactoryProperties` is also provided. Internally, it uses PowerShell Host to find and install DSC modules and apply them via InvokeDscResource. Requires [PSDesiredStateConfiguration 2.0.6](https://www.powershellgallery.com/packages/PSDesiredStateConfiguration/2.0.6) Diffstat:
60 files changed, 6838 insertions(+), 15 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -27,6 +27,7 @@ Atest ATL AType AUrl +Authenticode azurewebsites bcp BEFACEF @@ -41,6 +42,7 @@ blogs bluetooth bomgar BOMs +boop boundparms Browsable BSODs @@ -73,6 +75,7 @@ count'th countof countryregion createmanifestmetadata +cswinrt ctc DACL Dbg @@ -85,15 +88,15 @@ dllimport DMPAs dnld Dobbeleer -Dsc +dsc DUPLICATEALIAS dustojnikhummer dvinns ecfr ecfrbrowse EFGH -EQU endregion +EQU errmsg ESRB etest @@ -125,6 +128,7 @@ gcpi GESMBH GHS gity +goku Google guiddef Hackathon @@ -171,6 +175,7 @@ jrsoftware jsoncpp JToken JValue +Kaido KNOWNFOLDERID ktf ldcase @@ -189,7 +194,8 @@ LPWCH LPWSTR LSTATUS LTDA -maclachlan +luffy +Luffytaro malware maxvalue maybenull @@ -202,8 +208,8 @@ minidump minschema missingdependency MMmmbbbb +mof monicka -MOF MPNS msdownload msft @@ -211,6 +217,7 @@ msftrubengu MSIHASH MSIXHASH msstore +Mugiwara Multideclaration mycustom myinstalldir @@ -228,6 +235,7 @@ NONFOLDERS nonterminated normer NOSEPARATOR +notalostreference NOTAPROPERTY notmatch npmjs @@ -294,7 +302,6 @@ roblox ronomon rosoft rowids -roy RRF rrr runspace @@ -353,6 +360,7 @@ TResult trimstart TState TStatus +TType typeparam UCase ucasemap @@ -377,7 +385,6 @@ UWP VALUENAMECASE VERSI VERSIE -visualstudiocode vns vsconfig webpages @@ -402,9 +409,13 @@ wwinmain WZDNCRFJ xcopy XFile +XMUGIWARAMODULE XPLATSTR +XRESOURCEZORO xsi +yamato yamlcreateps yao Zanollo ZIPHASH +zoro diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -155,6 +155,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Management.Config EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Management.Configuration.UnitTests", "Microsoft.Management.Configuration.UnitTests\Microsoft.Management.Configuration.UnitTests.csproj", "{EE43C990-7789-4A60-B077-BF0ED3D093A1}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Management.Configuration.Processor", "Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj", "{71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -786,8 +788,8 @@ Global {E8454BF1-2068-4513-A525-ABF55CC8742C}.Fuzzing|x86.Build.0 = Debug|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|ARM64.ActiveCfg = Release|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|ARM64.Build.0 = Release|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x64.ActiveCfg = Release|Any CPU - {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x64.Build.0 = Release|Any CPU + {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x64.ActiveCfg = Release|x64 + {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x64.Build.0 = Release|x64 {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x86.ActiveCfg = Release|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.Release|x86.Build.0 = Release|Any CPU {E8454BF1-2068-4513-A525-ABF55CC8742C}.TestRelease|ARM64.ActiveCfg = Release|Any CPU @@ -820,6 +822,30 @@ Global {EE43C990-7789-4A60-B077-BF0ED3D093A1}.TestRelease|x64.Build.0 = Release|x64 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.TestRelease|x86.ActiveCfg = Release|x86 {EE43C990-7789-4A60-B077-BF0ED3D093A1}.TestRelease|x86.Build.0 = Release|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|ARM64.Build.0 = Debug|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x64.ActiveCfg = Debug|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x64.Build.0 = Debug|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x86.ActiveCfg = Debug|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Debug|x86.Build.0 = Debug|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|ARM64.ActiveCfg = Debug|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|ARM64.Build.0 = Debug|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x64.ActiveCfg = Debug|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x64.Build.0 = Debug|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x86.ActiveCfg = Debug|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Fuzzing|x86.Build.0 = Debug|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|ARM64.ActiveCfg = Release|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|ARM64.Build.0 = Release|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|x64.ActiveCfg = Release|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|x64.Build.0 = Release|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|x86.ActiveCfg = Release|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.Release|x86.Build.0 = Release|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|ARM64.ActiveCfg = Release|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|ARM64.Build.0 = Release|ARM64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|x64.ActiveCfg = Release|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|x64.Build.0 = Release|x64 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|x86.ActiveCfg = Release|x86 + {71FA29AA-9035-468B-A11D-0F0B0F5D5AF4}.TestRelease|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -164,6 +164,14 @@ #define WINGET_CONFIG_ERROR_ASSERTION_FAILED ((HRESULT)0x8A15C009) #define WINGET_CONFIG_ERROR_MANUALLY_SKIPPED ((HRESULT)0x8A15C00A) +// Configuration Processor Errors +#define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) +#define WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY ((HRESULT)0x8A15C102) +#define WINGET_CONFIG_ERROR_UNIT_MULTIPLE_MATCHES ((HRESULT)0x8A15C103) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_GET ((HRESULT)0x8A15C104) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_TEST ((HRESULT)0x8A15C105) +#define WINGET_CONFIG_ERROR_UNIT_INVOKE_SET ((HRESULT)0x8A15C106) + namespace AppInstaller { diff --git a/src/Microsoft.Management.Configuration.Processor/Constants/DirectiveConstants.cs b/src/Microsoft.Management.Configuration.Processor/Constants/DirectiveConstants.cs @@ -0,0 +1,23 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DirectiveConstants.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Constants +{ + /// <summary> + /// Directives. + /// </summary> + internal static class DirectiveConstants + { +#pragma warning disable SA1600 // ElementsMustBeDocumented + public const string MaxVersion = "maxVersion"; + public const string MinVersion = "minVersion"; + public const string Module = "module"; + public const string ModuleGuid = "moduleGuid"; + public const string Repository = "repository"; + public const string Version = "version"; +#pragma warning restore SA1600 // ElementsMustBeDocumented + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs b/src/Microsoft.Management.Configuration.Processor/Constants/PowerShellConstants.cs @@ -0,0 +1,75 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellConstants.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Constants +{ + /// <summary> + /// Constants related to PowerShell. + /// </summary> + internal static class PowerShellConstants + { +#pragma warning disable SA1600 // ElementsMustBeDocumented + public const string Core = "Core"; + + internal static class Variables + { + public const string PSEdition = "PSEdition"; + public const string PSModulePath = "env:PSModulePath"; + } + + internal static class Modules + { + public const string PSDesiredStateConfiguration = "PSDesiredStateConfiguration"; + public const string PSDesiredStateConfigurationMinVersion = "2.0.6"; + public const string PowerShellGet = "PowerShellGet"; + public const string PowerShellGetMinVersion = "2.2.5"; + } + + internal static class Commands + { + public const string FindDscResource = "Find-DscResource"; + public const string GetAuthenticodeSignature = "Get-AuthenticodeSignature"; + public const string GetChildItem = "Get-ChildItem"; + public const string GetCommand = "Get-Command"; + public const string GetDscResource = "Get-DscResource"; + public const string GetInstalledModule = "Get-InstalledModule"; + public const string GetModule = "Get-Module"; + public const string ImportModule = "Import-Module"; + public const string InstallModule = "Install-Module"; + public const string InvokeDscResource = "Invoke-DscResource"; + public const string SaveModule = "Save-Module"; + } + + internal static class Parameters + { + public const string Force = "Force"; + public const string FullyQualifiedName = "FullyQualifiedName"; + public const string Guid = "GUID"; + public const string InputObject = "InputObject"; + public const string ListAvailable = "ListAvailable"; + public const string MaximumVersion = "MaximumVersion"; + public const string Method = "Method"; + public const string MinimumVersion = "MinimumVersion"; + public const string Module = "Module"; + public const string ModuleName = "ModuleName"; + public const string ModuleVersion = "ModuleVersion"; + public const string Name = "Name"; + public const string Path = "Path"; + public const string Property = "Property"; + public const string Recurse = "Recurse"; + public const string Repository = "Repository"; + public const string RequiredVersion = "RequiredVersion"; + } + + internal static class DscMethods + { + public const string Get = "Get"; + public const string Set = "Set"; + public const string Test = "Test"; + } +#pragma warning restore SA1600 // ElementsMustBeDocumented + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DscModules/DscModuleV2.cs b/src/Microsoft.Management.Configuration.Processor/DscModules/DscModuleV2.cs @@ -0,0 +1,250 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscModuleV2.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DscModule +{ + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// PSDesiredStateConfiguration v2.0.6. + /// </summary> + internal class DscModuleV2 : IDscModule + { + private const string InDesiredState = "InDesiredState"; + private const string RebootRequired = "RebootRequired"; + + private static readonly IEnumerable<string> ExclusionResourcesParentPath = new string[] + { + @"C:\WINDOWS\system32\WindowsPowershell\v1.0\Modules\PsDesiredStateConfiguration\DscResources", + @"C:\Program Files\WindowsPowerShell\Modules\PackageManagement\1.0.0.1", + }; + + /// <summary> + /// Initializes a new instance of the <see cref="DscModuleV2"/> class. + /// </summary> + public DscModuleV2() + { + this.ModuleSpecification = PowerShellHelpers.CreateModuleSpecification( + Modules.PSDesiredStateConfiguration, + minVersion: Modules.PSDesiredStateConfigurationMinVersion); + } + + /// <inheritdoc/> + public ModuleSpecification ModuleSpecification { get; private init; } + + /// <inheritdoc/> + public string GetDscResourceCmd { get; } = Commands.GetDscResource; + + /// <inheritdoc/> + public string InvokeDscResourceCmd { get; } = Commands.InvokeDscResource; + + /// <inheritdoc/> + public IReadOnlyList<DscResourceInfoInternal> GetAllDscResources(Runspace runspace) + { + var result = new List<DscResourceInfoInternal>(); + + using PowerShell pwsh = PowerShell.Create(runspace); + var resources = pwsh.AddCommand(this.GetDscResourceCmd) + .InvokeAndStopOnError(); + + return this.ConvertToDscResourceInfoInternal(resources); + } + + /// <inheritdoc/> + public IReadOnlyList<DscResourceInfoInternal> GetDscResourcesInModule( + Runspace runspace, + ModuleSpecification moduleSpecification) + { + var result = new List<DscResourceInfoInternal>(); + + using PowerShell pwsh = PowerShell.Create(runspace); + + var resources = pwsh.AddCommand(this.GetDscResourceCmd) + .AddParameter(Parameters.Module, moduleSpecification) + .InvokeAndStopOnError(); + + return this.ConvertToDscResourceInfoInternal(resources); + } + + /// <inheritdoc/> + public DscResourceInfoInternal? GetDscResource( + Runspace runspace, + string name, + ModuleSpecification? moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(runspace); + pwsh.AddCommand(this.GetDscResourceCmd) + .AddParameter(Parameters.Name, name); + + if (moduleSpecification is not null) + { + pwsh.AddParameter(Parameters.Module, moduleSpecification); + } + + var resources = pwsh.InvokeAndStopOnError(); + + var dscResourceInfos = this.ConvertToDscResourceInfoInternal(resources); + + if (dscResourceInfos.Count == 0) + { + return null; + } + else if (dscResourceInfos.Count > 1) + { + throw new GetDscResourceMultipleMatches(name, moduleSpecification); + } + + return dscResourceInfos[0]; + } + + /// <inheritdoc/> + public ValueSet InvokeGetResource( + Runspace runspace, + ValueSet settings, + string name, + ModuleSpecification? moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(runspace); + + var getResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) + .AddParameter(Parameters.Method, DscMethods.Get) + .InvokeAndStopOnError() + .FirstOrDefault(); + + if (getResult is null) + { + throw new InvokeDscResourceGetException(name, moduleSpecification); + } + + // Script based resource. + if (getResult.BaseObject is Hashtable) + { + Hashtable hashTable = (Hashtable)getResult.BaseObject; + var resultSettings = new ValueSet(); + foreach (DictionaryEntry entry in hashTable) + { + resultSettings.Add(entry.Key as string, entry.Value); + } + + return resultSettings; + } + + // Class-based resource. + return TypeHelpers.GetAllPropertiesValues(getResult.BaseObject); + } + + /// <inheritdoc/> + public bool InvokeTestResource( + Runspace runspace, + ValueSet settings, + string name, + ModuleSpecification? moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(runspace); + + // Returned type is InvokeDscResourceTestResult which is a PowerShell classed defined + // in PSDesiredStateConfiguration.psm1. + dynamic? testResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) + .AddParameter(Parameters.Method, DscMethods.Test) + .InvokeAndStopOnError() + .FirstOrDefault(); + + if (testResult is null || + !TypeHelpers.PropertyWithTypeExists<bool>(testResult, InDesiredState)) + { + throw new InvokeDscResourceTestException(name, moduleSpecification); + } + + return testResult?.InDesiredState; + } + + /// <inheritdoc/> + public bool InvokeSetResource( + Runspace runspace, + ValueSet settings, + string name, + ModuleSpecification? moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(runspace); + + // Returned type is InvokeDscResourceSetResult which is a PowerShell classed defined + // in PSDesiredStateConfiguration.psm1. + dynamic? setResult = pwsh.AddCommand(this.InvokeDscResourceCmd) + .AddParameters(PrepareInvokeParameters(name, settings, moduleSpecification)) + .AddParameter(Parameters.Method, DscMethods.Set) + .InvokeAndStopOnError() + .FirstOrDefault(); + + if (setResult is null || + !TypeHelpers.PropertyWithTypeExists<bool>(setResult, RebootRequired)) + { + throw new InvokeDscResourceSetException(name, moduleSpecification); + } + + return setResult?.RebootRequired; + } + + private static Dictionary<string, object> PrepareInvokeParameters( + string name, + ValueSet settings, + ModuleSpecification? moduleSpecification) + { + var properties = new Hashtable(); + foreach (var setting in settings) + { + properties.Add(setting.Key, setting.Value); + } + + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, name }, + { Parameters.Property, properties }, + }; + + if (moduleSpecification is not null) + { + parameters.Add(Parameters.ModuleName, moduleSpecification); + } + + return parameters; + } + + private List<DscResourceInfoInternal> ConvertToDscResourceInfoInternal(Collection<PSObject> psObjects) + { + var result = new List<DscResourceInfoInternal>(); + foreach (dynamic psObject in psObjects) + { + var dscResourceInfo = new DscResourceInfoInternal(psObject); + + // Explicitly don't support old DSC resources from v1 PSDesiredStateConfiguration. + // Even if the Windows System32 Windows PowerShell module path is removed they + // will show up. + if (ExclusionResourcesParentPath.Any(e => dscResourceInfo.ParentPath!.StartsWith(e))) + { + continue; + } + + result.Add(dscResourceInfo); + } + + return result; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DscModules/IDscModule.cs b/src/Microsoft.Management.Configuration.Processor/DscModules/IDscModule.cs @@ -0,0 +1,89 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IDscModule.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DscModule +{ + using System.Collections.Generic; + using System.Management.Automation.Runspaces; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + + /// <summary> + /// Interface for defining a PSDesiredStateConfiguration module. + /// </summary> + internal interface IDscModule + { + /// <summary> + /// Gets the module specification. + /// </summary> + ModuleSpecification ModuleSpecification { get; } + + /// <summary> + /// Gets the name of the Get-DscResource Cmdlet. + /// </summary> + string GetDscResourceCmd { get; } + + /// <summary> + /// Gets the name of the Invoke-DscResource Cmdlet. + /// </summary> + string InvokeDscResourceCmd { get; } + + /// <summary> + /// Gets all DSC resource. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <returns>A list with the DSC resource.</returns> + IReadOnlyList<DscResourceInfoInternal> GetAllDscResources(Runspace runspace); + + /// <summary> + /// Gets all resources in a module. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>List of resources of that module and version.</returns> + IReadOnlyList<DscResourceInfoInternal> GetDscResourcesInModule(Runspace runspace, ModuleSpecification moduleSpecification); + + /// <summary> + /// Gets a DSC Resource. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>DSC Resource from that module and version.</returns> + DscResourceInfoInternal? GetDscResource(Runspace runspace, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Invoke-DscResource -Method Get from this module. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>Properties of resource.</returns> + ValueSet InvokeGetResource(Runspace runspace, ValueSet settings, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Invoke-DscResource -Method Test from this module. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>Is in desired state.</returns> + bool InvokeTestResource(Runspace runspace, ValueSet settings, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Invoke-DscResource -Method Set from this module. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>If a reboot is required.</returns> + bool InvokeSetResource(Runspace runspace, ValueSet settings, string name, ModuleSpecification? moduleSpecification); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/DscResourceInfoInternal.cs b/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/DscResourceInfoInternal.cs @@ -0,0 +1,157 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscResourceInfoInternal.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DscResourcesInfo +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor.Helpers; + + /// <summary> + /// Contains a DSC resource information. + /// This object should be initialized with a Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo. + /// That object is the result of Get-DscResource and is not the same as System.Management.Automation.DscResourceInfo. + /// The type is defined in a psd1 in the PSDesiredStateConfiguration. This file is based on that. + /// If Invoke-DscResource gets support for passing the DscResourceInfo, we could keep the original object as member + /// to then pass it in code. + /// </summary> + internal class DscResourceInfoInternal + { + private const string DscResourceInfoFullName = "Microsoft.PowerShell.DesiredStateConfiguration.DscResourceInfo"; + + /// <summary> + /// Initializes a new instance of the <see cref="DscResourceInfoInternal"/> class. + /// </summary> + /// <param name="info">Dynamic object. Expected DscResourceInfo.</param> + public DscResourceInfoInternal(dynamic info) + { + if (info.GetType().FullName != DscResourceInfoFullName) + { + throw new ArgumentException(); + } + + this.ResourceType = info.ResourceType; + this.Name = info.Name; + this.FriendlyName = info.FriendlyName; + this.Module = info.Module; + this.Path = info.Path; + this.ParentPath = info.ParentPath; + this.ImplementedAs = Enum.Parse<ImplementedAsTypeInternal>(info.ImplementedAs.ToString()); + this.CompanyName = info.CompanyName; + + if (info.Module is not null) + { + this.ModuleName = this.Module.Name; + this.Version = this.Module.Version; + } + + foreach (object property in info.Properties) + { + this.Properties.Add(new DscResourcePropertyInfoInternal(property)); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="DscResourceInfoInternal"/> class. + /// Used for unit tests. + /// </summary> + /// <param name="name">Resource name.</param> + /// <param name="moduleName">Module name.</param> + /// <param name="version">Version.</param> + internal DscResourceInfoInternal(string name, string? moduleName, Version? version) + { + this.Name = name; + this.ModuleName = moduleName; + this.Version = version; + } + + /// <summary> + /// Gets or sets resource type name. + /// </summary> + public string? ResourceType { get; set; } + + /// <summary> + /// Gets or sets Name of the resource. This name is used to access the resource. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets the normalized resource name. + /// </summary> + public string NormalizedName + { + get { return StringHelpers.Normalize(this.Name); } + } + + /// <summary> + /// Gets or sets friendly name defined for the resource. + /// </summary> + public string? FriendlyName { get; set; } + + /// <summary> + /// Gets or sets module which implements the resource. This could point to parent module, if the DSC resource is implemented. + /// by one of nested modules. + /// </summary> + public PSModuleInfo? Module { get; set; } + + /// <summary> + /// Gets or sets name of the module which implements the resource. + /// </summary> + public string? ModuleName { get; set; } + + /// <summary> + /// Gets the normalized module name. + /// </summary> + public string? NormalizedModuleName + { + get { return (this.ModuleName is not null) ? StringHelpers.Normalize(this.ModuleName) : null; } + } + + /// <summary> + /// Gets or sets version of the module which implements the resource. + /// </summary> + public Version? Version { get; set; } + + /// <summary> + /// Gets or sets of the file which implements the resource. For the resources which are defined using + /// MOF file, this will be path to a module which resides in the same folder where schema.mof file is present. + /// For composite resources, this will be the module which implements the resource. + /// </summary> + public string? Path { get; set; } + + /// <summary> + /// Gets or sets parent folder, where the resource is defined + /// It is the folder containing either the implementing module(=Path) or folder containing ".schema.mof". + /// For native providers, Path will be null and only ParentPath will be present. + /// </summary> + public string? ParentPath { get; set; } + + /// <summary> + /// Gets or sets a value which indicate how DSC resource is implemented. + /// </summary> + public ImplementedAsTypeInternal? ImplementedAs { get; set; } + + /// <summary> + /// Gets or sets company which owns this resource. + /// </summary> + public string? CompanyName { get; set; } + + /// <summary> + /// Gets properties of the resource. + /// </summary> + public List<DscResourcePropertyInfoInternal> Properties { get; private set; } = new List<DscResourcePropertyInfoInternal>(); + + /// <summary> + /// Updates properties of the resource. + /// </summary> + /// <param name="properties">Updated properties.</param> + public void UpdateProperties(List<DscResourcePropertyInfoInternal> properties) + { + this.Properties = properties; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/DscResourcePropertyInfoInternal.cs b/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/DscResourcePropertyInfoInternal.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscResourcePropertyInfoInternal.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DscResourcesInfo +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// Contains a DSC resource property information. + /// </summary> + internal sealed class DscResourcePropertyInfoInternal + { + private const string DscResourcePropertyInfoFullName = "Microsoft.PowerShell.DesiredStateConfiguration.DscResourcePropertyInfo"; + + /// <summary> + /// Initializes a new instance of the <see cref="DscResourcePropertyInfoInternal"/> class. + /// </summary> + /// <param name="dscPropertyInfo">Dynamic DSC property info.</param> + public DscResourcePropertyInfoInternal(dynamic dscPropertyInfo) + { + if (dscPropertyInfo.GetType().FullName != DscResourcePropertyInfoFullName) + { + throw new ArgumentException(); + } + + this.Name = dscPropertyInfo.Name; + this.PropertyType = dscPropertyInfo.PropertyType; + this.IsMandatory = dscPropertyInfo.IsMandatory; + this.Values = dscPropertyInfo.Values; + } + + /// <summary> + /// Gets or sets name of the property. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets type of the property. + /// </summary> + public string PropertyType { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the property is mandatory or not. + /// </summary> + public bool IsMandatory { get; set; } + + /// <summary> + /// Gets Values for a resource property. + /// </summary> + public List<string> Values { get; private set; } = new List<string>(); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/ImplementedAsTypeInternal.cs b/src/Microsoft.Management.Configuration.Processor/DscResourcesInfo/ImplementedAsTypeInternal.cs @@ -0,0 +1,34 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ImplementedAsTypeInternal.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DscResourcesInfo +{ + /// <summary> + /// Enumerated values for DSC resource implementation type. + /// </summary> + internal enum ImplementedAsTypeInternal + { + /// <summary> + /// DSC resource implementation type not known. + /// </summary> + None = 0, + + /// <summary> + /// DSC resource is implemented using PowerShell module. + /// </summary> + PowerShell = 1, + + /// <summary> + /// DSC resource is implemented using a CIM provider. + /// </summary> + Binary = 2, + + /// <summary> + /// DSC resource is a composite and implemented using configuration keyword. + /// </summary> + Composite = 3, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ErrorCodes.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + /// <summary> + /// This should match the ones in AppInstallerErrors.h. + /// </summary> + internal static class ErrorCodes + { + /// <summary> + /// The module of the unit was installed, but the unit was not found. + /// </summary> + internal const int WinGetConfigUnitNotFound = unchecked((int)0x8A15C101); + + /// <summary> + /// The unit couldn't be found in the repository. + /// </summary> + internal const int WinGetConfigUnitNotFoundRepository = unchecked((int)0x8A15C102); + + /// <summary> + /// Multiple units found with the same criteria. + /// </summary> + internal const int WinGetConfigUnitMultipleMatches = unchecked((int)0x8A15C103); + + /// <summary> + /// Internal error calling Invoke-DscResource Get. + /// </summary> + internal const int WinGetConfigUnitInvokeGet = unchecked((int)0x8A15C104); + + /// <summary> + /// Internal error calling Invoke-DscResource Test. + /// </summary> + internal const int WinGetConfigUnitInvokeTest = unchecked((int)0x8A15C105); + + /// <summary> + /// Internal error calling Invoke-DscResource Set. + /// </summary> + internal const int WinGetConfigUnitInvokeSet = unchecked((int)0x8A15C106); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/FindDscResourceNotFoundException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/FindDscResourceNotFoundException.cs @@ -0,0 +1,31 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FindDscResourceNotFoundException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// <summary> + /// Resource not found by Find-DscResource. + /// </summary> + internal class FindDscResourceNotFoundException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="FindDscResourceNotFoundException"/> class. + /// </summary> + /// <param name="unitName">Unit name.</param> + public FindDscResourceNotFoundException(string unitName) + { + this.HResult = ErrorCodes.WinGetConfigUnitNotFoundRepository; + this.UnitName = unitName; + } + + /// <summary> + /// Gets the unit name. + /// </summary> + public string UnitName { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/GetDscResourceMultipleMatches.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/GetDscResourceMultipleMatches.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetDscResourceMultipleMatches.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// A call to Get-DscResource return multiple results for a specific resource. + /// </summary> + internal class GetDscResourceMultipleMatches : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="GetDscResourceMultipleMatches"/> class. + /// </summary> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + public GetDscResourceMultipleMatches(string resourceName, ModuleSpecification? module) + { + this.HResult = ErrorCodes.WinGetConfigUnitMultipleMatches; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InstallDscResourceException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InstallDscResourceException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InstallDscResourceException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Installing a DSC resource failed unexpectedly. + /// </summary> + internal class InstallDscResourceException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="InstallDscResourceException"/> class. + /// </summary> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Module.</param> + public InstallDscResourceException(string resourceName, ModuleSpecification? module) + { + this.HResult = ErrorCodes.WinGetConfigUnitNotFound; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceGetException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceGetException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvokeDscResourceGetException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// A call to Invoke-DscResource Get failed unexpectedly. + /// </summary> + internal class InvokeDscResourceGetException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceGetException"/> class. + /// </summary> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + public InvokeDscResourceGetException(string resourceName, ModuleSpecification? module) + { + this.HResult = ErrorCodes.WinGetConfigUnitInvokeGet; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceSetException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceSetException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvokeDscResourceSetException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// A call to Invoke-DscResource Set failed unexpectedly. + /// </summary> + internal class InvokeDscResourceSetException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceSetException"/> class. + /// </summary> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + public InvokeDscResourceSetException(string resourceName, ModuleSpecification? module) + { + this.HResult = ErrorCodes.WinGetConfigUnitInvokeSet; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceTestException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceTestException.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="InvokeDscResourceTestException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// A call to Invoke-DscResource Test failed unexpectedly. + /// </summary> + internal class InvokeDscResourceTestException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceTestException"/> class. + /// </summary> + /// <param name="resourceName">Resource name.</param> + /// <param name="module">Optional module.</param> + public InvokeDscResourceTestException(string resourceName, ModuleSpecification? module) + { + this.HResult = ErrorCodes.WinGetConfigUnitInvokeTest; + this.ResourceName = resourceName; + this.Module = module; + } + + /// <summary> + /// Gets the resource name. + /// </summary> + public string ResourceName { get; } + + /// <summary> + /// Gets the module, if any. + /// </summary> + public ModuleSpecification? Module { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/ExceptionExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/ExceptionExtensions.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ExceptionExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System; + + /// <summary> + /// Extension method for Exception. + /// </summary> + internal static class ExceptionExtensions + { + /// <summary> + /// Gets the most inner exception. + /// </summary> + /// <param name="e">Exception.</param> + /// <returns>Most inner exception.</returns> + public static Exception GetMostInnerException(this Exception e) + { + Exception ex = e; + while (ex.InnerException != null) + { + ex = ex.InnerException; + } + + return ex; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/PowerShellExtensions.cs @@ -0,0 +1,76 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Collections.ObjectModel; + using System.Management.Automation; + using System.Text; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Extensions methods for <see cref="PowerShell"/> class. + /// </summary> + internal static class PowerShellExtensions + { + /// <summary> + /// Calls Invoke with <see cref="ActionPreference.Stop"/> to stop execution of commands. + /// </summary> + /// <param name="pwsh">PowerShell.</param> + /// <returns>Collection of PSObjects representing output.</returns> + public static Collection<PSObject> InvokeAndStopOnError(this PowerShell pwsh) + { + var settings = new PSInvocationSettings() + { + ErrorActionPreference = ActionPreference.Stop, + }; + + var result = pwsh.Invoke(null, settings); + + pwsh.ValidateErrorStream(); + + return result; + } + + /// <summary> + /// Calls Invoke with <see cref="ActionPreference.Stop"/> to stop execution of commands. + /// </summary> + /// <typeparam name="T">Type of output object(s) expected from the command invocation.</typeparam> + /// <param name="pwsh">PowerShell.</param> + /// <returns>Collection of the type output representing output.</returns> + public static Collection<T> InvokeAndStopOnError<T>(this PowerShell pwsh) + { + var settings = new PSInvocationSettings() + { + ErrorActionPreference = ActionPreference.Stop, + }; + + var result = pwsh.Invoke<T>(null, settings); + + pwsh.ValidateErrorStream(); + + return result; + } + + /// <summary> + /// Throws <see cref="WriteErrorException"/> if there are streams in the stream error. + /// </summary> + /// <param name="pwsh">PowerShell.</param> + public static void ValidateErrorStream(this PowerShell pwsh) + { + if (pwsh.HadErrors) + { + var psStreamBuilder = new StringBuilder(); + foreach (var line in pwsh.Streams.Error) + { + psStreamBuilder.AppendLine(line.ToString()); + } + + throw new WriteErrorException(psStreamBuilder.ToString()); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitAndResource.cs @@ -0,0 +1,96 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitAndResource.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + + /// <summary> + /// Contains information about the unit and the DSC resource that applies to it. + /// </summary> + internal class ConfigurationUnitAndResource + { + private readonly ConfigurationUnitInternal configurationUnitInternal; + private readonly DscResourceInfoInternal dscResourceInfoInternal; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitAndResource"/> class. + /// </summary> + /// <param name="configurationUnitInternal">Configuration unit internal.</param> + /// <param name="dscResourceInfoInternal">DscResourceInfoInternal.</param> + public ConfigurationUnitAndResource( + ConfigurationUnitInternal configurationUnitInternal, + DscResourceInfoInternal dscResourceInfoInternal) + { + if (configurationUnitInternal.Unit.UnitName != dscResourceInfoInternal.Name) + { + throw new ArgumentException(); + } + + this.configurationUnitInternal = configurationUnitInternal; + this.dscResourceInfoInternal = dscResourceInfoInternal; + } + + /// <summary> + /// Gets the configuration unit. + /// </summary> + public ConfigurationUnit Unit + { + get { return this.configurationUnitInternal.Unit; } + } + + /// <summary> + /// Gets the directives overlay. + /// </summary> + public IReadOnlyDictionary<string, object>? DirectivesOverlay + { + get { return this.configurationUnitInternal.DirectivesOverlay; } + } + + /// <summary> + /// Gets the DSC resource name. + /// </summary> + /// <returns>DSC resource name.</returns> + public string ResourceName + { + get { return this.dscResourceInfoInternal.Name; } + } + + /// <summary> + /// Gets the module specification. + /// </summary> + public ModuleSpecification? Module + { + get { return this.configurationUnitInternal.Module; } + } + + /// <summary> + /// Gets a directive if exits. + /// </summary> + /// <param name="directiveName">Name of directive.</param> + /// <returns>The value of the directive. Null if doesn't exist.</returns> + public string? GetDirective(string directiveName) + { + return this.configurationUnitInternal.GetDirective(directiveName); + } + + /// <summary> + /// TODO: Implement. + /// I am so sad because rs.SessionStateProxy.InvokeCommand.ExpandString doesn't work as I wanted. + /// PowerShell assumes all code passed to ExpandString is trusted and we cannot assume that. + /// </summary> + /// <returns>ValueSet with settings.</returns> + public ValueSet GetExpandedSettings() + { + return this.Unit.Settings; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/ConfigurationUnitInternal.cs @@ -0,0 +1,100 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitInternal.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor.Constants; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Wrapper around Configuration units and its directives. Creates a normalized directives map + /// for consumption. + /// </summary> + internal class ConfigurationUnitInternal + { + private readonly Dictionary<string, object> normalizedDirectives = new (); + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitInternal"/> class. + /// </summary> + /// <param name="unit">Configuration unit.</param> + /// <param name="directivesOverlay">Directives overlay.</param> + public ConfigurationUnitInternal( + ConfigurationUnit unit, + IReadOnlyDictionary<string, object>? directivesOverlay = null) + { + this.Unit = unit; + this.DirectivesOverlay = directivesOverlay; + this.InitializeDirectives(); + + string? moduleName = this.GetDirective(DirectiveConstants.Module); + if (string.IsNullOrEmpty(moduleName)) + { + this.Module = null; + } + else + { + this.Module = PowerShellHelpers.CreateModuleSpecification( + moduleName, + this.GetDirective(DirectiveConstants.Version), + this.GetDirective(DirectiveConstants.MinVersion), + this.GetDirective(DirectiveConstants.MaxVersion), + this.GetDirective(DirectiveConstants.ModuleGuid)); + } + } + + /// <summary> + /// Gets the configuration unit. + /// </summary> + public ConfigurationUnit Unit { get; } + + /// <summary> + /// Gets the directives overlay. + /// </summary> + public IReadOnlyDictionary<string, object>? DirectivesOverlay { get; } + + /// <summary> + /// Gets the module specification. + /// </summary> + public ModuleSpecification? Module { get; } + + /// <summary> + /// Get a directive from the unit taking into account the directives overlay. + /// </summary> + /// <param name="directiveName">Directive name.</param> + /// <returns>Value of directive, null if not found.</returns> + public string? GetDirective(string directiveName) + { + var normalizedDirectiveName = StringHelpers.Normalize(directiveName); + if (this.normalizedDirectives.TryGetValue(normalizedDirectiveName, out object? value)) + { + return value as string; + } + + return null; + } + + private void InitializeDirectives() + { + // Overlay directives have precedence. + if (this.DirectivesOverlay is not null) + { + foreach (var directive in this.DirectivesOverlay) + { + var normalizedKey = StringHelpers.Normalize(directive.Key); + this.normalizedDirectives.Add(normalizedKey, directive.Value); + } + } + + foreach (var directive in this.Unit.Directives) + { + var normalizedKey = StringHelpers.Normalize(directive.Key); + _ = this.normalizedDirectives.TryAdd(normalizedKey, directive.Value); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/DscResourcesMap.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/DscResourcesMap.cs @@ -0,0 +1,223 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscResourcesMap.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + + /// <summary> + /// DscResources in the system. Get-DscResource looks the entire PSModulePath to find resources every time, so + /// calling it multiples times is expensive. This class will store them in memory per configuration set. + /// The resources are stored in a dictionary in the following hierarchy. + /// - Module name + /// - Resource name + /// - Module Version + /// - DscResourceInfo obj. + /// There are some resources that don't have a module name. These are modules that live under + /// C:\WINDOWS\system32\WindowsPowershell\v1.0\Modules\PsDesiredStateConfiguration\DscResources. + /// This is not currently used. For now, we let PowerShell handle all the versioning. In order to avoid + /// calls to Get-DscResource every time a unit is set, this class needs to be updated to handle min and max + /// version. As it is, it just does RequiredVersion. + /// </summary> + internal sealed class DscResourcesMap + { + private readonly Dictionary<string, ModuleDscResources> resources = new Dictionary<string, ModuleDscResources>(); + + /// <summary> + /// Initializes a new instance of the <see cref="DscResourcesMap"/> class. + /// </summary> + /// <param name="resources">DSC Resources.</param> + public DscResourcesMap(IReadOnlyList<DscResourceInfoInternal> resources) + { + foreach (var resource in resources) + { + if (string.IsNullOrEmpty(resource.Name)) + { + throw new ArgumentException(nameof(resource.Name)); + } + + if (resource.Version is null) + { + throw new ArgumentException(nameof(resource.Version)); + } + + this.Insert(resource); + } + } + + /// <summary> + /// Looks for a DSC resource in the specified module. + /// </summary> + /// <param name="dscResourceName">DSC resource name.</param> + /// <param name="moduleName">Optional module name.</param> + /// <param name="version">Optional module version.</param> + /// <returns>DscResourceInfoInternal. Null if not found.</returns> + public DscResourceInfoInternal? GetResource(string dscResourceName, string? moduleName, Version? version) + { + string normalizedResourceName = StringHelpers.Normalize(dscResourceName); + string? normalizedModuleName = null; + if (moduleName is not null) + { + normalizedModuleName = StringHelpers.Normalize(moduleName); + } + + if (normalizedModuleName is null) + { + foreach (var module in this.resources.Values) + { + var resourceInfo = module.GetDscResourceInfo(normalizedResourceName, version); + if (resourceInfo is not null) + { + return resourceInfo; + } + } + } + else + { + if (this.resources.TryGetValue(normalizedModuleName, out ModuleDscResources? module)) + { + return module.GetDscResourceInfo(normalizedResourceName, version); + } + } + + return null; + } + + /// <summary> + /// Does a DSC resource exists. + /// </summary> + /// <param name="dscResourceName">DSC resource name.</param> + /// <param name="moduleName">Optional module name.</param> + /// <param name="version">Optional module version.</param> + /// <returns>True if resource exists.</returns> + public bool Exists(string dscResourceName, string? moduleName, Version? version) + { + return this.GetResource(dscResourceName, moduleName, version) != null; + } + + private void Insert(DscResourceInfoInternal dscResourceInfo) + { + string? normalizedModuleName = dscResourceInfo.NormalizedModuleName; + + // There are some resources that doesn't have module, use empty as key. + string moduleKey = string.Empty; + if (normalizedModuleName is not null) + { + moduleKey = normalizedModuleName; + } + + if (!this.resources.ContainsKey(moduleKey)) + { + this.resources.Add(moduleKey, new ModuleDscResources(moduleKey, dscResourceInfo)); + } + else + { + this.resources[moduleKey].AddResource(moduleKey, dscResourceInfo); + } + } + + /// <summary> + /// Represents a map versions for a DscResource. + /// </summary> + private class DscResourceVersions : Dictionary<Version, DscResourceInfoInternal> + { + private readonly string resourceName; + + public DscResourceVersions(DscResourceInfoInternal dscResourceInfo) + { + this.resourceName = dscResourceInfo.NormalizedName; + this.AddVersion(dscResourceInfo); + } + + /// <summary> + /// Adds a resource version. + /// </summary> + /// <param name="dscResourceInfo">DscResourceInfo.</param> + public void AddVersion(DscResourceInfoInternal dscResourceInfo) + { + if (this.resourceName != dscResourceInfo.NormalizedName) + { + throw new ArgumentException(dscResourceInfo.NormalizedName); + } + + // There are some system resources without version or module name. + if (dscResourceInfo.Version is null) + { + this.Add(new Version(), dscResourceInfo); + } + else + { + // Get-DscResource will fail if the same module with same + // resources and same version exists in the module path + // so this shouldn't happen. Either way, we should throw there or here. + this.Add(dscResourceInfo.Version, dscResourceInfo); + } + } + + public DscResourceInfoInternal? GetDscResourceInfo(Version? version) + { + if (version is null) + { + // Sort keys, get the latest. + var newestVersion = this.Keys.OrderByDescending(v => v).First(); + return this[newestVersion]; + } + + if (this.TryGetValue(version, out DscResourceInfoInternal? resourceInfo)) + { + return resourceInfo; + } + + return null; + } + } + + /// <summary> + /// Represent a map of resources for a module. + /// </summary> + private class ModuleDscResources : Dictionary<string, DscResourceVersions> + { + private readonly string normalizedModuleName; + + public ModuleDscResources(string normalizedModuleName, DscResourceInfoInternal dscResourceInfo) + { + this.normalizedModuleName = normalizedModuleName; + this.AddResource(this.normalizedModuleName, dscResourceInfo); + } + + public void AddResource(string normalizedModuleName, DscResourceInfoInternal dscResourceInfo) + { + if (this.normalizedModuleName != normalizedModuleName) + { + throw new ArgumentException(nameof(normalizedModuleName)); + } + + var normalizedResourceName = dscResourceInfo.NormalizedName; + if (!this.ContainsKey(normalizedResourceName)) + { + this.Add(normalizedResourceName, new DscResourceVersions(dscResourceInfo)); + } + else + { + this[normalizedResourceName].AddVersion(dscResourceInfo); + } + } + + public DscResourceInfoInternal? GetDscResourceInfo(string dscResourceName, Version? version) + { + if (this.TryGetValue(dscResourceName, out DscResourceVersions? versions)) + { + return versions.GetDscResourceInfo(version); + } + + return null; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/PowerShellHelpers.cs @@ -0,0 +1,71 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellHelpers.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System.Collections; + using Microsoft.PowerShell.Commands; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// General PowerShell helpers. + /// </summary> + internal static class PowerShellHelpers + { + /// <summary> + /// Creates a module specification object. + /// </summary> + /// <param name="moduleName">Module name.</param> + /// <param name="version">Optional version.</param> + /// <param name="minVersion">Optional min version.</param> + /// <param name="maxVersion">Optional max version.</param> + /// <param name="guid">Optional guid.</param> + /// <returns>ModuleSpecification.</returns> + public static ModuleSpecification CreateModuleSpecification( + string moduleName, + string? version = null, + string? minVersion = null, + string? maxVersion = null, + string? guid = null) + { + if (version is null && + minVersion is null && + maxVersion is null) + { + // Otherwise will fail with MissingMemberException... + return new ModuleSpecification(moduleName); + } + + var moduleInfo = new Hashtable + { + { Parameters.ModuleName, moduleName }, + }; + + if (!string.IsNullOrEmpty(version)) + { + moduleInfo.Add(Parameters.RequiredVersion, version); + } + + if (!string.IsNullOrEmpty(minVersion)) + { + moduleInfo.Add(Parameters.ModuleVersion, minVersion); + } + + if (!string.IsNullOrEmpty(maxVersion)) + { + moduleInfo.Add(Parameters.MaximumVersion, maxVersion); + } + + if (!string.IsNullOrEmpty(guid)) + { + moduleInfo.Add(Parameters.Guid, guid); + } + + // Using the Hashtable constructor will verify that RequiredVersion is used properly. + return new ModuleSpecification(moduleInfo); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/StringHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/StringHelpers.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// <copyright file="StringHelpers.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + /// <summary> + /// String helpers. + /// </summary> + internal static class StringHelpers + { + /// <summary> + /// Normalize string. + /// </summary> + /// <param name="value">Value.</param> + /// <returns>Normalized string.</returns> + public static string Normalize(string value) + { + return value.ToLowerInvariant(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs @@ -0,0 +1,91 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TypeHelpers.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Helpers +{ + using System.Collections.Generic; + using System.Reflection; + using Windows.Foundation.Collections; + + /// <summary> + /// Type helpers. + /// </summary> + internal static class TypeHelpers + { + /// <summary> + /// Verifies a property exists. + /// </summary> + /// <param name="obj">Dynamic object.</param> + /// <param name="name">Name of property.</param> + /// <returns>True if property exists.</returns> + public static bool PropertyExists(dynamic obj, string name) + { + return obj.GetType().GetProperty(name) is not null; + } + + /// <summary> + /// Verifies a property exists with the specified type. + /// </summary> + /// <typeparam name="TType">Expected type.</typeparam> + /// <param name="obj">Dynamic object.</param> + /// <param name="name">Name of property.</param> + /// <returns>True if property and is of the specified type.</returns> + public static bool PropertyWithTypeExists<TType>(dynamic obj, string name) + { + return PropertyExists(obj, name) && + obj.GetType().GetProperty(name).PropertyType == typeof(TType); + } + + /// <summary> + /// Verifies the property exist and is an enum. + /// </summary> + /// <param name="obj">Dynamic object.</param> + /// <param name="name">Name of property.</param> + /// <returns>True if property exists and is an enum.</returns> + public static bool PropertyExistsAndIsEnum(dynamic obj, string name) + { + return PropertyExists(obj, name) && + obj.GetType().GetProperty(name).PropertyType.IsEnum; + } + + /// <summary> + /// Verifies the property exists and is a list. Use this when you don't know the type of the List. + /// </summary> + /// <param name="obj">Dynamic object.</param> + /// <param name="name">Name of property.</param> + /// <returns>True if property exists and is a list.</returns> + public static bool PropertyExistsAndIsList(dynamic obj, string name) + { + return PropertyExists(obj, name) && + obj.GetType().GetProperty(name).PropertyType.IsGenericType && + obj.GetType().GetProperty(name).PropertyType.GetGenericTypeDefinition() == typeof(List<>); + } + + /// <summary> + /// Gets all the properties and values from an object. + /// </summary> + /// <param name="obj">Object.</param> + /// <returns>ValueSet with properties names and values.</returns> + public static ValueSet GetAllPropertiesValues(object obj) + { + var result = new ValueSet(); + foreach (PropertyInfo property in obj.GetType().GetProperties()) + { + // Specialize here. + if (property.PropertyType.IsEnum) + { + result.Add(property.Name, property.GetValue(obj)?.ToString()); + } + else + { + result.Add(property.Name, property.GetValue(obj)); + } + } + + return result; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj b/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj @@ -0,0 +1,46 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0-windows10.0.22000.0</TargetFramework> + <Nullable>enable</Nullable> + <SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion> + <Platforms>x64;x86;arm64</Platforms> + <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutputPath> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)'=='Release'"> + <!-- TODO: cswinrt generate source files that will fail with warning CS1591. + We need to somehow skip these warning via a .editorconfig or something similar. + Ideally cswinrt would fix this in the future. + --> + <!--<TreatWarningsAsErrors>true</TreatWarningsAsErrors>--> + </PropertyGroup> + + <PropertyGroup> + <CsWinRTComponent>true</CsWinRTComponent> + <CsWinRTWindowsMetadata>10.0.22000.0</CsWinRTWindowsMetadata> + </PropertyGroup> + + <ItemGroup> + <AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.1" /> + <PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.8" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Microsoft.Management.Configuration.Projection\Microsoft.Management.Configuration.Projection.csproj" > + <OutputItemType>Content</OutputItemType> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ReferenceOutputAssembly>True</ReferenceOutputAssembly> + </ProjectReference> + </ItemGroup> + +</Project>+ \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/HostedEnvironment.cs @@ -0,0 +1,412 @@ +// ----------------------------------------------------------------------------- +// <copyright file="HostedEnvironment.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Runspaces +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using System.Runtime.InteropServices.WindowsRuntime; + using Microsoft.Management.Configuration.Processor.Constants; + using Microsoft.Management.Configuration.Processor.DscModule; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + using Windows.Security.Cryptography.Certificates; + using Windows.Storage.Streams; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// Process environment. Provides interaction with PowerShell for a hosted environment. + /// </summary> + internal class HostedEnvironment : IProcessorEnvironment + { + private ConfigurationProcessorType type; + + /// <summary> + /// Initializes a new instance of the <see cref="HostedEnvironment"/> class. + /// </summary> + /// <param name="runspace">PowerShell Runspace.</param> + /// <param name="type">Configuration processor type.</param> + /// <param name="dscModule">IDscModule.</param> + public HostedEnvironment(Runspace runspace, ConfigurationProcessorType type, IDscModule dscModule) + { + this.Runspace = runspace; + this.type = type; + this.DscModule = dscModule; + } + + /// <inheritdoc/> + public Runspace Runspace { get; } + + /// <summary> + /// Gets the DscModule. + /// </summary> + protected IDscModule DscModule { get; } + + /// <inheritdoc/> + public void ValidateRunspace() + { + // Only support PowerShell Core. + if (this.GetVariable<string>(Variables.PSEdition) != Core) + { + throw new NotSupportedException(); + } + + var powerShellGet = PowerShellHelpers.CreateModuleSpecification( + Modules.PowerShellGet, + minVersion: Modules.PowerShellGetMinVersion); + if (!this.ValidateModule(powerShellGet)) + { + var previousVersion = this.GetAvailableModule( + PowerShellHelpers.CreateModuleSpecification( + Modules.PowerShellGet)); + string message = $"Required '{powerShellGet}'"; + if (previousVersion is not null) + { + message += $" Found '{previousVersion.Name} {previousVersion.Version}'"; + } + + throw new NotSupportedException(message); + } + + // Make sure PSDesiredConfiguration is present. + this.InstallModule(this.DscModule.ModuleSpecification); + } + + /// <inheritdoc/> + public IReadOnlyList<DscResourceInfoInternal> GetAllDscResources() => + this.DscModule.GetAllDscResources(this.Runspace); + + /// <inheritdoc/> + public IReadOnlyList<DscResourceInfoInternal> GetDscResourcesInModule(ModuleSpecification moduleSpecification) => + this.DscModule.GetDscResourcesInModule(this.Runspace, moduleSpecification); + + /// <inheritdoc/> + public DscResourceInfoInternal? GetDscResource(ConfigurationUnitInternal unitInternal) => + this.DscModule.GetDscResource(this.Runspace, unitInternal.Unit.UnitName, unitInternal.Module); + + /// <inheritdoc/> + public ValueSet InvokeGetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) => + this.DscModule.InvokeGetResource(this.Runspace, settings, name, moduleSpecification); + + /// <inheritdoc/> + public bool InvokeTestResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) => + this.DscModule.InvokeTestResource(this.Runspace, settings, name, moduleSpecification); + + /// <inheritdoc/> + public bool InvokeSetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification) => + this.DscModule.InvokeSetResource(this.Runspace, settings, name, moduleSpecification); + + /// <inheritdoc/> + public PSModuleInfo? GetImportedModule(ModuleSpecification moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + var moduleInfo = pwsh.AddCommand(Commands.GetModule) + .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) + .Invoke<PSModuleInfo>() + .FirstOrDefault(); + + return moduleInfo; + } + + /// <inheritdoc/> + public PSModuleInfo? GetAvailableModule(ModuleSpecification moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + var moduleInfo = pwsh.AddCommand(Commands.GetModule) + .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) + .AddParameter(Parameters.ListAvailable) + .Invoke<PSModuleInfo>() + .FirstOrDefault(); + + return moduleInfo; + } + + /// <inheritdoc/> + public PSModuleInfo? GetAvailableModule(string path) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + var moduleInfo = pwsh.AddCommand(Commands.GetModule) + .AddParameter(Parameters.Name, path) + .AddParameter(Parameters.ListAvailable) + .Invoke<PSModuleInfo>() + .FirstOrDefault(); + + return moduleInfo; + } + + /// <inheritdoc/> + public void ImportModule(ModuleSpecification moduleSpecification) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + _ = pwsh.AddCommand(Commands.ImportModule) + .AddParameter(Parameters.FullyQualifiedName, moduleSpecification) + .Invoke(); + } + + /// <inheritdoc/> + public PSObject? GetInstalledModule(ModuleSpecification moduleSpecification) + { + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, moduleSpecification.Name }, + }; + + if (moduleSpecification.Version is not null) + { + parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); + } + + if (moduleSpecification.MaximumVersion is not null) + { + parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); + } + + if (moduleSpecification.RequiredVersion is not null) + { + parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); + } + + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + var result = pwsh.AddCommand(Commands.GetInstalledModule) + .AddParameters(parameters) + .Invoke() + .FirstOrDefault(); + + return result; + } + + /// <inheritdoc/> + public PSObject? FindDscResource(ConfigurationUnitInternal unitInternal) + { + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, unitInternal.Unit.UnitName }, + }; + + if (unitInternal.Module is not null) + { + parameters.Add(Parameters.ModuleName, unitInternal.Module.Name); + + if (unitInternal.Module.Version is not null) + { + parameters.Add(Parameters.MinimumVersion, unitInternal.Module.Version); + } + + if (unitInternal.Module.MaximumVersion is not null) + { + parameters.Add(Parameters.MaximumVersion, unitInternal.Module.MaximumVersion); + } + + if (unitInternal.Module.RequiredVersion is not null) + { + parameters.Add(Parameters.RequiredVersion, unitInternal.Module.RequiredVersion); + } + } + + string? repository = unitInternal.GetDirective(DirectiveConstants.Repository); + if (!string.IsNullOrEmpty(repository)) + { + parameters.Add(Parameters.Repository, repository); + } + + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + // TODO: Implement prerelease directive. + // The result is just a PSCustomObject with a type name of Microsoft.PowerShell.Commands.PSGetDscResourceInfo. + // When no module is passed and a resource is not found, this will return an empty list. If a module + // is specified and no resource is found then it will fail earlier because of a Write-Error. + var result = pwsh.AddCommand(Commands.FindDscResource) + .AddParameters(parameters) + .Invoke() + .FirstOrDefault(); + + return result; + } + + /// <inheritdoc/> + public void SaveModule(PSObject inputObject, string location) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + _ = pwsh.AddCommand(Commands.SaveModule) + .AddParameter(Parameters.Path, location) + .AddParameter(Parameters.InputObject, inputObject) + .InvokeAndStopOnError(); + } + + /// <inheritdoc/> + public void InstallModule(PSObject inputObject) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + // If the repository is untrusted, it will fail with: + // Microsoft.PowerShell.Commands.WriteErrorException : Exception calling "ShouldContinue" with "5" + // argument(s): "A command that prompts the user failed because the host program or the command type + // does not support user interaction. + // If its trusted, PowerShellGets adds the Force parameter to the call to PackageManager\Install-Package. + // TODO: Once we have policies, we should remove Force. For hosted environments and depending + // on the policy we will trust PSGallery when we create the Runspace or add Force here. + _ = pwsh.AddCommand(Commands.InstallModule) + .AddParameter(Parameters.InputObject, inputObject) + .AddParameter(Parameters.Force) + .InvokeAndStopOnError(); + } + + /// <inheritdoc/> + public void InstallModule(ModuleSpecification moduleSpecification) + { + // Maybe is already there. + if (!this.ValidateModule(moduleSpecification)) + { + // Ok, we have to get it. + var parameters = new Dictionary<string, object>() + { + { Parameters.Name, moduleSpecification.Name }, + }; + if (moduleSpecification.Version is not null) + { + parameters.Add(Parameters.MinimumVersion, moduleSpecification.Version); + } + + if (moduleSpecification.MaximumVersion is not null) + { + parameters.Add(Parameters.MaximumVersion, moduleSpecification.MaximumVersion); + } + + if (moduleSpecification.RequiredVersion is not null) + { + parameters.Add(Parameters.RequiredVersion, moduleSpecification.RequiredVersion); + } + + using PowerShell pwsh = PowerShell.Create(this.Runspace); + _ = pwsh.AddCommand(Commands.InstallModule) + .AddParameters(parameters) + .AddParameter(Parameters.Force) + .InvokeAndStopOnError(); + } + } + + /// <inheritdoc/> + public List<Certificate> GetCertsOfValidSignedFiles(string[] paths) + { + using PowerShell pwsh = PowerShell.Create(this.Runspace); + + var signatures = pwsh.AddCommand(Commands.GetChildItem) + .AddParameter(Parameters.Path, paths) + .AddCommand(Commands.GetAuthenticodeSignature) + .InvokeAndStopOnError<Signature>(); + + var thumbprint = new HashSet<string>(); + var certificates = new List<Certificate>(); + foreach (var signature in signatures) + { + if (signature.Status == SignatureStatus.Valid) + { + if (thumbprint.Add(signature.SignerCertificate.Thumbprint)) + { + IBuffer buffer = signature.SignerCertificate.GetRawCertData().AsBuffer(); + certificates.Add(new Certificate(buffer)); + } + } + } + + return certificates; + } + + /// <inheritdoc/> + public TType GetVariable<TType>(string name) + { + return (TType)this.Runspace.SessionStateProxy.PSVariable.GetValue(name); + } + + /// <inheritdoc/> + public void SetVariable(string name, object value) + { + this.Runspace.SessionStateProxy.PSVariable.Set(name, value); + } + + /// <inheritdoc/> + public void SetPSModulePath(string path) + { + this.SetVariable(Variables.PSModulePath, path); + } + + /// <inheritdoc/> + public void SetPSModulePaths(IReadOnlyList<string> paths) + { + this.SetVariable(Variables.PSModulePath, string.Join(";", paths)); + } + + /// <inheritdoc/> + public void PrependPSModulePath(string path) + { + string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); + this.SetPSModulePath($"{path};{oldModulePath}"); + } + + /// <inheritdoc/> + public void PrependPSModulePaths(IReadOnlyList<string> paths) + { + string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); + this.SetPSModulePath($"{string.Join(";", paths)};{oldModulePath}"); + } + + /// <inheritdoc/> + public void AppendPSModulePath(string path) + { + string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); + this.SetPSModulePath($"{oldModulePath};{path}"); + } + + /// <inheritdoc/> + public void AppendPSModulePaths(IReadOnlyList<string> paths) + { + string oldModulePath = this.GetVariable<string>(Variables.PSModulePath); + this.SetPSModulePath($"{oldModulePath};{string.Join(";", paths)}"); + } + + /// <inheritdoc/> + public void CleanupPSModulePath(string path) + { + string newModulePath = this.GetVariable<string>(Variables.PSModulePath) + .Replace($"{path};", null) + .Replace($";{path}", null); + + this.SetPSModulePath(newModulePath); + } + + private bool ValidateModule(ModuleSpecification moduleSpecification) + { + var loadedModule = this.GetImportedModule(moduleSpecification); + if (loadedModule is not null) + { + return true; + } + + var availableModule = this.GetAvailableModule(moduleSpecification); + if (availableModule is not null) + { + this.ImportModule(moduleSpecification); + return true; + } + + return false; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/IProcessorEnvironment.cs @@ -0,0 +1,205 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IProcessorEnvironment.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments +{ + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + using Windows.Security.Cryptography.Certificates; + + /// <summary> + /// IProcessorEnvironment. Provides interaction with PowerShell. + /// </summary> + internal interface IProcessorEnvironment + { + /// <summary> + /// Gets the runspace. + /// </summary> + System.Management.Automation.Runspaces.Runspace Runspace { get; } + + /// <summary> + /// Validates the runspace. + /// </summary> + void ValidateRunspace(); + + /// <summary> + /// Gets all DSC resource. + /// </summary> + /// <returns>A list with the DSC resource.</returns> + IReadOnlyList<DscResourceInfoInternal> GetAllDscResources(); + + /// <summary> + /// Gets all resources in a module. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>List of resources.</returns> + IReadOnlyList<DscResourceInfoInternal> GetDscResourcesInModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Gets a DSC Resource. + /// </summary> + /// <param name="unitInternal">Configuration unit internal.</param> + /// <returns>DSC Resource.</returns> + DscResourceInfoInternal? GetDscResource(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// Calls Invoke-DscResource -Method Get from this module. + /// </summary> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>Properties of resource.</returns> + ValueSet InvokeGetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Invoke-DscResource -Method Test from this module. + /// </summary> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>Is in desired state.</returns> + bool InvokeTestResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Invoke-DscResource -Method Set from this module. + /// </summary> + /// <param name="settings">Settings.</param> + /// <param name="name">Name.</param> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>If a reboot is required.</returns> + bool InvokeSetResource(ValueSet settings, string name, ModuleSpecification? moduleSpecification); + + /// <summary> + /// Calls Get-Module with fully qualified name. + /// </summary> + /// <param name="moduleSpecification">Module name.</param> + /// <returns>PSModuleInfo, null if not imported.</returns> + PSModuleInfo? GetImportedModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Calls Get-Module with the fully qualified name and using ListAvailable. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>PSModuleInfo, null if not found.</returns> + PSModuleInfo? GetAvailableModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Calls Get-Module from a path using ListAvailable. + /// </summary> + /// <param name="path">Path.</param> + /// <returns>The first module returned, null if none.</returns> + PSModuleInfo? GetAvailableModule(string path); + + /// <summary> + /// Call Import-Module with the fully qualified name. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + void ImportModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Calls Get-InstalledModule. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + /// <returns>Module info, null if not installed.</returns> + PSObject? GetInstalledModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Calls Find-DscResource. + /// </summary> + /// <param name="unitInternal">Configuration unit internal.</param> + /// <returns>Dsc Resource info, null if not found.</returns> + PSObject? FindDscResource(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// Calls Save-Module -InputObject object -Path location. + /// Input object must be the result of Find cmdlets of PowerShellGet. + /// </summary> + /// <param name="inputObject">Input object.</param> + /// <param name="location">Location to save module.</param> + void SaveModule(PSObject inputObject, string location); + + /// <summary> + /// Calls Install-Module -InputObject object. + /// Input object must be the result of Find cmdlets of PowerShellGet. + /// </summary> + /// <param name="inputObject">Input object.</param> + void InstallModule(PSObject inputObject); + + /// <summary> + /// Calls Install-Module with a module specification. + /// </summary> + /// <param name="moduleSpecification">Module specification.</param> + void InstallModule(ModuleSpecification moduleSpecification); + + /// <summary> + /// Get unique certificates of valid signed files from the specified paths. + /// </summary> + /// <param name="paths">Path.</param> + /// <returns>List with valid signatures.</returns> + List<Certificate> GetCertsOfValidSignedFiles(string[] paths); + + /// <summary> + /// Gets the value of a variable. + /// </summary> + /// <typeparam name="TType">Type of the variable.</typeparam> + /// <param name="name">Name of variable.</param> + /// <returns>The value of a variable, null if doesn't exist.</returns> + TType GetVariable<TType>(string name); + + /// <summary> + /// Sets a variable with its value. + /// </summary> + /// <param name="name">Name of variable.</param> + /// <param name="value">Value of variable.</param> + void SetVariable(string name, object value); + + /// <summary> + /// Overwrites PSModulePath with the specified path. + /// </summary> + /// <param name="path">Path.</param> + void SetPSModulePath(string path); + + /// <summary> + /// Overwrites PSModulePath with the specified paths. + /// </summary> + /// <param name="paths">Paths.</param> + void SetPSModulePaths(IReadOnlyList<string> paths); + + /// <summary> + /// Prepends path to the PSModulePath. + /// </summary> + /// <param name="path">Path.</param> + void PrependPSModulePath(string path); + + /// <summary> + /// Prepends paths to the PSModulePath. + /// </summary> + /// <param name="paths">Paths.</param> + void PrependPSModulePaths(IReadOnlyList<string> paths); + + /// <summary> + /// Append path to the PSModulePath. + /// </summary> + /// <param name="path">Path.</param> + void AppendPSModulePath(string path); + + /// <summary> + /// Append paths to the PSModulePath. + /// </summary> + /// <param name="paths">Path.</param> + void AppendPSModulePaths(IReadOnlyList<string> paths); + + /// <summary> + /// Removes a path from the module path. + /// </summary> + /// <param name="path">Path.</param> + void CleanupPSModulePath(string path); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/ProcessorEnvironmentFactory.cs b/src/Microsoft.Management.Configuration.Processor/ProcessorEnvironments/ProcessorEnvironmentFactory.cs @@ -0,0 +1,76 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ProcessorEnvironmentFactory.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.ProcessorEnvironments +{ + using System; + using System.Collections.Generic; + using System.Management.Automation.Runspaces; + using Microsoft.Management.Configuration.Processor.DscModule; + using Microsoft.Management.Configuration.Processor.Runspaces; + using Microsoft.PowerShell; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Factory class to create a processor environment. + /// </summary> + internal class ProcessorEnvironmentFactory + { + private readonly ConfigurationProcessorType type; + + /// <summary> + /// Initializes a new instance of the <see cref="ProcessorEnvironmentFactory"/> class. + /// </summary> + /// <param name="type">Configuration processor type.</param> + public ProcessorEnvironmentFactory(ConfigurationProcessorType type) + { + this.type = type; + } + + /// <summary> + /// Create process environment. + /// </summary> + /// <returns>IProcessorEnvironment.</returns> + public IProcessorEnvironment CreateEnvironment() + { + return this.CreateProcessorEnvironment(); + } + + private IProcessorEnvironment CreateProcessorEnvironment() + { + IDscModule dscModule = new DscModuleV2(); + + if (this.type == ConfigurationProcessorType.Default) + { + throw new NotImplementedException(); + } + else if (this.type == ConfigurationProcessorType.Hosted) + { + InitialSessionState initialSessionState = InitialSessionState.CreateDefault(); + + // If this call fails importing the module, it won't throw but write to the error output. DSCModule is + // in charge of verifying that it got loaded correctly and if not, to install it. Once logging is implemented + // we should log the Error PSVariable. + initialSessionState.ImportPSModule(new List<ModuleSpecification>() + { + dscModule.ModuleSpecification, + }); + + // This is where our policy will get translated to PowerShell's execution policy. + initialSessionState.ExecutionPolicy = ExecutionPolicy.Unrestricted; + + // The $PSHome\Modules directory is added by default in the modules path. Because this is a hosted PowerShell, + // we don't have all the nice things that PowerShell installs by default. This includes PowerShellGet. + var runspace = RunspaceFactory.CreateRunspace(initialSessionState); + runspace.Open(); + + return new HostedEnvironment(runspace, this.type, dscModule); + } + + throw new ArgumentException(this.type.ToString()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Properties/AssemblyInfo.cs b/src/Microsoft.Management.Configuration.Processor/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +// ----------------------------------------------------------------------------- +// <copyright file="AssemblyInfo.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +using System.Runtime.CompilerServices; + +// InternalsVisibleTo specifies that types that are ordinarily visible only within the current +// assembly are visible to a specified assembly. This is only for types and members with internal +// or private protected scope, NOT private. Add any test dll that requires access to internal members. +[assembly: InternalsVisibleTo("Microsoft.Management.Configuration.UnitTests")] + +// Needed to allow us mock internal interfaces. +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorFactoryProperties.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorFactoryProperties.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationProcessorFactoryProperties.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + using System.Collections.Generic; + + /// <summary> + /// Implementation of <see cref="IConfigurationProcessorFactoryProperties"/>. + /// </summary> + public sealed class ConfigurationProcessorFactoryProperties : IConfigurationProcessorFactoryProperties + { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationProcessorFactoryProperties"/> class. + /// </summary> + public ConfigurationProcessorFactoryProperties() + { + } + + /// <inheritdoc/> + public IReadOnlyList<string>? AdditionalModulePaths { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorType.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationProcessorType.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationProcessorType.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + /// <summary> + /// Configuration processor runspace type. + /// </summary> + public enum ConfigurationProcessorType + { + /// <summary> + /// Uses default runspace. Requires to be running in PowerShell. Uses current runspace. + /// </summary> + Default, + + /// <summary> + /// Creates a new runspace in a hosted environment. + /// </summary> + Hosted, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/ConfigurationSetProcessorFactory.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationSetProcessorFactory.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.Management.Configuration.Processor.Set; + + /// <summary> + /// ConfigurationProcessorFactory implementation. Does not support out-of-proc. + /// TODO: change to IConfigurationSetProcessorFactory. + /// </summary> + public sealed class ConfigurationSetProcessorFactory : IConfigurationProcessorFactory + { + private readonly ConfigurationProcessorType type; + private readonly IConfigurationProcessorFactoryProperties? properties; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationSetProcessorFactory"/> class. + /// </summary> + /// <param name="type">Type.</param> + /// <param name="properties">Properties.</param> + public ConfigurationSetProcessorFactory(ConfigurationProcessorType type, IConfigurationProcessorFactoryProperties? properties) + { + this.type = type; + this.properties = properties; + } + + /// <summary> + /// Gets the configuration unit processor details for the given unit. + /// </summary> + /// <param name="set">Configuration Set.</param> + /// <returns>Configuration set processor.</returns> + public IConfigurationSetProcessor CreateSetProcessor(ConfigurationSet set) + { + var envFactory = new ProcessorEnvironmentFactory(this.type); + var processorEnvironment = envFactory.CreateEnvironment(); + processorEnvironment.ValidateRunspace(); + + if (this.properties is not null) + { + var additionalPsModulePaths = this.properties.AdditionalModulePaths; + if (additionalPsModulePaths is not null) + { + processorEnvironment.PrependPSModulePaths(additionalPsModulePaths); + } + } + + return new ConfigurationSetProcessor(processorEnvironment, set); + } + } +}+ \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration.Processor/Public/IConfigurationProcessorFactoryProperties.cs b/src/Microsoft.Management.Configuration.Processor/Public/IConfigurationProcessorFactoryProperties.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IConfigurationProcessorFactoryProperties.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + using System.Collections.Generic; + + /// <summary> + /// Properties for the configuration processor factory. + /// </summary> + public interface IConfigurationProcessorFactoryProperties + { + /// <summary> + /// Gets or sets the additional module paths. + /// </summary> + IReadOnlyList<string>? AdditionalModulePaths { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Set/ConfigurationSetProcessor.cs @@ -0,0 +1,247 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationSetProcessor.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Set +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.Management.Configuration.Processor.Unit; + using Windows.Security.Cryptography.Certificates; + + /// <summary> + /// Configuration set processor. + /// </summary> + internal sealed class ConfigurationSetProcessor : IConfigurationSetProcessor + { + private readonly ConfigurationSet configurationSet; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationSetProcessor"/> class. + /// </summary> + /// <param name="processorEnvironment">The processor environment.</param> + /// <param name="configurationSet">Configuration set.</param> + public ConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, ConfigurationSet configurationSet) + { + this.ProcessorEnvironment = processorEnvironment; + this.configurationSet = configurationSet; + } + + /// <summary> + /// Gets the processor environment. + /// </summary> + internal IProcessorEnvironment ProcessorEnvironment { get; } + + /// <summary> + /// Creates a configuration unit processor for the given unit. + /// </summary> + /// <param name="unit">Configuration unit.</param> + /// <param name="directivesOverlay">Allows for the ConfigurationProcessor to alter behavior without needing to change the unit itself.</param> + /// <returns>A configuration unit processor.</returns> + public IConfigurationUnitProcessor CreateUnitProcessor( + ConfigurationUnit unit, + IReadOnlyDictionary<string, object>? directivesOverlay) + { + var configurationUnitInternal = new ConfigurationUnitInternal(unit, directivesOverlay); + + var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); + + return new ConfigurationUnitProcessor( + this.ProcessorEnvironment, + new ConfigurationUnitAndResource(configurationUnitInternal, dscResourceInfo)); + } + + /// <summary> + /// Gets the configuration unit processor details for the given unit. + /// </summary> + /// <param name="unit">Configuration unit.</param> + /// <param name="detailLevel">Detail level.</param> + /// <returns>Configuration unit processor details.</returns> + public IConfigurationUnitProcessorDetails? GetUnitProcessorDetails( + ConfigurationUnit unit, + ConfigurationUnitDetailLevel detailLevel) + { + var unitInternal = new ConfigurationUnitInternal(unit); + var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); + + if (dscResourceInfo is not null) + { + return this.GetUnitProcessorDetailsLocal( + unit.UnitName, + dscResourceInfo, + detailLevel == ConfigurationUnitDetailLevel.Load); + } + + if (detailLevel == ConfigurationUnitDetailLevel.Local) + { + // Not found locally. + return null; + } + + var getFindResource = this.ProcessorEnvironment.FindDscResource(unitInternal); + if (getFindResource is null) + { + // Not found in catalog. + return null; + } + + // Hopefully they will never change the properties name. If someone can explain to me + // why assign it Name to $_ in Find-DscResource turns into a string in PowerShell but + // into a PSObject here that would be nice... + dynamic findResource = getFindResource; + string findResourceName = findResource.Name.ToString(); + + if (detailLevel == ConfigurationUnitDetailLevel.Catalog) + { + return new ConfigurationUnitProcessorDetails( + findResourceName, + null, + null, + findResource.PSGetModuleInfo, + null); + } + + if (detailLevel == ConfigurationUnitDetailLevel.Download) + { + var tempSavePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempSavePath); + this.ProcessorEnvironment.SaveModule(getFindResource, tempSavePath); + + var moduleInfo = this.ProcessorEnvironment.GetAvailableModule( + Path.Combine(tempSavePath, findResource.PSGetModuleInfo.Name)); + + return new ConfigurationUnitProcessorDetails( + findResourceName, + null, + moduleInfo, + findResource.PSGetModuleInfo, + this.GetCertificates(moduleInfo)); + } + + if (detailLevel == ConfigurationUnitDetailLevel.Load) + { + this.ProcessorEnvironment.InstallModule(getFindResource); + + dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); + + if (dscResourceInfo is null) + { + // Well, this is awkward. + throw new InstallDscResourceException( + unit.UnitName, + PowerShellHelpers.CreateModuleSpecification(findResource.ModuleName, findResource.Version)); + } + + return this.GetUnitProcessorDetailsLocal(unit.UnitName, dscResourceInfo, true); + } + + return null; + } + + private DscResourceInfoInternal PrepareUnitForProcessing(ConfigurationUnitInternal unitInternal) + { + // Invoke-DscResource makes a call to Get-DscResource which looks at the entire PSModulePath + // to see if a resource exists. DscResourcesMap is an attempt to try to optimize Get-DscResource + // by making just one call and get all of them, but it doesn't support minVersion and maxVersion. + // For now, lets make PowerShell fully figure out which module to use and try to optimize it later. + // This class will have a private member Lazy<DscResourcesMap> which will be initialized by calling + // this.ProcessorEnvironment.GetAllDscResources() + // To improve the performance even more, we will still need Invoke-DscResource to be update to + // get a DSC resource info object instead of calling Get-DscResource every time. + var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); + + if (dscResourceInfo is null) + { + var findDscResourceResult = this.ProcessorEnvironment.FindDscResource(unitInternal); + + if (findDscResourceResult is null) + { + throw new FindDscResourceNotFoundException(unitInternal.Unit.UnitName); + } + + this.ProcessorEnvironment.InstallModule(findDscResourceResult); + + // Now we should find it. + dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); + if (dscResourceInfo is null) + { + throw new InstallDscResourceException(unitInternal.Unit.UnitName, unitInternal.Module); + } + } + + return dscResourceInfo; + } + + private ConfigurationUnitProcessorDetails GetUnitProcessorDetailsLocal( + string unitName, + DscResourceInfoInternal dscResourceInfo, + bool importModule) + { + // I'm looking at you resources under C:\WINDOWS\system32\WindowsPowershell + if (dscResourceInfo.ModuleName is null || + dscResourceInfo.Version is null) + { + return new ConfigurationUnitProcessorDetails( + dscResourceInfo.Name, + dscResourceInfo, + null, + null, + null); + } + + var module = PowerShellHelpers.CreateModuleSpecification( + dscResourceInfo.ModuleName, + dscResourceInfo.Version.ToString()); + + // Get-InstalledModule only works for modules installed via PowerShell-Get. + // There are some properties that can only be obtain by that it so is better to take both. + var moduleInfo = this.ProcessorEnvironment.GetAvailableModule(module); + var installedModule = this.ProcessorEnvironment.GetInstalledModule(module); + + if (importModule) + { + this.ProcessorEnvironment.ImportModule(module); + } + + return new ConfigurationUnitProcessorDetails( + dscResourceInfo.Name, + dscResourceInfo, + moduleInfo, + installedModule, + this.GetCertificates(moduleInfo)); + } + + private List<Certificate>? GetCertificates(PSModuleInfo? moduleInfo) + { + if (moduleInfo is null) + { + return null; + } + + // TODO: we still need to investigate more here, but lets start with something. + var paths = new List<string>(); + + var psdPath = Path.Combine(moduleInfo.ModuleBase, $"{moduleInfo.Name}.psd1"); + if (File.Exists(psdPath)) + { + paths.Add(psdPath); + } + + var psmPath = Path.Combine(moduleInfo.ModuleBase, $"{moduleInfo.Name}.psm1"); + if (File.Exists(psmPath)) + { + paths.Add(psmPath); + } + + return this.ProcessorEnvironment.GetCertsOfValidSignedFiles(paths.ToArray()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessor.cs @@ -0,0 +1,153 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitProcessor.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Unit +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.PowerShell.Commands; + + /// <summary> + /// Provides access to a specific configuration unit within the runtime. + /// </summary> + internal sealed class ConfigurationUnitProcessor : IConfigurationUnitProcessor + { + private readonly IProcessorEnvironment processorEnvironment; + private readonly ConfigurationUnitAndResource unitResource; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitProcessor"/> class. + /// </summary> + /// <param name="processorEnvironment">Processor environment.</param> + /// <param name="unitResource">UnitResource.</param> + internal ConfigurationUnitProcessor( + IProcessorEnvironment processorEnvironment, + ConfigurationUnitAndResource unitResource) + { + this.processorEnvironment = processorEnvironment; + this.unitResource = unitResource; + } + + /// <summary> + /// Gets the configuration unit that the processor was created for. + /// </summary> + public ConfigurationUnit Unit => this.unitResource.Unit; + + /// <summary> + /// Gets the directives overlay that the processor was created with. + /// </summary> + public IReadOnlyDictionary<string, object>? DirectivesOverlay => this.unitResource.DirectivesOverlay; + + /// <summary> + /// Gets the current system state for the configuration unit. + /// Calls Get on the DSC resource. + /// </summary> + /// <returns>A <see cref="GetSettingsResult"/>.</returns> + public GetSettingsResult GetSettings() + { + var result = new GetSettingsResult(); + try + { + result.Settings = this.processorEnvironment.InvokeGetResource( + this.unitResource.GetExpandedSettings(), + this.unitResource.ResourceName, + this.unitResource.Module); + } + catch (Exception e) when (e is RuntimeException || + e is WriteErrorException) + { + var inner = e.GetMostInnerException(); + result.ResultInformation.ResultCode = inner; + result.ResultInformation.Description = e.ToString(); + } + catch (Exception) + { + throw; + } + + return result; + } + + /// <summary> + /// Determines if the system is already in the state described by the configuration unit. + /// Calls Test on the DSC resource. + /// </summary> + /// <returns>A <see cref="TestSettingsResult"/>.</returns> + public TestSettingsResult TestSettings() + { + if (this.Unit.Intent == ConfigurationUnitIntent.Inform) + { + throw new NotSupportedException(); + } + + var result = new TestSettingsResult(); + result.TestResult = ConfigurationTestResult.Failed; + try + { + bool testResult = this.processorEnvironment.InvokeTestResource( + this.unitResource.GetExpandedSettings(), + this.unitResource.ResourceName, + this.unitResource.Module); + + result.TestResult = testResult ? ConfigurationTestResult.Positive : ConfigurationTestResult.Negative; + } + catch (Exception e) when (e is RuntimeException || + e is WriteErrorException) + { + var inner = e.GetMostInnerException(); + result.ResultInformation.ResultCode = inner; + result.ResultInformation.Description = e.ToString(); + } + catch (Exception) + { + throw; + } + + return result; + } + + /// <summary> + /// Applies the state described in the configuration unit. + /// Calls Set in the DSC resource. + /// </summary> + /// <returns>A <see cref="ApplySettingsResult"/>.</returns> + public ApplySettingsResult ApplySettings() + { + if (this.Unit.Intent == ConfigurationUnitIntent.Inform || + this.Unit.Intent == ConfigurationUnitIntent.Assert) + { + throw new NotSupportedException(); + } + + var result = new ApplySettingsResult(); + try + { + result.RebootRequired = this.processorEnvironment.InvokeSetResource( + this.unitResource.GetExpandedSettings(), + this.unitResource.ResourceName, + this.unitResource.Module); + } + catch (Exception e) when (e is RuntimeException || + e is WriteErrorException) + { + var inner = e.GetMostInnerException(); + result.ResultInformation.ResultCode = inner; + result.ResultInformation.Description = e.ToString(); + } + catch (Exception) + { + throw; + } + + return result; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs @@ -0,0 +1,233 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitProcessorDetails.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Unit +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using System.Reflection; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Windows.Security.Cryptography.Certificates; + + /// <summary> + /// Provides information for a specific configuration unit within the runtime. + /// </summary> + internal sealed class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails + { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitProcessorDetails"/> class. + /// </summary> + /// <param name="unitName">Unit name.</param> + /// <param name="dscResourceInfo">DSC Resource Info.</param> + /// <param name="psModuleInfo">PSModuleInfo.</param> + /// <param name="getModuleInfo">GetModuleInfo.</param> + /// <param name="certs">List of certificates..</param> + public ConfigurationUnitProcessorDetails( + string unitName, + DscResourceInfoInternal? dscResourceInfo, + PSModuleInfo? psModuleInfo, + PSObject? getModuleInfo, + List<Certificate>? certs) + { + if (dscResourceInfo is null && + psModuleInfo is null && + getModuleInfo is null) + { + throw new ArgumentException(); + } + + this.UnitName = unitName; + + if (dscResourceInfo is not null) + { + this.IsLocal = true; + + var settings = new List<IConfigurationUnitSettingDetails>(); + foreach (var properties in dscResourceInfo.Properties) + { + settings.Add(new ConfigurationUnitSettingDetails(properties)); + } + + this.Settings = settings; + + if (psModuleInfo is null) + { + this.ModuleName = dscResourceInfo.ModuleName; + this.Version = dscResourceInfo.Version is not null ? dscResourceInfo.Version.ToString() : null; + } + } + + if (psModuleInfo is not null) + { + this.ModuleDocumentationUri = psModuleInfo.HelpInfoUri is not null ? new Uri(psModuleInfo.HelpInfoUri) : null; + this.UnitIconUri = psModuleInfo.IconUri; + this.ModuleName = psModuleInfo.Name; + this.ModuleType = psModuleInfo.ModuleType.ToString(); + this.ModuleDescription = psModuleInfo.Description; + this.PublishedModuleUri = psModuleInfo.ProjectUri; + this.Version = psModuleInfo.Version.ToString(); + this.Author = psModuleInfo.Author; + this.Publisher = psModuleInfo.CompanyName; + } + + if (getModuleInfo is not null) + { + this.TrySetPropertyAsString(getModuleInfo, "Repository", nameof(this.ModuleSource)); + this.TrySetPropertyFromDateTimeToDateTimeOffset(getModuleInfo, "PublishedDate", nameof(this.PublishedDate)); + + if (psModuleInfo is null) + { + // Type is not the same as this PSModuleType. + this.TrySetPropertyAsUri(getModuleInfo, "IconUri", nameof(this.UnitIconUri)); + this.TrySetPropertyAsString(getModuleInfo, "Name", nameof(this.ModuleName)); + this.TrySetPropertyAsString(getModuleInfo, "Description", nameof(this.ModuleDescription)); + this.TrySetPropertyAsString(getModuleInfo, "Version", nameof(this.Version)); + this.TrySetPropertyAsString(getModuleInfo, "Author", nameof(this.Author)); + this.TrySetPropertyAsString(getModuleInfo, "CompanyName", nameof(this.Publisher)); + } + } + + this.SigningInformation = certs; + } + + /// <summary> + /// Gets the name of the unit of configuration. + /// </summary> + public string UnitName { get; private set; } + + /// <summary> + /// Gets the description of the unit of configuration. + /// </summary> + public string? UnitDescription { get; private set; } + + /// <summary> + /// Gets the URI of the documentation for the unit of configuration. + /// </summary> + public Uri? UnitDocumentationUri { get; private set; } + + /// <summary> + /// Gets the URI of the icon for the unit of configuration. + /// </summary> + public Uri? UnitIconUri { get; private set; } + + /// <summary> + /// Gets the name of the module containing the unit of configuration. + /// </summary> + public string? ModuleName { get; private set; } + + /// <summary> + /// Gets the type of the module containing the unit of configuration. + /// </summary> + public string? ModuleType { get; private set; } + + /// <summary> + /// Gets the source of the module containing the unit of configuration. + /// </summary> + public string? ModuleSource { get; private set; } + + /// <summary> + /// Gets the description of the module containing the unit of configuration. + /// </summary> + public string? ModuleDescription { get; private set; } + + /// <summary> + /// Gets the URI of the documentation for the module containing the unit of configuration. + /// </summary> + public Uri? ModuleDocumentationUri { get; private set; } + + /// <summary> + /// Gets the URI for the published module containing the unit of configuration. + /// </summary> + public Uri? PublishedModuleUri { get; private set; } + + /// <summary> + /// Gets the version of the module containing the unit of configuration. + /// </summary> + public string? Version { get; private set; } + + /// <summary> + /// Gets the publishing date of the module containing the unit of configuration. + /// </summary> + public DateTimeOffset PublishedDate { get; private set; } + + /// <summary> + /// Gets a value indicating whether the module is already present on the system. + /// </summary> + public bool IsLocal { get; private set; } + + /// <summary> + /// Gets the author of the module containing the unit of configuration. + /// </summary> + public string? Author { get; private set; } + + /// <summary> + /// Gets the publisher of the module containing the unit of configuration. + /// </summary> + public string? Publisher { get; private set; } + + /// <summary> + /// Gets the signing certificate of the module files containing the unit of configuration. + /// </summary> + public IReadOnlyList<object>? SigningInformation { get; private set; } + + /// <summary> + /// Gets the settings information for the unit of configuration. + /// </summary> + public IReadOnlyList<IConfigurationUnitSettingDetails>? Settings { get; } + + private void TrySetPropertyAsString(PSObject getModuleInfo, string getModuleInfoProperty, string propertyName) + { + Type meType = typeof(ConfigurationUnitProcessorDetails); + PropertyInfo? propertyInfo = meType.GetProperty(propertyName); + if (propertyInfo is not null) + { + var moduleProperty = getModuleInfo.Properties[getModuleInfoProperty]; + if (moduleProperty is not null) + { + propertyInfo.SetValue(this, moduleProperty.Value as string); + } + else + { + propertyInfo.SetValue(this, null); + } + } + } + + private void TrySetPropertyAsUri(PSObject getModuleInfo, string getModuleInfoProperty, string propertyName) + { + Type meType = typeof(ConfigurationUnitProcessorDetails); + PropertyInfo? propertyInfo = meType.GetProperty(propertyName); + if (propertyInfo is not null) + { + var moduleProperty = getModuleInfo.Properties[getModuleInfoProperty]; + if (moduleProperty is not null) + { + string? modulePropertyString = moduleProperty.Value as string; + if (modulePropertyString is not null) + { + propertyInfo.SetValue(this, new Uri(modulePropertyString)); + } + } + } + } + + private void TrySetPropertyFromDateTimeToDateTimeOffset(PSObject getModuleInfo, string getModuleInfoProperty, string propertyName) + { + Type meType = typeof(ConfigurationUnitProcessorDetails); + PropertyInfo? propertyInfo = meType.GetProperty(propertyName); + if (propertyInfo is not null) + { + var moduleProperty = getModuleInfo.Properties[getModuleInfoProperty]; + if (moduleProperty is not null) + { + propertyInfo.SetValue(this, new DateTimeOffset((DateTime)moduleProperty.Value)); + } + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitSettingDetails.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitSettingDetails.cs @@ -0,0 +1,70 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitSettingDetails.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Unit +{ + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + + /// <summary> + /// Provides information for a specific configuration unit setting. + /// </summary> + internal sealed class ConfigurationUnitSettingDetails : IConfigurationUnitSettingDetails + { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitSettingDetails"/> class. + /// </summary> + /// <param name="dscResourceInfo">DSC Resource info.</param> + public ConfigurationUnitSettingDetails(DscResourcePropertyInfoInternal dscResourceInfo) + { + this.Name = dscResourceInfo.Name; + this.IsRequired = dscResourceInfo.IsMandatory; + this.Type = dscResourceInfo.PropertyType; + this.Semantics = string.Join(", ", dscResourceInfo.Values.ToArray()); + + // We don't have this information right now. + this.Description = null; + this.IsKey = false; + this.IsInformational = false; + } + + /// <summary> + /// Gets the name of the setting. + /// </summary> + public string Name { get; } + + /// <summary> + /// Gets the description of the setting. + /// </summary> + public string? Description { get; } + + /// <summary> + /// Gets a value indicating whether the setting is a key. This is used to determine if different settings are in conflict. + /// </summary> + public bool IsKey { get; } + + /// <summary> + /// Gets a value indicating whether a non-empty value for the setting is required. + /// </summary> + public bool IsRequired { get; } + + /// <summary> + /// Gets a value indicating whether the setting should be serialized in order to be applied on another system. + /// </summary> + public bool IsInformational { get; } + + /// <summary> + /// Gets the data type for the value of this setting. + /// </summary> + public string Type { get; } + + /// <summary> + /// Gets the semantics to be used for this setting. The goal is to enable richer conflict detection and authoring + /// scenarios by having a deeper understanding of this value than "String". + /// </summary> + public string? Semantics { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs b/src/Microsoft.Management.Configuration.UnitTests/Fixtures/UnitTestFixture.cs @@ -6,7 +6,17 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures { + using System; + using System.IO; + using System.Management.Automation.Runspaces; + using System.Reflection; + using Microsoft.Management.Configuration.Processor; + using Microsoft.Management.Configuration.Processor.DscModule; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.Management.Configuration.Processor.Runspaces; + using Moq; using Xunit.Abstractions; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; /// <summary> /// Unit test fixture. @@ -20,11 +30,42 @@ namespace Microsoft.Management.Configuration.UnitTests.Fixtures public UnitTestFixture(IMessageSink messageSink) { this.MessageSink = messageSink; + + string assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? throw new ArgumentException(); + + this.TestModulesPath = Path.Combine(assemblyPath, "TestCollateral", "PowerShellModules"); + if (!Directory.Exists(this.TestModulesPath)) + { + throw new DirectoryNotFoundException(this.TestModulesPath); + } } /// <summary> /// Gets the message sink for the fixture. /// </summary> public IMessageSink MessageSink { get; private init; } + + /// <summary> + /// Gets the test module path. + /// </summary> + public string TestModulesPath { get; } + + /// <summary> + /// Creates a runspace adding the test module path. + /// </summary> + /// <returns>PowerShellRunspace.</returns> + internal IProcessorEnvironment PrepareTestProcessorEnvironment(bool validate = false) + { + var processorEnv = new ProcessorEnvironmentFactory(ConfigurationProcessorType.Hosted).CreateEnvironment(); + processorEnv.PrependPSModulePath(this.TestModulesPath); + + if (validate) + { + processorEnv.ValidateRunspace(); + } + + return processorEnv; + } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/FactSkipIfCI.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/FactSkipIfCI.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FactSkipIfCI.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using System; + using Xunit; + + /// <summary> + /// Skip fact tests if running in CI builds. + /// </summary> + public class FactSkipIfCI : FactAttribute + { + /// <summary> + /// Initializes a new instance of the <see cref="FactSkipIfCI"/> class. + /// </summary> + public FactSkipIfCI() + { + if (Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER") is not null) + { + this.Skip = "Skip test for CI builds"; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/PowerShellTestsConstants.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/PowerShellTestsConstants.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------------- +// <copyright file="PowerShellTestsConstants.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + /// <summary> + /// PowerShell related constants. + /// </summary> + internal static class PowerShellTestsConstants + { +#pragma warning disable SA1600 // ElementsMustBeDocumented + public static class TestModule + { + public const string SimpleTestResourceModuleName = "xSimpleTestResource"; + public const string SimpleFileResourceName = "SimpleFileResource"; + public const string SimpleTestResourceName = "SimpleTestResource"; + public const string SimpleTestResourceThrowsName = "SimpleTestResourceThrows"; + public const string SimpleTestResourceErrorName = "SimpleTestResourceError"; + public const string SimpleTestResourceManifestFileName = "xSimpleTestResource.psd1"; + public const string SimpleTestResourceVersion = "0.0.0.1"; + } +#pragma warning restore SA1600 // ElementsMustBeDocumented + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TempDirectory.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TempDirectory.cs @@ -0,0 +1,120 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TempDirectory.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using System; + using System.IO; + + /// <summary> + /// Creates a temporary directory in the user's temporary directory. + /// </summary> + internal class TempDirectory : IDisposable + { + private bool disposed = false; + private bool cleanup; + + /// <summary> + /// Initializes a new instance of the <see cref="TempDirectory"/> class. + /// </summary> + /// <param name="directoryName">Optional directory name. If null, creates a random directory name.</param> + /// <param name="deleteIfExists">Delete directory if already exists. Default true.</param> + /// <param name="cleanup">Deletes directory at disposing time. Default true.</param> + public TempDirectory( + string? directoryName = null, + bool deleteIfExists = true, + bool cleanup = true) + { + var path = Path.GetTempPath(); + + if (directoryName is null) + { + this.DirectoryName = Path.GetRandomFileName(); + } + else + { + this.DirectoryName = directoryName; + } + + this.FullDirectoryPath = Path.Combine(Path.GetTempPath(), this.DirectoryName); + + if (deleteIfExists && Directory.Exists(this.FullDirectoryPath)) + { + Directory.Delete(this.FullDirectoryPath, true); + } + + Directory.CreateDirectory(this.FullDirectoryPath); + this.cleanup = cleanup; + } + + /// <summary> + /// Gets the directory name. + /// </summary> + public string DirectoryName { get; } + + /// <summary> + /// Gets the full directory name. + /// </summary> + public string FullDirectoryPath { get; } + + /// <summary> + /// IDisposable.Dispose . + /// </summary> + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Copies all contents of a directory into this directory. + /// </summary> + /// <param name="sourceDir">Source directory.</param> + public void CopyDirectory(string sourceDir) + { + this.CopyDirectory(sourceDir, this.FullDirectoryPath); + } + + /// <summary> + /// Protected disposed. + /// </summary> + /// <param name="disposing">Disposing.</param> + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (this.cleanup && Directory.Exists(this.FullDirectoryPath)) + { + Directory.Delete(this.FullDirectoryPath, true); + } + + this.disposed = true; + } + } + + private void CopyDirectory(string sourceDir, string destinationDir) + { + var dir = new DirectoryInfo(sourceDir); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException(dir.FullName); + } + + Directory.CreateDirectory(destinationDir); + + foreach (FileInfo file in dir.GetFiles()) + { + file.CopyTo(Path.Combine(destinationDir, file.Name)); + } + + foreach (DirectoryInfo subDir in dir.GetDirectories()) + { + this.CopyDirectory(subDir.FullName, Path.Combine(destinationDir, subDir.Name)); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TempFile.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TempFile.cs @@ -0,0 +1,109 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TempFile.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using System; + using System.IO; + + /// <summary> + /// Creates a temporary file in the user's temporary directory. + /// </summary> + internal class TempFile : IDisposable + { + private bool disposed = false; + private bool cleanup; + + /// <summary> + /// Initializes a new instance of the <see cref="TempFile"/> class. + /// </summary> + /// <param name="fileName">Optional file name. If null, creates a random file name.</param> + /// <param name="deleteIfExists">Delete file if already exists. Default true.</param> + /// <param name="content">Optional content. If not null or empty, creates file and writes to it.</param> + /// <param name="cleanup">Deletes file at disposing time. Default true.</param> + public TempFile( + string? fileName = null, + bool deleteIfExists = true, + string? content = null, + bool cleanup = true) + { + if (fileName is null) + { + this.FileName = Path.GetRandomFileName(); + } + else + { + this.FileName = fileName; + } + + this.FullFileName = Path.Combine(Path.GetTempPath(), this.FileName); + + if (deleteIfExists && File.Exists(this.FullFileName)) + { + File.Delete(this.FullFileName); + } + + if (!string.IsNullOrWhiteSpace(content)) + { + this.CreateFile(content); + } + + this.cleanup = cleanup; + } + + /// <summary> + /// Gets the file name. + /// </summary> + public string FileName { get; } + + /// <summary> + /// Gets the full file name. + /// </summary> + public string FullFileName { get; } + + /// <summary> + /// IDisposable.Dispose . + /// </summary> + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Creates the file. + /// </summary> + /// <param name="content">Content.</param> + public void CreateFile(string? content = null) + { + if (content is null) + { + File.Create(this.FullFileName); + } + else + { + File.WriteAllText(this.FullFileName, content); + } + } + + /// <summary> + /// Protected disposed. + /// </summary> + /// <param name="disposing">Disposing.</param> + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (this.cleanup && File.Exists(this.FullFileName)) + { + File.Delete(this.FullFileName); + } + + this.disposed = true; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationUnitProcessorDetails.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestConfigurationUnitProcessorDetails.cs @@ -52,7 +52,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers public IReadOnlyList<IConfigurationUnitSettingDetails>? Settings { get; internal set; } - public CertificateChain? SigningCertificateChain { get; internal set; } + public IReadOnlyList<object>? SigningInformation { get; internal set; } public string? UnitDescription { get; internal set; } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TheorySkipIfCI.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TheorySkipIfCI.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TheorySkipIfCI.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using System; + using Xunit; + + /// <summary> + /// Skip theory test if running in CI builds. + /// </summary> + public sealed class TheorySkipIfCI : TheoryAttribute + { + /// <summary> + /// Initializes a new instance of the <see cref="TheorySkipIfCI"/> class. + /// </summary> + public TheorySkipIfCI() + { + if (Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER") is not null) + { + this.Skip = "Skip test for CI builds"; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Microsoft.Management.Configuration.UnitTests.csproj b/src/Microsoft.Management.Configuration.UnitTests/Microsoft.Management.Configuration.UnitTests.csproj @@ -8,6 +8,11 @@ <OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutputPath> </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)'=='Release'"> + <!-- See comments on Microsoft.Management.Configuration.Processor.csproj --> + <!-- <TreatWarningsAsErrors>true</TreatWarningsAsErrors> --> + </PropertyGroup> + <ItemGroup> <AdditionalFiles Include="..\stylecop.json" Link="stylecop.json" /> </ItemGroup> @@ -28,17 +33,30 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> + <PackageReference Include="Moq" Version="4.18.2" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Microsoft.Management.Configuration.Projection\Microsoft.Management.Configuration.Projection.csproj"> - <OutputItemType>Content</OutputItemType> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - <ReferenceOutputAssembly>True</ReferenceOutputAssembly> + <OutputItemType>Content</OutputItemType> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ReferenceOutputAssembly>True</ReferenceOutputAssembly> </ProjectReference> <ProjectReference Include="..\Microsoft.Management.Configuration\Microsoft.Management.Configuration.vcxproj"> <Private>True</Private> </ProjectReference> + <ProjectReference Include="..\Microsoft.Management.Configuration.Processor\Microsoft.Management.Configuration.Processor.csproj"> + <Private>True</Private> + </ProjectReference> + </ItemGroup> + + <ItemGroup> + <None Update="TestCollateral\PowerShellModules\xSimpleTestResource\xSimpleTestResource.psd1"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="TestCollateral\PowerShellModules\xSimpleTestResource\xSimpleTestResource.psm1"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> </ItemGroup> </Project> diff --git a/src/Microsoft.Management.Configuration.UnitTests/TestCollateral/PowerShellModules/xSimpleTestResource/xSimpleTestResource.psd1 b/src/Microsoft.Management.Configuration.UnitTests/TestCollateral/PowerShellModules/xSimpleTestResource/xSimpleTestResource.psd1 @@ -0,0 +1,40 @@ +# +# Module manifest for module 'xSimpleTestResource' +# +# Generated by: Luffytaro +# +# Generated on: 1/24/2023 +# + +@{ + +RootModule = 'xSimpleTestResource.psm1' +ModuleVersion = '0.0.0.1' +GUID = 'a0be43e8-ac22-4244-8efc-7263dfa50b8c' +CompatiblePSEditions = 'Core' +Author = 'Luffytaro' +CompanyName = 'Microsoft Corporation' +Copyright = '(c) Microsoft Corporation. All rights reserved.' +Description = 'PowerShell module with DSC resources for unit tests' +PowerShellVersion = '7.2' +FunctionsToExport = @() +CmdletsToExport = @() +DscResourcesToExport = @( + 'SimpleFileResource' + 'SimpleTestResource' + 'SimpleTestResourceThrows' + 'SimpleTestResourceError' +) +HelpInfoURI = 'https://www.contoso.com/help' + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + ProjectUri = 'https://github.com/microsoft/winget-cli' + IconUri = 'https://www.contoso.com/icons/icon.png' + } + +} + +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/TestCollateral/PowerShellModules/xSimpleTestResource/xSimpleTestResource.psm1 b/src/Microsoft.Management.Configuration.UnitTests/TestCollateral/PowerShellModules/xSimpleTestResource/xSimpleTestResource.psm1 @@ -0,0 +1,171 @@ +# Simple module with resources. + +enum Ensure +{ + Absent + Present +} + +# This resource just checks if a file is there or not with and if its with the specified content. +[DscResource()] +class SimpleFileResource +{ + [DscProperty(Key)] + [string] $Path + + [DscProperty()] + [Ensure] $Ensure = [Ensure]::Present + + [DscProperty()] + [string] $Content = $null + + [SimpleFileResource] Get() + { + if ([string]::IsNullOrEmpty($this.Path)) + { + throw + } + + $fileContent = $null + if (Test-Path -Path $this.Path -PathType Leaf) + { + $fileContent = Get-Content $this.Path -Raw + } + + $result = @{ + Path = $this.Path + Content = $fileContent + } + + return $result + } + + [bool] Test() + { + $get = $this.Get() + + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + return $this.Content -eq $get.Content + } + } + elseif ($this.Ensure -eq [Ensure]::Absent) + { + return $true + } + + return $false + } + + [void] Set() + { + if (-not $this.Test()) + { + if (Test-Path -Path $this.Path -PathType Leaf) + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + else + { + Remove-Item $this.Path + } + } + else + { + if ($this.Ensure -eq [Ensure]::Present) + { + Set-Content $this.Path $this.Content -NoNewline + } + } + } + } +} + +[DscResource()] +class SimpleTestResource +{ + [DscProperty(Key)] + [string] $key + + [DscProperty(Mandatory)] + [string] $secretCode + + [SimpleTestResource] Get() + { + $result = @{ + key = "SimpleTestResourceKey" + } + return $result + } + + [bool] Test() + { + return $this.secretCode -eq "4815162342" + } + + [void] Set() + { + if (-not $this.Test()) + { + $global:DSCMachineStatus = 1 + } + } +} + +[DscResource()] +class SimpleTestResourceThrows +{ + [DscProperty(Key)] + [string] $key + + [SimpleTestResourceThrows] Get() + { + $result = @{ + key = "SimpleTestResourceThrowsKey" + } + throw "throws in Get" + return $result + } + + [bool] Test() + { + throw "throws in Test" + return $false + } + + [void] Set() + { + throw "throws in Set" + } +} + +[DscResource()] +class SimpleTestResourceError +{ + [DscProperty(Key)] + [string] $key + + [SimpleTestResourceError] Get() + { + $result = @{ + key = "SimpleTestResourceErrorKey" + } + Write-Error "Error in Get" + return $result + } + + [bool] Test() + { + Write-Error "Error in Test" + return $true + } + + [void] Set() + { + Write-Error "Error in Set" + } +}+ \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationDetailsTests.cs @@ -0,0 +1,217 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationDetailsTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.Unit; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Windows.Security.Cryptography.Certificates; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Tests for ConfigurationUnitProcessorDetails and ConfigurationUnitSettingDetails. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationDetailsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationDetailsTests"/> class. + /// </summary> + public ConfigurationDetailsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests creating ConfigurationUnitProcessorDetails and ConfigurationUnitSettingDetails with different inputs. + /// </summary> + /// <param name="hasDscInfo">Has dsc info.</param> + /// <param name="hasPSModuleInfo">Has ps module info.</param> + /// <param name="hasGetModuleInfo">Has get module info.</param> + /// <param name="hasCerts">Has certs.</param> + [Theory] + [InlineData(false, false, false, false)] + [InlineData(false, false, false, true)] + [InlineData(false, false, true, false)] + [InlineData(false, false, true, true)] + [InlineData(false, true, false, false)] + [InlineData(false, true, false, true)] + [InlineData(false, true, true, false)] + [InlineData(false, true, true, true)] + [InlineData(true, false, false, false)] + [InlineData(true, false, false, true)] + [InlineData(true, false, true, false)] + [InlineData(true, false, true, true)] + [InlineData(true, true, false, false)] + [InlineData(true, true, false, true)] + [InlineData(true, true, true, false)] + [InlineData(true, true, true, true)] + public void ConfigurationUnitProcessorDetails_CreationTest(bool hasDscInfo, bool hasPSModuleInfo, bool hasGetModuleInfo, bool hasCerts) + { + List<Certificate>? certsInput = null; + if (hasCerts) + { + certsInput = new List<Certificate>(); + } + + if (!hasDscInfo && !hasPSModuleInfo && !hasGetModuleInfo) + { + Assert.Throws<ArgumentException>( + () => new ConfigurationUnitProcessorDetails("unitName", null, null, null, certsInput)); + } + else + { + var unit = this.CreteConfigurationUnit(); + var (dscResourceInfo, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + + DscResourceInfoInternal? dscResourceInfoInput = null; + if (hasDscInfo) + { + dscResourceInfoInput = dscResourceInfo; + } + + PSModuleInfo? psModuleInfoInput = null; + if (hasPSModuleInfo) + { + psModuleInfoInput = psModuleInfo; + } + + PSObject? getModuleInfo = null; + if (hasGetModuleInfo) + { + getModuleInfo = this.CreateGetModuleInfo(); + } + + var details = new ConfigurationUnitProcessorDetails(unit.UnitName, dscResourceInfoInput, psModuleInfoInput, getModuleInfo, certsInput); + + Assert.Equal(unit.UnitName, details.UnitName); + + if (hasDscInfo) + { + Assert.True(details.IsLocal); + Assert.Equal("xSimpleTestResource", details.ModuleName); + Assert.Equal("0.0.0.1", details.Version); + Assert.NotNull(details.Settings); + Assert.True(details.Settings.Count == 5); + + var pathSetting = details.Settings.Where(s => s.Name == "Path").FirstOrDefault(); + Assert.NotNull(pathSetting); + Assert.Equal("[string]", pathSetting.Type); + Assert.True(pathSetting.IsRequired); + Assert.Equal(string.Empty, pathSetting.Semantics); + + var contentSetting = details.Settings.Where(s => s.Name == "Content").FirstOrDefault(); + Assert.NotNull(contentSetting); + Assert.Equal("[string]", contentSetting.Type); + Assert.False(contentSetting.IsRequired); + Assert.Equal(string.Empty, contentSetting.Semantics); + + var dependsOnSetting = details.Settings.Where(s => s.Name == "DependsOn").FirstOrDefault(); + Assert.NotNull(dependsOnSetting); + Assert.Equal("[string[]]", dependsOnSetting.Type); + Assert.False(dependsOnSetting.IsRequired); + Assert.Equal(string.Empty, dependsOnSetting.Semantics); + + var ensureSetting = details.Settings.Where(s => s.Name == "Ensure").FirstOrDefault(); + Assert.NotNull(ensureSetting); + Assert.Equal("[string]", ensureSetting.Type); + Assert.False(ensureSetting.IsRequired); + Assert.Equal("Absent, Present", ensureSetting.Semantics); + + var psDscRunAsCredentialSetting = details.Settings.Where(s => s.Name == "PsDscRunAsCredential").FirstOrDefault(); + Assert.NotNull(psDscRunAsCredentialSetting); + Assert.Equal("[PSCredential]", psDscRunAsCredentialSetting.Type); + Assert.False(psDscRunAsCredentialSetting.IsRequired); + Assert.Equal(string.Empty, psDscRunAsCredentialSetting.Semantics); + } + + if (hasPSModuleInfo) + { + Assert.Equal(new Uri("https://www.contoso.com/help"), details.ModuleDocumentationUri); + Assert.Equal(new Uri("https://www.contoso.com/icons/icon.png"), details.UnitIconUri); + Assert.Equal("xSimpleTestResource", details.ModuleName); + Assert.Equal(ModuleType.Script.ToString(), details.ModuleType); + Assert.Equal("PowerShell module with DSC resources for unit tests", details.ModuleDescription); + Assert.Equal(new Uri("https://github.com/microsoft/winget-cli"), details.PublishedModuleUri); + Assert.Equal("0.0.0.1", details.Version); + Assert.Equal("Luffytaro", details.Author); + Assert.Equal("Microsoft Corporation", details.Publisher); + } + + if (hasGetModuleInfo) + { + Assert.Equal("PSGallery", details.ModuleSource); + Assert.Equal(new DateTimeOffset(new DateTime(2017, 12, 10)), details.PublishedDate); + Assert.Equal(new Uri("https://www.contoso.com/icons/icon.png"), details.UnitIconUri); + Assert.Equal("xSimpleTestResource", details.ModuleName); + Assert.Equal("PowerShell module with DSC resources for unit tests", details.ModuleDescription); + Assert.Equal("0.0.0.1", details.Version); + Assert.Equal("Luffytaro", details.Author); + Assert.Equal("Microsoft Corporation", details.Publisher); + } + + if (hasCerts) + { + Assert.NotNull(details.SigningInformation); + } + } + } + + private ConfigurationUnit CreteConfigurationUnit() + { + var unit = new ConfigurationUnit(); + unit.UnitName = "SimpleFileResource"; + unit.Directives.Add("module", "xSimpleTestResource"); + unit.Directives.Add("version", "0.0.0.1"); + + return unit; + } + + private (DscResourceInfoInternal dscResourceInfo, PSModuleInfo psModuleInfo) GetResourceAndModuleInfo(ConfigurationUnit unit) + { + // This is easier than trying to mock sealed class from external code... + var testEnv = this.fixture.PrepareTestProcessorEnvironment(); + testEnv.ValidateRunspace(); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); + + if (dscResourceInfo is null || psModuleInfo is null) + { + throw new ArgumentNullException("Test processor environment not set correctly"); + } + + return (dscResourceInfo, psModuleInfo); + } + + private PSObject CreateGetModuleInfo() + { + return new PSObject(new + { + Repository = "PSGallery", + PublishedDate = new DateTime(2017, 12, 10), + IconUri = "https://www.contoso.com/icons/icon.png", + Name = "xSimpleTestResource", + Description = "PowerShell module with DSC resources for unit tests", + RepositorySourceLocation = "https://github.com/microsoft/winget-cli", + Version = "0.0.0.1", + Author = "Luffytaro", + CompanyName = "Microsoft Corporation", + }); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorFactoryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorFactoryTests.cs @@ -0,0 +1,90 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationProcessorFactoryTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor; + using Microsoft.Management.Configuration.Processor.Set; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Moq; + using Xunit; + using Xunit.Abstractions; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// Tests ConfigurationProcessorFactory. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationProcessorFactoryTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationProcessorFactoryTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationProcessorFactoryTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// CreateSetProcessor test. + /// </summary> + [Fact] + public void CreateSetProcessor_Test() + { + var configurationProcessorFactory = new ConfigurationSetProcessorFactory( + ConfigurationProcessorType.Hosted, + null); + + var configurationSet = new ConfigurationSet(); + + var configurationProcessorSet = configurationProcessorFactory.CreateSetProcessor(configurationSet); + + Assert.NotNull(configurationProcessorSet); + Assert.IsType<ConfigurationSetProcessor>(configurationProcessorSet); + var processorSet = configurationProcessorSet as ConfigurationSetProcessor; + Assert.NotNull(processorSet); + } + + /// <summary> + /// CreateSetProcessor test. + /// </summary> + [Fact] + public void CreateSetProcessor_Properties_PsModulePath() + { + var configurationProcessorFactoryPropertiesMock = new Mock<IConfigurationProcessorFactoryProperties>(); + configurationProcessorFactoryPropertiesMock.Setup(c => c.AdditionalModulePaths) + .Returns(new List<string> + { + "ThisIsOnePath", + "ThisIsAnotherPath", + }); + + var configurationProcessorFactory = new ConfigurationSetProcessorFactory( + ConfigurationProcessorType.Hosted, + configurationProcessorFactoryPropertiesMock.Object); + + var configurationSet = new ConfigurationSet(); + + var configurationProcessorSet = configurationProcessorFactory.CreateSetProcessor(configurationSet); + + Assert.IsType<ConfigurationSetProcessor>(configurationProcessorSet); + var processorSet = configurationProcessorSet as ConfigurationSetProcessor; + Assert.NotNull(processorSet); + + configurationProcessorFactoryPropertiesMock.Verify(); + + var modulePath = processorSet.ProcessorEnvironment.GetVariable<string>(Variables.PSModulePath); + Assert.StartsWith("ThisIsOnePath;ThisIsAnotherPath", modulePath); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetProcessorTests.cs @@ -0,0 +1,698 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationSetProcessorTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.Management.Configuration.Processor.Set; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Microsoft.PowerShell.Commands; + using Moq; + using Windows.Security.Cryptography.Certificates; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Unit tests for configuration processor tests. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationSetProcessorTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationSetProcessorTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationSetProcessorTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Test CreateUnitProcessor. Happy path. + /// </summary> + [Fact] + public void CreateUnitProcessor_ResourceExists() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(new DscResourceInfoInternal(resourceName, moduleName, version)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + unit.Directives.Add("module", moduleName); + unit.Directives.Add("version", version.ToString()); + + var unitProcessor = configurationSetProcessor.CreateUnitProcessor(unit, null); + Assert.NotNull(unitProcessor); + Assert.Equal(unit.UnitName, unitProcessor.Unit.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test CreateUnitProcessor with no version directive. + /// </summary> + [Fact] + public void CreateUnitProcessor_ResourceExists_NoVersionDirective() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0.0.0"); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(new DscResourceInfoInternal(resourceName, moduleName, version)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + unit.Directives.Add("module", moduleName); + + var unitProcessor = configurationSetProcessor.CreateUnitProcessor(unit, null); + Assert.NotNull(unitProcessor); + Assert.Equal(unit.UnitName, unitProcessor.Unit.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test CreateUnitProcessor with no module directive. + /// </summary> + [Fact] + public void CreateUnitProcessor_ResourceExists_NoModuleDirective() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(new DscResourceInfoInternal(resourceName, moduleName, version)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + + var unitProcessor = configurationSetProcessor.CreateUnitProcessor(unit, null); + Assert.NotNull(unitProcessor); + Assert.Equal(unit.UnitName, unitProcessor.Unit.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Tests Creating a unit processor by downloading the resource. + /// </summary> + [Fact] + public void CreateUnitProcessor_InstallResource() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + DscResourceInfoInternal? nullResource = null; + DscResourceInfoInternal dscResourceInfo = new DscResourceInfoInternal(resourceName, moduleName, version); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.SetupSequence( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(nullResource) + .Returns(dscResourceInfo); + + PSObject findDscResourceResult = new PSObject(processorEnvMock); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(findDscResourceResult) + .Verifiable(); + + processorEnvMock.Setup( + m => m.InstallModule(findDscResourceResult)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + unit.Directives.Add("module", moduleName); + unit.Directives.Add("version", version.ToString()); + + var unitProcessor = configurationSetProcessor.CreateUnitProcessor(unit, null); + Assert.NotNull(unitProcessor); + Assert.Equal(unit.UnitName, unitProcessor.Unit.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Tests Creating a unit processor by downloading the resource. + /// </summary> + [Fact] + public void CreateUnitProcessor_InstallResource_NotFoundAfterInstall() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + DscResourceInfoInternal? nullResource = null; + DscResourceInfoInternal dscResourceInfo = new DscResourceInfoInternal(resourceName, moduleName, version); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(nullResource); + + PSObject findDscResourceResult = new PSObject(processorEnvMock); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(findDscResourceResult) + .Verifiable(); + + processorEnvMock.Setup( + m => m.InstallModule(findDscResourceResult)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + unit.Directives.Add("module", moduleName); + unit.Directives.Add("version", version.ToString()); + + Assert.Throws<InstallDscResourceException>( + () => configurationSetProcessor.CreateUnitProcessor(unit, null)); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Tests Creating a unit processor by downloading the resource. + /// </summary> + [Fact] + public void CreateUnitProcessor_InstallResource_NotFound() + { + string resourceName = "xResourceName"; + string moduleName = "xModuleName"; + Version version = new Version("1.0"); + + DscResourceInfoInternal? nullResource = null; + DscResourceInfoInternal dscResourceInfo = new DscResourceInfoInternal(resourceName, moduleName, version); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(nullResource); + + PSObject? findDscResourceResult = null; + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => c.Unit.UnitName == resourceName))) + .Returns(findDscResourceResult) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit + { + UnitName = resourceName, + }; + unit.Directives.Add("module", moduleName); + unit.Directives.Add("version", version.ToString()); + + Assert.Throws<FindDscResourceNotFoundException>( + () => configurationSetProcessor.CreateUnitProcessor(unit, null)); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test GetUnitProcessorDetails Local Resource not found. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Local_NoFound() + { + string resourceName = "xResource"; + DscResourceInfoInternal? nullDscInfoInternal = null; + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == resourceName))) + .Returns(nullDscInfoInternal) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var unit = new ConfigurationUnit() + { + UnitName = resourceName, + }; + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Local); + + Assert.Null(configurationUnitProcessorDetails); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test GetUnitProcessorDetails Local Found. Module not installed by PowerShellGet. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Local_NotInstalledByPowerShellGet() + { + var unit = this.CreteConfigurationUnit(); + var (dscResourceInfo, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + PSObject? nullPsModuleInfo = null; + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == dscResourceInfo.Name))) + .Returns(dscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetAvailableModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(psModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetInstalledModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(nullPsModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetCertsOfValidSignedFiles(It.IsAny<string[]>())) + .Returns(new List<Certificate>()) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Local); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal(dscResourceInfo.Name, configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test GetUnitProcessorDetails locally found. Do not include Load. + /// </summary> + /// <param name="detailLevel">Detail level.</param> + [Theory] + [InlineData(ConfigurationUnitDetailLevel.Local)] + [InlineData(ConfigurationUnitDetailLevel.Catalog)] + [InlineData(ConfigurationUnitDetailLevel.Download)] + public void GetUnitProcessorDetails_Local(ConfigurationUnitDetailLevel detailLevel) + { + var unit = this.CreteConfigurationUnit(); + var (dscResourceInfo, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + var getModuleInfo = this.CreateGetModuleInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == dscResourceInfo.Name))) + .Returns(dscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetAvailableModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(psModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetInstalledModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(getModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetCertsOfValidSignedFiles(It.IsAny<string[]>())) + .Returns(new List<Certificate>()) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + detailLevel); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal(dscResourceInfo.Name, configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + processorEnvMock.Verify(m => m.FindDscResource(It.IsAny<ConfigurationUnitInternal>()), Times.Never()); + processorEnvMock.Verify(m => m.ImportModule(It.IsAny<ModuleSpecification>()), Times.Never()); + } + + /// <summary> + /// Test GetUnitProcessorDetails locally found and load. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Local_Load() + { + var unit = this.CreteConfigurationUnit(); + var (dscResourceInfo, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + var getModuleInfo = this.CreateGetModuleInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == dscResourceInfo.Name))) + .Returns(dscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetAvailableModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(psModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetInstalledModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(getModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.ImportModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetCertsOfValidSignedFiles(It.IsAny<string[]>())) + .Returns(new List<Certificate>()) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Load); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal(dscResourceInfo.Name, configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + processorEnvMock.Verify(m => m.FindDscResource(It.IsAny<ConfigurationUnitInternal>()), Times.Never()); + } + + /// <summary> + /// Test GetUnitProcessorDetails Catalog Not Found. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Catalog_NotFound() + { + var unit = this.CreteConfigurationUnit(); + DscResourceInfoInternal? nullDscResourceInfo = null; + PSObject? nullPsModuleInfo = null; + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName))) + .Returns(nullDscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => unit.UnitName == unit.UnitName))) + .Returns(nullPsModuleInfo) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Catalog); + + Assert.Null(configurationUnitProcessorDetails); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test GetUnitProcessorDetails Catalog Found. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Catalog() + { + var unit = this.CreteConfigurationUnit(); + DscResourceInfoInternal? nullDscResourceInfo = null; + var getFindResourceInfo = this.CreateFindResourceInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName))) + .Returns(nullDscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => unit.UnitName == unit.UnitName))) + .Returns(getFindResourceInfo) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Catalog); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal("SimpleFileResource", configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Test GetUnitProcessorDetails downloading module. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Download() + { + var unit = this.CreteConfigurationUnit(); + DscResourceInfoInternal? nullDscResourceInfo = null; + var (_, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + var getFindResourceInfo = this.CreateFindResourceInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName))) + .Returns(nullDscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => unit.UnitName == unit.UnitName))) + .Returns(getFindResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.SaveModule(getFindResourceInfo, It.IsAny<string>())) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetAvailableModule(It.Is<string>(s => s.EndsWith("xSimpleTestResource")))) + .Returns(psModuleInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetCertsOfValidSignedFiles(It.IsAny<string[]>())) + .Returns(new List<Certificate>()) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Download); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal("SimpleFileResource", configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + + processorEnvMock.Verify(m => m.InstallModule(It.IsAny<PSObject>()), Times.Never()); + } + + /// <summary> + /// Tests GetUnitProcessorDetails install module, but resource not found anyway. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Load_NotFoundAfterInstall() + { + var unit = this.CreteConfigurationUnit(); + DscResourceInfoInternal? nullDscResourceInfo = null; + var (_, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + var getFindResourceInfo = this.CreateFindResourceInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName))) + .Returns(nullDscResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => unit.UnitName == unit.UnitName))) + .Returns(getFindResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.InstallModule(getFindResourceInfo)) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + Assert.Throws<InstallDscResourceException>(() => configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Load)); + + processorEnvMock.Verify(); + processorEnvMock.Verify( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName)), + Times.Exactly(2)); + } + + /// <summary> + /// Tests GetUnitProcessorDetails install module. + /// </summary> + [Fact] + public void GetUnitProcessorDetails_Load() + { + var unit = this.CreteConfigurationUnit(); + DscResourceInfoInternal? nullDscResourceInfo = null; + var (dscResourceInfo, psModuleInfo) = this.GetResourceAndModuleInfo(unit); + var getFindResourceInfo = this.CreateFindResourceInfo(); + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.SetupSequence( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName))) + .Returns(nullDscResourceInfo) + .Returns(dscResourceInfo); + processorEnvMock.Setup( + m => m.FindDscResource(It.Is<ConfigurationUnitInternal>(c => unit.UnitName == unit.UnitName))) + .Returns(getFindResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.InstallModule(getFindResourceInfo)) + .Verifiable(); + processorEnvMock.Setup( + m => m.GetInstalledModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Returns(getFindResourceInfo) + .Verifiable(); + processorEnvMock.Setup( + m => m.ImportModule(It.Is<ModuleSpecification>(s => s.Name == dscResourceInfo.ModuleName))) + .Verifiable(); + + var configurationSetProcessor = new ConfigurationSetProcessor( + processorEnvMock.Object, + new ConfigurationSet()); + + var configurationUnitProcessorDetails = configurationSetProcessor.GetUnitProcessorDetails( + unit, + ConfigurationUnitDetailLevel.Load); + + Assert.NotNull(configurationUnitProcessorDetails); + Assert.Equal(dscResourceInfo.Name, configurationUnitProcessorDetails.UnitName); + + processorEnvMock.Verify(); + processorEnvMock.Verify( + m => m.GetDscResource(It.Is<ConfigurationUnitInternal>(u => u.Unit.UnitName == unit.UnitName)), + Times.Exactly(2)); + } + + private ConfigurationUnit CreteConfigurationUnit() + { + var unit = new ConfigurationUnit(); + unit.UnitName = "SimpleFileResource"; + unit.Directives.Add("module", "xSimpleTestResource"); + unit.Directives.Add("version", "0.0.0.1"); + + return unit; + } + + private (DscResourceInfoInternal dscResourceInfo, PSModuleInfo psModuleInfo) GetResourceAndModuleInfo(ConfigurationUnit unit) + { + // This is easier than trying to mock sealed class from external code... + var testEnv = this.fixture.PrepareTestProcessorEnvironment(true); + var dscResourceInfo = testEnv.GetDscResource(new ConfigurationUnitInternal(unit, null)); + var psModuleInfo = testEnv.GetAvailableModule(PowerShellHelpers.CreateModuleSpecification("xSimpleTestResource", "0.0.0.1")); + + if (dscResourceInfo is null || psModuleInfo is null) + { + throw new ArgumentNullException("Test processor environment not set correctly"); + } + + return (dscResourceInfo, psModuleInfo); + } + + private PSObject CreateGetModuleInfo() + { + return new PSObject(new + { + Repository = "PSGallery", + PublishedDate = new DateTime(2017, 12, 10), + IconUri = "https://github.com/microsoft/winget-cli", + Name = "xSimpleTestResource", + Description = "PowerShell module with DSC resources for unit tests", + RepositorySourceLocation = "https://github.com/microsoft/winget-cli", + Version = "0.0.0.1", + Author = "Luffytaro", + CompanyName = "Microsoft Corporation", + }); + } + + private PSObject CreateFindResourceInfo() + { + var getModuleInfo = this.CreateGetModuleInfo(); + return new PSObject(new + { + Name = "SimpleFileResource", + ModuleName = "xSimpleTestResource", + Version = "0.0.0.1", + PSGetModuleInfo = getModuleInfo, + }); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitInternalTests.cs @@ -0,0 +1,101 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitInternalTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Tests ConfigurationUnitExtensionsTests. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationUnitInternalTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitInternalTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationUnitInternalTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Test GetDirectives. + /// </summary> + [Fact] + public void GetDirectivesTest() + { + string moduleDirective = "module"; + string unitModule = "xModule"; + + string versionDirective = "version"; + string unitVersion = "1.0.0.0"; + + string descriptionDirective = "description"; + string unitDescription = "beep beep boop i am a text"; + string overlayDescription = "beep beep boop i am an overlay text"; + + string anotherDirective = "another"; + string overlayAnother = "insert another text"; + + var unit = new ConfigurationUnit(); + unit.Directives.Add(moduleDirective, unitModule); + unit.Directives.Add(versionDirective, unitVersion); + unit.Directives.Add(descriptionDirective, unitDescription); + + var overlays = new Dictionary<string, object>() + { + { descriptionDirective, overlayDescription }, + { anotherDirective, overlayAnother }, + }; + + var unitInternal = new ConfigurationUnitInternal(unit, overlays); + + var description = unitInternal.GetDirective(descriptionDirective); + Assert.Equal(description, overlayDescription); + + var another = unitInternal.GetDirective(anotherDirective); + Assert.Equal(another, overlayAnother); + + var fake = unitInternal.GetDirective("fake"); + Assert.Null(fake); + + var description2 = unitInternal.GetDirective("DESCRIPTION"); + Assert.Equal(description2, overlayDescription); + + Assert.Equal(unitModule, unitInternal.Module!.Name); + + Assert.Equal(Version.Parse(unitVersion), unitInternal.Module!.RequiredVersion); + } + + /// <summary> + /// Tests GetVersion with a bad version. + /// </summary> + [Fact] + public void GetVersion_BadVersion() + { + var unit = new ConfigurationUnit(); + unit.Directives.Add("module", "module"); + unit.Directives.Add("version", "not a version"); + + Assert.Throws<PSInvalidCastException>( + () => new ConfigurationUnitInternal(unit, null)); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationUnitProcessorTests.cs @@ -0,0 +1,432 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationUnitProcessorTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using System.Management.Automation; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.ProcessorEnvironments; + using Microsoft.Management.Configuration.Processor.Unit; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.PowerShell.Commands; + using Moq; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Configuration unit processor tests. + /// </summary> + [Collection("UnitTestCollection")] + public class ConfigurationUnitProcessorTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationUnitProcessorTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationUnitProcessorTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Call GetSettings with all intents should succeed. + /// </summary> + /// <param name="intent">Intent.</param> + [Theory] + [InlineData(ConfigurationUnitIntent.Inform)] + [InlineData(ConfigurationUnitIntent.Assert)] + [InlineData(ConfigurationUnitIntent.Apply)] + public void GetSettings_Test(ConfigurationUnitIntent intent) + { + string theKey = "key"; + string theValue = "value"; + var valueGetResult = new ValueSet + { + { theKey, theValue }, + }; + + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeGetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Returns(valueGetResult) + .Verifiable(); + + var unitResource = this.CreateUnitResource(intent); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.GetSettings(); + + processorEnvMock.Verify(); + + Assert.True(result.Settings.Count == 1); + Assert.True(result.Settings.ContainsKey(theKey)); + Assert.True(result.Settings.TryGetValue(theKey, out object keyValue)); + Assert.Equal(theValue, keyValue as string); + } + + /// <summary> + /// Tests GetSettings when a System.Management.Automation.RuntimeException is thrown. + /// </summary> + [Fact] + public void GetSettings_Throws_Pwsh_RuntimeException() + { + var thrownException = new RuntimeException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeGetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Inform); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.GetSettings(); + + processorEnvMock.Verify(); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests GetSettings when a Microsoft.PowerShell.Commands.WriteErrorException is thrown. + /// </summary> + [Fact] + public void GetSettings_Throws_Pwsh_WriteErrorException() + { + var thrownException = new WriteErrorException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeGetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Inform); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.GetSettings(); + + processorEnvMock.Verify(); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests GetSettings when a non specialized catched exception is thrown. + /// </summary> + [Fact] + public void GetSettings_Throws() + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeGetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => new ArgumentNullException("a message")) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Inform); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + Assert.Throws<ArgumentNullException>(() => unitProcessor.GetSettings()); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Call TestSettings with Inform intent is not allowed. + /// </summary> + [Fact] + public void TestSettings_InformIntent() + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Inform); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + Assert.Throws<NotSupportedException>(() => unitProcessor.TestSettings()); + } + + /// <summary> + /// Call TestSettings with Assert and Apply should work. + /// </summary> + /// <param name="intent">Intent.</param> + /// <param name="invokeTestResult">Invoke test result.</param> + [Theory] + [InlineData(ConfigurationUnitIntent.Assert, false)] + [InlineData(ConfigurationUnitIntent.Apply, false)] + [InlineData(ConfigurationUnitIntent.Assert, true)] + [InlineData(ConfigurationUnitIntent.Apply, true)] + public void TestSettings_TestSucceeded(ConfigurationUnitIntent intent, bool invokeTestResult) + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeTestResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Returns(invokeTestResult) + .Verifiable(); + + var unitResource = this.CreateUnitResource(intent); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var testResult = unitProcessor.TestSettings(); + + processorEnvMock.Verify(); + + var expectedConfigTestResult = ConfigurationTestResult.Negative; + if (invokeTestResult) + { + expectedConfigTestResult = ConfigurationTestResult.Positive; + } + + Assert.Equal(expectedConfigTestResult, testResult.TestResult); + } + + /// <summary> + /// Tests TestSettings when a System.Management.Automation.RuntimeException is thrown. + /// </summary> + [Fact] + public void TestSettings_Throws_Pwsh_RuntimeException() + { + var thrownException = new RuntimeException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeTestResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Assert); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.TestSettings(); + + processorEnvMock.Verify(); + + Assert.Equal(ConfigurationTestResult.Failed, result.TestResult); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests TestSettings when a Microsoft.PowerShell.Commands.WriteErrorException is thrown. + /// </summary> + [Fact] + public void TestSettings_Throws_Pwsh_WriteErrorException() + { + var thrownException = new WriteErrorException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeTestResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Assert); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.TestSettings(); + + processorEnvMock.Verify(); + + Assert.Equal(ConfigurationTestResult.Failed, result.TestResult); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests TestSettings when a non specialized catched exception is thrown. + /// </summary> + [Fact] + public void TestSettings_Throws() + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeTestResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => new ArgumentNullException("a message")) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Assert); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + Assert.Throws<ArgumentNullException>(() => unitProcessor.TestSettings()); + + processorEnvMock.Verify(); + } + + /// <summary> + /// Call ApplySettings with invalid intents. + /// </summary> + /// <param name="intent">Intent.</param> + [Theory] + [InlineData(ConfigurationUnitIntent.Inform)] + [InlineData(ConfigurationUnitIntent.Assert)] + public void ApplySettings_InvalidIntent(ConfigurationUnitIntent intent) + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + var unitResource = this.CreateUnitResource(intent); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + Assert.Throws<NotSupportedException>(() => unitProcessor.ApplySettings()); + } + + /// <summary> + /// Call ApplySettings. + /// </summary> + /// <param name="rebootRequired">Reboot required.</param> + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ApplySettings_Test(bool rebootRequired) + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeSetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Returns(rebootRequired) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Apply); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.ApplySettings(); + + Assert.Equal(rebootRequired, result.RebootRequired); + } + + /// <summary> + /// Tests ApplySettings when a System.Management.Automation.RuntimeException is thrown. + /// </summary> + [Fact] + public void ApplySettings_Throws_Pwsh_RuntimeException() + { + var thrownException = new RuntimeException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeSetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Apply); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.ApplySettings(); + + processorEnvMock.Verify(); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests ApplySettings when a Microsoft.PowerShell.Commands.WriteErrorException is thrown. + /// </summary> + [Fact] + public void ApplySettings_Throws_Pwsh_WriteErrorException() + { + var thrownException = new RuntimeException("a message"); + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeSetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => thrownException) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Apply); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + var result = unitProcessor.ApplySettings(); + + processorEnvMock.Verify(); + + // Do not check for the type. + Assert.Equal(thrownException.HResult, result.ResultInformation.ResultCode.HResult); + Assert.True(!string.IsNullOrWhiteSpace(result.ResultInformation.Description)); + } + + /// <summary> + /// Tests ApplySettings when a non specialized catched exception is thrown. + /// </summary> + [Fact] + public void ApplySettings_Throws() + { + var processorEnvMock = new Mock<IProcessorEnvironment>(); + processorEnvMock.Setup(m => m.InvokeSetResource( + It.IsAny<ValueSet>(), + It.IsAny<string>(), + It.IsAny<ModuleSpecification?>())) + .Throws(() => new ArgumentNullException("a message")) + .Verifiable(); + + var unitResource = this.CreateUnitResource(ConfigurationUnitIntent.Apply); + + var unitProcessor = new ConfigurationUnitProcessor(processorEnvMock.Object, unitResource); + + Assert.Throws<ArgumentNullException>(() => unitProcessor.ApplySettings()); + + processorEnvMock.Verify(); + } + + private ConfigurationUnitAndResource CreateUnitResource(ConfigurationUnitIntent intent) + { + string resourceName = "xResourceName"; + return new ConfigurationUnitAndResource( + new ConfigurationUnitInternal( + new ConfigurationUnit + { + UnitName = resourceName, + Intent = intent, + }, + new Dictionary<string, object>()), + new DscResourceInfoInternal(resourceName, null, null)); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2SimpleFileResourceTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2SimpleFileResourceTests.cs @@ -0,0 +1,274 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscModuleV2SimpleFileResourceTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.IO; + using Microsoft.Management.Configuration.Processor.DscModule; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + using static Microsoft.Management.Configuration.UnitTests.Helpers.PowerShellTestsConstants; + + /// <summary> + /// Class that tests a little not that complex resource. + /// </summary> + [Collection("UnitTestCollection")] + public class DscModuleV2SimpleFileResourceTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="DscModuleV2SimpleFileResourceTests"/> class. + /// </summary> + /// <param name="fixture">Fixture.</param> + /// <param name="log">log.</param> + public DscModuleV2SimpleFileResourceTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Test SimpleFileResource Ensure Present and Absent when file doesn't exist. + /// </summary> + /// <param name="ensureValue">Ensure value.</param> + /// <param name="expectedResult">Expected result.</param> + [Theory] + [InlineData("Absent", true)] + [InlineData("Present", false)] + public void SimpleFileResource_FileAbsent(string ensureValue, bool expectedResult) + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + // Doesn't create a file. + using var tmpFile = new TempFile(); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", ensureValue }, + }; + + var dscModule = new DscModuleV2(); + Assert.Equal( + expectedResult, + dscModule.InvokeTestResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Test SimpleFileResource when the file exists. + /// </summary> + [Fact] + public void SimpleFileResource_FilePresent() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + using var tmpFile = new TempFile(); + tmpFile.CreateFile(); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", "Present" }, + }; + + var dscModule = new DscModuleV2(); + Assert.True(dscModule.InvokeTestResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Test SimpleFileResource with different content. + /// </summary> + [Fact] + public void SimpleFileResource_DifferentContent() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + using var tmpFile = new TempFile(content: "All work and no play makes Ruben a dull boy"); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", "Present" }, + { "Content", "Is that a from somewhere?" }, + }; + + var dscModule = new DscModuleV2(); + Assert.False(dscModule.InvokeTestResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Test SimpleFileResource with same context. + /// </summary> + [Fact] + public void SimpleFileResource_SameContent() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + string content = "This is literally the same, dont fail"; + using var tmpFile = new TempFile(content: content); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", "Present" }, + { "Content", content }, + }; + + var dscModule = new DscModuleV2(); + Assert.True(dscModule.InvokeTestResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Test SimpleFileResource Set creates the file. + /// </summary> + /// <param name="ensureValue">Ensure value.</param> + /// <param name="preCondition">If file exists before calling set.</param> + /// <param name="postCondition">If file exists after calling set.</param> + [Theory] + [InlineData("Absent", false, false)] + [InlineData("Present", false, true)] + [InlineData("Absent", true, false)] + [InlineData("Present", true, true)] + public void SimpleFileResource_Set_Ensure(string ensureValue, bool preCondition, bool postCondition) + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + // Doesn't create a file. + using var tmpFile = new TempFile(); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", ensureValue }, + }; + + if (preCondition) + { + tmpFile.CreateFile(); + } + + Assert.Equal( + preCondition, + File.Exists(tmpFile.FullFileName)); + + var dscModule = new DscModuleV2(); + dscModule.InvokeSetResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + Assert.Equal( + postCondition, + File.Exists(tmpFile.FullFileName)); + } + + /// <summary> + /// Tests SimpleFileResource Set writes to the file. + /// </summary> + /// <param name="preSetContent">The text in the file before calling Set.</param> + /// <param name="postSetContent">The test in the file after calling Set.</param> + [Theory] + [InlineData(null, "after content")] + [InlineData("i am a content", "and im another")] + [InlineData("copy paste", "copy paste")] + public void SimpleFileResource_Set_Content(string? preSetContent, string postSetContent) + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + // Doesn't create a file. + using var tmpFile = new TempFile(); + if (preSetContent is not null) + { + tmpFile.CreateFile(preSetContent); + } + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + { "Ensure", "Present" }, + { "Content", postSetContent }, + }; + + var dscModule = new DscModuleV2(); + dscModule.InvokeSetResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + Assert.Equal( + postSetContent, + File.ReadAllText(tmpFile.FullFileName)); + } + + /// <summary> + /// Test SimpleFileResource Get. + /// </summary> + [Fact] + public void SimpleFileResource_Get() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + string content = "I'm out of ideas"; + using var tmpFile = new TempFile(content: content); + + var settings = new ValueSet + { + { "Path", tmpFile.FullFileName }, + }; + + var dscModule = new DscModuleV2(); + var properties = dscModule.InvokeGetResource( + processorEnv.Runspace, + settings, + TestModule.SimpleFileResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + Assert.True(properties.ContainsKey("Path")); + Assert.True(properties.TryGetValue("Path", out object pathResult)); + Assert.Equal(tmpFile.FullFileName, pathResult as string); + + // Present is just the default value. + Assert.True(properties.ContainsKey("Ensure")); + Assert.True(properties.TryGetValue("Ensure", out object ensureResult)); + Assert.Equal("Present", ensureResult as string); + + Assert.True(properties.ContainsKey("Content")); + Assert.True(properties.TryGetValue("Content", out object contentResult)); + Assert.Equal(content, contentResult); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2Tests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DscModuleV2Tests.cs @@ -0,0 +1,534 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscModuleV2Tests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.IO; + using System.Management.Automation; + using Microsoft.Management.Configuration.Processor.DscModule; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Microsoft.PowerShell.Commands; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + using static Microsoft.Management.Configuration.UnitTests.Helpers.PowerShellTestsConstants; + + /// <summary> + /// Tests DscModuleV2 with really simple resources. + /// </summary> + [Collection("UnitTestCollection")] + public class DscModuleV2Tests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="DscModuleV2Tests"/> class. + /// </summary> + /// <param name="fixture">Fixture.</param> + /// <param name="log">log.</param> + public DscModuleV2Tests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests GetAllDscResources. + /// </summary> + [Fact] + public void GetAllDscResources_Test() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + var resources = dscModule.GetAllDscResources(testEnvironment.Runspace); + + Assert.True(resources.Count > 0); + Assert.Contains(resources, r => r.Name == TestModule.SimpleFileResourceName); + } + + /// <summary> + /// Tests GetDscResourcesInModule. + /// </summary> + /// <param name="module">Module.</param> + /// <param name="expectedResources">Expected DSC resources.</param> + [Theory] + [InlineData(TestModule.SimpleTestResourceModuleName, 4)] + [InlineData("MyReallyFakeModule", 0)] + public void GetDscResourcesInModule_Test(string module, int expectedResources) + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + var resources = dscModule.GetDscResourcesInModule( + testEnvironment.Runspace, + PowerShellHelpers.CreateModuleSpecification(module)); + Assert.Equal(expectedResources, resources.Count); + } + + /// <summary> + /// Tests GetDscResourcesInModule with versions. + /// </summary> + [Fact] + public void GetDscResourcesInModule_VersionTest() + { + string newVersion = "1.0.0.0"; + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + // Get duplicated resources by creating a new directory and copy our modules. + // Change version and add them to the PSModulePath. + using var tmpDir = new TempDirectory(); + tmpDir.CopyDirectory(this.fixture.TestModulesPath); + var manifestFile = Path.Combine( + tmpDir.FullDirectoryPath, + TestModule.SimpleTestResourceModuleName, + TestModule.SimpleTestResourceManifestFileName); + File.WriteAllText( + manifestFile, + File.ReadAllText(manifestFile).Replace("0.0.0.1", newVersion)); + testEnvironment.AppendPSModulePath(tmpDir.FullDirectoryPath); + + var dscModule = new DscModuleV2(); + + // This doesn't work on 2.0.6 + ////var allResources = dscModule.GetDscResourcesInModule( + //// testEnvironment.Runspace, + //// PowerShellHelpers.CreateModuleSpecification(TestModule.SimpleTestResourceModuleName)); + ////Assert.Equal(8, allResources.Count); + + var ogResources = dscModule.GetDscResourcesInModule( + testEnvironment.Runspace, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + version: TestModule.SimpleTestResourceVersion)); + Assert.Equal(4, ogResources.Count); + + var newVersionResources = dscModule.GetDscResourcesInModule( + testEnvironment.Runspace, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + version: newVersion)); + Assert.Equal(4, ogResources.Count); + + var badVersionResources = dscModule.GetDscResourcesInModule( + testEnvironment.Runspace, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + version: "1.2.3.4")); + Assert.Equal(0, badVersionResources.Count); + } + + /// <summary> + /// Tests GetDscResource. Should return a resource. + /// </summary> + [Fact] + public void GetDscResource_ResourceExists() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + var resource = dscModule.GetDscResource( + testEnvironment.Runspace, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification(TestModule.SimpleTestResourceModuleName)); + + Assert.NotNull(resource); + } + + /// <summary> + /// Test GetDscResource for a resource that doesn't exist. + /// </summary> + [Fact] + public void GetDscResource_ResourceDoesntExist() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + Assert.Throws<WriteErrorException>( + () => dscModule.GetDscResource( + testEnvironment.Runspace, + "FakeResourceName", + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Test GetDscResource when the same module is in different paths. + /// </summary> + [Fact] + public void GetDscResource_Conflict() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + // Get duplicated resources by creating a new directory and copy our modules. + // Then add it to the PSModulePath. + using var tmpDir = new TempDirectory(); + tmpDir.CopyDirectory(this.fixture.TestModulesPath); + testEnvironment.AppendPSModulePath(tmpDir.FullDirectoryPath); + + var dscModule = new DscModuleV2(); + + Assert.Throws<RuntimeException>( + () => dscModule.GetDscResource( + testEnvironment.Runspace, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Tests GetDscResource when there are multiple versions of a resource. + /// </summary> + [Fact] + public void GetDscResource_DiffVersions() + { + string newVersion = "2.0.0.0"; + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + // Get duplicated resources by creating a new directory and copy our modules. + // Change version and add them to the PSModulePath. + using var tmpDir = new TempDirectory(); + tmpDir.CopyDirectory(this.fixture.TestModulesPath); + var manifestFile = Path.Combine( + tmpDir.FullDirectoryPath, + TestModule.SimpleTestResourceModuleName, + TestModule.SimpleTestResourceManifestFileName); + File.WriteAllText( + manifestFile, + File.ReadAllText(manifestFile).Replace("0.0.0.1", newVersion)); + testEnvironment.AppendPSModulePath(tmpDir.FullDirectoryPath); + + var dscModule = new DscModuleV2(); + + // specific version. + var resource = dscModule.GetDscResource( + testEnvironment.Runspace, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + TestModule.SimpleTestResourceVersion)); + Assert.NotNull(resource); + + var dsc = dscModule.GetDscResource( + testEnvironment.Runspace, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + version: newVersion)); + + Assert.NotNull(dsc); + Assert.NotNull(dsc.Version); + Assert.Equal(newVersion, dsc.Version.ToString()); + } + + /// <summary> + /// Calls Invoke-DscResource Get. + /// </summary> + [Fact] + public void InvokeGetResource_Test() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var getResult = dscModule.InvokeGetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + Assert.True(getResult.ContainsKey("key")); + Assert.True(getResult.TryGetValue("key", out object keyValue)); + Assert.Equal("SimpleTestResourceKey", keyValue as string); + } + + /// <summary> + /// Calls Invoke-DscResource Get. Resource Get throws. + /// </summary> + [Fact] + public void InvokeGetResource_ResourceThrows() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>(() => + dscModule.InvokeGetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceThrowsName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<RuntimeException>(exception.InnerException); + } + + /// <summary> + /// Calls Invoke-DscResource Get. Resource writes error. + /// </summary> + [Fact(Skip = "Not supported in PSDesiredStateConfiguration 2.0.6")] + public void InvokeGetResource_ResourceError() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + Assert.Throws<WriteErrorException>(() => + dscModule.InvokeGetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceErrorName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Calls Invoke-DscResource Get. Resource does not exist. + /// </summary> + [Fact] + public void InvokeGetResource_ResourceDoesntExist() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>( + () => dscModule.InvokeGetResource( + testEnvironment.Runspace, + new ValueSet(), + "FakeResourceName", + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<WriteErrorException>(exception.InnerException); + } + + /// <summary> + /// Calls Invoke-DscResource Test. + /// </summary> + /// <param name="value">Setting value.</param> + /// <param name="expectedResult">Expected result.</param> + [Theory] + [InlineData("4815162342", true)] + [InlineData("notalostreference", false)] + public void InvokeTestResource_Test(string value, bool expectedResult) + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var settings = new ValueSet() + { + { "secretCode", value }, + }; + + var testResult = dscModule.InvokeTestResource( + testEnvironment.Runspace, + settings, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + Assert.Equal(expectedResult, testResult); + } + + /// <summary> + /// Calls Invoke-DscResource Test. Resource throws. + /// </summary> + [Fact] + public void InvokeTestResource_Throws() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>(() => + dscModule.InvokeTestResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceThrowsName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<RuntimeException>(exception.InnerException); + } + + /// <summary> + /// Calls Invoke-DscResource Test. Resource writes error. + /// </summary> + [Fact(Skip = "Not supported in PSDesiredStateConfiguration 2.0.6")] + public void InvokeTestResource_ResourceError() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + Assert.Throws<WriteErrorException>(() => + dscModule.InvokeTestResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceErrorName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Calls Invoke-DscResource Test. Resource does not exist. + /// </summary> + [Fact] + public void InvokeTestResource_ResourceDoesntExist() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>(() => + _ = dscModule.InvokeTestResource( + testEnvironment.Runspace, + new ValueSet(), + "FakeResourceName", + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<WriteErrorException>(exception.InnerException); + } + + /// <summary> + /// Calls Invoke-DscResource Set. + /// </summary> + /// <param name="value">Setting value.</param> + /// <param name="rebootRequired">Expected reboot required.</param> + [Fact] + public void InvokeSetResource_Test() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var settings = new ValueSet() + { + { "secretCode", "4815162342" }, + }; + + var testResult = dscModule.InvokeSetResource( + testEnvironment.Runspace, + settings, + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName)); + + // TODO: Verify reboot required when is supported for class resources. + ////Assert.Equal(rebootRequired, testResult); + } + + /// <summary> + /// Calls Invoke-DscResource Set. Resource throws. + /// </summary> + [Fact] + public void InvokeSetResource_Throws() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>(() => + dscModule.InvokeSetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceThrowsName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<RuntimeException>(exception.InnerException); + } + + /// <summary> + /// Calls Invoke-DscResource Set. Resource writes error. + /// </summary> + [Fact(Skip = "Not supported in PSDesiredStateConfiguration 2.0.6")] + public void InvokeSetResource_ResourceError() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + Assert.Throws<WriteErrorException>(() => + dscModule.InvokeSetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceErrorName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + } + + /// <summary> + /// Calls Invoke-DscResource Set. Resource does not exist. + /// </summary> + [Fact] + public void InvokeSetResource_ResourceDoesntExist() + { + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + var dscModule = new DscModuleV2(); + + var exception = Assert.Throws<RuntimeException>(() => + dscModule.InvokeSetResource( + testEnvironment.Runspace, + new ValueSet(), + "FakeResourceName", + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName))); + + Assert.IsType<WriteErrorException>(exception.InnerException); + } + + /// <summary> + /// Test calling Invoke-DscResource when a resource has multiple versions. + /// </summary> + [Fact] + public void InvokeResource_MultipleVersions() + { + string newVersion = "0.0.2.0"; + var testEnvironment = this.fixture.PrepareTestProcessorEnvironment(); + + // Get duplicated resources by creating a new directory and copy our modules. + // Change version and add them to the PSModulePath. + using var tmpDir = new TempDirectory(); + tmpDir.CopyDirectory(this.fixture.TestModulesPath); + var manifestFile = Path.Combine( + tmpDir.FullDirectoryPath, + TestModule.SimpleTestResourceModuleName, + TestModule.SimpleTestResourceManifestFileName); + File.WriteAllText( + manifestFile, + File.ReadAllText(manifestFile).Replace("0.0.0.1", newVersion)); + testEnvironment.AppendPSModulePath(tmpDir.FullDirectoryPath); + + var dscModule = new DscModuleV2(); + + dscModule.InvokeSetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + TestModule.SimpleTestResourceVersion)); + + dscModule.InvokeSetResource( + testEnvironment.Runspace, + new ValueSet(), + TestModule.SimpleTestResourceName, + PowerShellHelpers.CreateModuleSpecification( + TestModule.SimpleTestResourceModuleName, + version: newVersion)); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DscResourceMapTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DscResourceMapTests.cs @@ -0,0 +1,176 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DscResourceMapTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor.DscResourcesInfo; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// DscResourceMap tests. + /// </summary> + [Collection("UnitTestCollection")] + public class DscResourceMapTests + { + private const string ResourceZoro = "xResourceZoro"; + + private const string ModuleMugiwara = "xMugiwaraModule"; + private const string ModuleMugiwaraResourceLuffy = "xResourceLuffy"; + + private const string ModuleOni = "xModuleOni"; + private const string ModuleOniResourceKaido = "xResourceKaido"; + private const string ModuleOniResourceYamato = "xResourceYamato"; + + private static readonly Version VersionZoro = new Version("1.0.0.0"); + + private static readonly Version VersionLuffyGear4 = new Version("4.0.0.1"); + private static readonly Version VersionLuffyGear5 = new Version("5.0.0.0"); + + private static readonly Version VersionKaido = new Version("0.0.0.4"); + private static readonly Version VersionYamato = new Version("0.0.0.11"); + + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="DscResourceMapTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log.</param> + public DscResourceMapTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Test GetResource for resources in the map. + /// </summary> + [Fact] + public void DscResourcesMap_GetResource() + { + var dscResourceMap = new DscResourcesMap(this.CreateDscResourceInfo()); + + // Get just by name. + var zoroResult = dscResourceMap.GetResource(ResourceZoro, null, null); + Assert.NotNull(zoroResult); + Assert.Equal(ResourceZoro, zoroResult.Name); + Assert.Equal(VersionZoro, zoroResult.Version); + + // Name not normalized. + var zoroResultNormalize = dscResourceMap.GetResource("XRESOURCEZORO", null, null); + Assert.NotNull(zoroResultNormalize); + Assert.Equal(ResourceZoro, zoroResultNormalize.Name); + Assert.Equal(VersionZoro, zoroResultNormalize.Version); + + var yamatoResult = dscResourceMap.GetResource(ModuleOniResourceYamato, null, null); + Assert.NotNull(yamatoResult); + Assert.Equal(ModuleOniResourceYamato, yamatoResult.Name); + Assert.Equal(ModuleOni, yamatoResult.ModuleName); + Assert.Equal(VersionYamato, yamatoResult.Version); + + // Just by name and module get latest. + var luffyResult5 = dscResourceMap.GetResource(ModuleMugiwaraResourceLuffy, ModuleMugiwara, null); + Assert.NotNull(luffyResult5); + Assert.Equal(ModuleMugiwaraResourceLuffy, luffyResult5.Name); + Assert.Equal(ModuleMugiwara, luffyResult5.ModuleName); + Assert.Equal(VersionLuffyGear5, luffyResult5.Version); + + // Specific version. + var luffyResult4 = dscResourceMap.GetResource(ModuleMugiwaraResourceLuffy, ModuleMugiwara, VersionLuffyGear4); + Assert.NotNull(luffyResult4); + Assert.Equal(ModuleMugiwaraResourceLuffy, luffyResult4.Name); + Assert.Equal(ModuleMugiwara, luffyResult4.ModuleName); + Assert.Equal(VersionLuffyGear4, luffyResult4.Version); + + // Module name not normalized + var luffyResult4Normalized = dscResourceMap.GetResource(ModuleMugiwaraResourceLuffy, "XMUGIWARAMODULE", VersionLuffyGear4); + Assert.NotNull(luffyResult4Normalized); + Assert.Equal(ModuleMugiwaraResourceLuffy, luffyResult4Normalized.Name); + Assert.Equal(ModuleMugiwara, luffyResult4Normalized.ModuleName); + Assert.Equal(VersionLuffyGear4, luffyResult4Normalized.Version); + } + + /// <summary> + /// Tests GetResource when resources are not in the maps. + /// </summary> + [Fact] + public void DscResourcesMap_GetResource_NoResource() + { + var dscResourceMap = new DscResourcesMap(this.CreateDscResourceInfo()); + + // Zoro is lost. + var zoroResult = dscResourceMap.GetResource(ResourceZoro, ModuleMugiwara, null); + Assert.Null(zoroResult); + + // Yamato didn't join (spoilers) + var yamatoResult = dscResourceMap.GetResource(ModuleOniResourceYamato, ModuleMugiwara, VersionYamato); + Assert.Null(yamatoResult); + + // Gear 6 is not a thing (?) + var luffyResult = dscResourceMap.GetResource(ModuleMugiwaraResourceLuffy, ModuleMugiwara, Version.Parse("6.0.0.0")); + Assert.Null(luffyResult); + + // Wrong universe. + var gokuResult = dscResourceMap.GetResource("xResourceGoku", null, null); + Assert.Null(gokuResult); + } + + /// <summary> + /// Test Exists for resources in the map. + /// </summary> + [Fact] + public void DscResourcesMap_Exists() + { + var dscResourceMap = new DscResourcesMap(this.CreateDscResourceInfo()); + + Assert.True(dscResourceMap.Exists(ResourceZoro, null, null)); + Assert.True(dscResourceMap.Exists(ModuleOniResourceYamato, null, null)); + Assert.True(dscResourceMap.Exists(ModuleMugiwaraResourceLuffy, ModuleMugiwara, null)); + Assert.True(dscResourceMap.Exists(ModuleMugiwaraResourceLuffy, ModuleMugiwara, VersionLuffyGear4)); + } + + /// <summary> + /// Tests Exists when resources are not in the maps. + /// </summary> + [Fact] + public void DscResourcesMap_Exists_NoResource() + { + var dscResourceMap = new DscResourcesMap(this.CreateDscResourceInfo()); + + Assert.False(dscResourceMap.Exists(ResourceZoro, ModuleMugiwara, null)); + Assert.False(dscResourceMap.Exists(ModuleOniResourceYamato, ModuleMugiwara, VersionYamato)); + Assert.False(dscResourceMap.Exists(ModuleMugiwaraResourceLuffy, ModuleMugiwara, Version.Parse("6.0.0.0"))); + Assert.False(dscResourceMap.Exists("xResourceGoku", null, null)); + } + + /// <summary> + /// Creates 5 fake resources. + /// 1 resource without module name. + /// 2 resources module A same resource v1 v2. + /// 2 resources module B different resources. + /// </summary> + /// <returns>Fake resources.</returns> + private IReadOnlyList<DscResourceInfoInternal> CreateDscResourceInfo() + { + return new List<DscResourceInfoInternal>() + { + new DscResourceInfoInternal(ResourceZoro, null, VersionZoro), + + new DscResourceInfoInternal(ModuleMugiwaraResourceLuffy, ModuleMugiwara, VersionLuffyGear4), + new DscResourceInfoInternal(ModuleMugiwaraResourceLuffy, ModuleMugiwara, VersionLuffyGear5), + + new DscResourceInfoInternal(ModuleOniResourceKaido, ModuleOni, VersionKaido), + new DscResourceInfoInternal(ModuleOniResourceYamato, ModuleOni, VersionYamato), + }; + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ExceptionExtensionsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ExceptionExtensionsTests.cs @@ -0,0 +1,64 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ExceptionExtensionsTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.PowerShell.Commands; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Exception extension tests. + /// </summary> + [Collection("UnitTestCollection")] + public class ExceptionExtensionsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ExceptionExtensionsTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ExceptionExtensionsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests GetMostInnerException. + /// </summary> + [Fact] + public void GetMostInnerException_Test() + { + var exception = new Exception( + "message1", + new WriteErrorException( + "WriteException", + new ArgumentNullException())); + + var mostInner = exception.GetMostInnerException(); + Assert.IsType<ArgumentNullException>(mostInner); + + var exception2 = new Exception( + "message2", + new WriteErrorException( + "WriteException2")); + + mostInner = exception2.GetMostInnerException(); + Assert.IsType<WriteErrorException>(mostInner); + + var exception3 = new ArgumentOutOfRangeException("message2"); + mostInner = exception3.GetMostInnerException(); + Assert.IsType<ArgumentOutOfRangeException>(mostInner); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ProcessorEnvironmentTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ProcessorEnvironmentTests.cs @@ -0,0 +1,215 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ProcessorEnvironmentTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Moq; + using Xunit; + using Xunit.Abstractions; + using static Microsoft.Management.Configuration.Processor.Constants.PowerShellConstants; + + /// <summary> + /// HostedEnvironment tests, that is more ProcessorEnvironmentBase tests for non forwarding functions. + /// </summary> + [Collection("UnitTestCollection")] + public class ProcessorEnvironmentTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="ProcessorEnvironmentTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log.</param> + public ProcessorEnvironmentTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Test SetVariable and SetVariable methods. + /// </summary> + [Fact] + public void HostedEnvironment_Variables() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + // As string. + string var1Name = "var1"; + string var1 = "This is a string"; + processorEnv.SetVariable(var1Name, var1); + string var1Result = processorEnv.GetVariable<string>(var1Name); + Assert.Equal(var1, var1Result); + + // As int. + string var2Name = "var2"; + int var2 = 42; + processorEnv.SetVariable(var2Name, var2); + int var2Result = processorEnv.GetVariable<int>(var2Name); + + // Wrong type. + Assert.Throws<System.InvalidCastException>(() => processorEnv.GetVariable<int>(var1Name)); + } + + /// <summary> + /// Tests SetPsModulePath. + /// </summary> + [Fact] + public void HostedEnvironment_SetPSModulePath() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + string psModulePathInput = "SetPSModulePathModulePath"; + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.NotEqual(psModulePathInput, psModulePath); + + processorEnv.SetPSModulePath(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.Equal(psModulePathInput, psModulePath); + } + + /// <summary> + /// Tests SetPsModulePaths. + /// </summary> + [Fact] + public void HostedEnvironment_SetPSModulePaths() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + var psModulePathInput = new List<string>() + { + "Path1", + "Path2", + "Path3", + "Path4", + }; + string psModulePathExpected = "Path1;Path2;Path3;Path4"; + + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.NotEqual(psModulePathExpected, psModulePath); + + processorEnv.SetPSModulePaths(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.Equal(psModulePathExpected, psModulePath); + } + + /// <summary> + /// Tests AppendPSModulePath. + /// </summary> + [Fact] + public void HostedEnvironment_AppendPSModulePath() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + string psModulePathInput = "AppendPSModulePathModulePath"; + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.EndsWith($";{psModulePathInput}")); + + processorEnv.AppendPSModulePath(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.EndsWith($";{psModulePathInput}", psModulePath); + } + + /// <summary> + /// Tests AppendPSModulePaths. + /// </summary> + [Fact] + public void HostedEnvironment_AppendPSModulePaths() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + var psModulePathInput = new List<string>() + { + "AppendPSModulePathsPath1", + "AppendPSModulePathsPath2", + "AppendPSModulePathsPath3", + "AppendPSModulePathsPath4", + }; + string psModulePathExpected = "AppendPSModulePathsPath1;AppendPSModulePathsPath2;AppendPSModulePathsPath3;AppendPSModulePathsPath4"; + + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.EndsWith($";{psModulePathExpected}")); + + processorEnv.AppendPSModulePaths(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.EndsWith($";{psModulePathExpected}", psModulePath); + } + + /// <summary> + /// Tests PrependPSModulePath. + /// </summary> + [Fact] + public void HostedEnvironment_PrependPSModulePath() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + string psModulePathInput = "PrependPSModulePathModulePath"; + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.StartsWith($";{psModulePathInput}")); + + processorEnv.PrependPSModulePath(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.StartsWith($"{psModulePathInput};", psModulePath); + } + + /// <summary> + /// Tests PrependPSModulePaths. + /// </summary> + [Fact] + public void HostedEnvironment_PrependPSModulePaths() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + + var psModulePathInput = new List<string>() + { + "PrependPSModulePathsPath1", + "PrependPSModulePathsPath2", + "PrependPSModulePathsPath3", + "PrependPSModulePathsPath4", + }; + string psModulePathExpected = "PrependPSModulePathsPath1;PrependPSModulePathsPath2;PrependPSModulePathsPath3;PrependPSModulePathsPath4"; + + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.False(psModulePath.StartsWith($";{psModulePathExpected}")); + + processorEnv.PrependPSModulePaths(psModulePathInput); + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.StartsWith($"{psModulePathExpected};", psModulePath); + } + + /// <summary> + /// Test CleanupPSModulePath. + /// </summary> + [Fact] + public void HostedEnvironment_CleanupPSModulePath() + { + var processorEnv = this.fixture.PrepareTestProcessorEnvironment(); + var psModulePathInput = new List<string>() + { + "CleanupPSModulePathPath1", + "CleanupPSModulePathPath2", + "CleanupPSModulePathPath3", + "CleanupPSModulePathPath1", + "CleanupPSModulePathPath1", + }; + + string psModulePathExpected = "CleanupPSModulePathPath1;CleanupPSModulePathPath2;CleanupPSModulePathPath3;CleanupPSModulePathPath1;CleanupPSModulePathPath1"; + processorEnv.SetPSModulePaths(psModulePathInput); + string psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.Equal(psModulePathExpected, psModulePath); + + processorEnv.CleanupPSModulePath("CleanupPSModulePathPath1"); + + psModulePathExpected = "CleanupPSModulePathPath2;CleanupPSModulePathPath3"; + psModulePath = processorEnv.GetVariable<string>(Variables.PSModulePath); + Assert.Equal(psModulePath, psModulePath); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs @@ -0,0 +1,140 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TypeHelpersTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// TypeHelpers tests. + /// </summary> + [Collection("UnitTestCollection")] + + public class TypeHelpersTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="TypeHelpersTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public TypeHelpersTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + private enum TestEnum + { + Value, + } + + /// <summary> + /// Tests PropertyExists. + /// </summary> + [Fact] + public void PropertyExistsTests() + { + dynamic obj = new + { + Property1 = "value", + }; + + Assert.True(TypeHelpers.PropertyExists(obj, "Property1")); + Assert.False(TypeHelpers.PropertyExists(obj, "Property2")); + } + + /// <summary> + /// Tests PropertyWithTypeExists. + /// </summary> + [Fact] + public void PropertyWithTypeExistsTests() + { + dynamic obj = new + { + Property1 = "value", + Property2 = 42, + }; + + Assert.True(TypeHelpers.PropertyWithTypeExists<string>(obj, "Property1")); + Assert.True(TypeHelpers.PropertyWithTypeExists<int>(obj, "Property2")); + + Assert.False(TypeHelpers.PropertyWithTypeExists<int>(obj, "Property1")); + Assert.False(TypeHelpers.PropertyWithTypeExists<string>(obj, "Property2")); + } + + /// <summary> + /// Test PropertyExistsAndIsEnum. + /// </summary> + [Fact] + public void PropertyExistsAndIsEnumTests() + { + dynamic obj = new + { + Property1 = TestEnum.Value, + Property2 = "string", + }; + + Assert.True(TypeHelpers.PropertyExistsAndIsEnum(obj, "Property1")); + Assert.False(TypeHelpers.PropertyExistsAndIsEnum(obj, "Property2")); + } + + /// <summary> + /// Tests PropertyExistsAndIsList. + /// </summary> + [Fact] + public void PropertyExistsAndIsListTests() + { + dynamic obj = new + { + Property1 = new List<string>() { "value" }, + Property2 = "string", + }; + + Assert.True(TypeHelpers.PropertyExistsAndIsList(obj, "Property1")); + Assert.False(TypeHelpers.PropertyExistsAndIsList(obj, "Property2")); + } + + /// <summary> + /// Tests GetAllPropertiesValues. + /// </summary> + [Fact] + public void GetAllPropertiesValuesTests() + { + string s = "value"; + int i = 42; + TestEnum e = TestEnum.Value; + dynamic obj = new + { + Property1 = s, + Property2 = i, + Property3 = e, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Equal(3, set.Count); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object v1)); + Assert.Equal(s, v1 as string); + + Assert.True(set.ContainsKey("Property2")); + Assert.True(set.TryGetValue("Property2", out object v2)); + Assert.Equal(i, (int)v2); + + Assert.True(set.ContainsKey("Property3")); + Assert.True(set.TryGetValue("Property3", out object v3)); + Assert.Equal(e.ToString(), v3); + } + } +} diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.idl @@ -76,7 +76,7 @@ namespace Microsoft.Management.Configuration // When the current settings are retrieved from the system, this can be used to exclude settings that are not relevant to a future application of the unit of configuration. Boolean IsInformational{ get; }; // The data type for the value of this setting. - Windows.Foundation.PropertyType Type{ get; }; + String Type{ get; }; // The semantics to be used for this setting. // The goal is to enable richer conflict detection and authoring scenarios by having a deeper understanding of this value than "String". // TODO: Create the actual semantics of the semantics value... @@ -117,8 +117,9 @@ namespace Microsoft.Management.Configuration String Author{ get; }; // The publisher of the module containing the unit of configuration. String Publisher{ get; }; - // The signing certificate chain of the module containing the unit of configuration. - Windows.Security.Cryptography.Certificates.CertificateChain SigningCertificateChain{ get; }; + // The signing information of the module files containing the unit of configuration. + // May contain Windows.Security.Cryptography.Certificates.Certificate or Windows.Security.Cryptography.Certificates.CertificateChain. + Windows.Foundation.Collections.IVectorView<IInspectable> SigningInformation{ get; }; // The settings information for the unit of configuration. Windows.Foundation.Collections.IVectorView<IConfigurationUnitSettingDetails> Settings{ get; }; }