diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 264a6c92..c0cf9e16 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -60,10 +60,32 @@ vsconfig website websites wekyb +Hmmss +MMdd +azdoserver +Codecov +eature +HQRM +hqrmtest +modulefast +MOF +nupkg +powershellgallery +quickconfig +QWord +registrering +schange +setvariable +sourcing +splatting +updatebuildnumber +vso +winrm +MMdd +NOLOGO whatif wildcards Windo winget workflows worktree -yml diff --git a/.gitignore b/.gitignore index dfcfd56f..c2862786 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,6 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Output folder from Sampler module +**/output/ \ No newline at end of file diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 new file mode 100644 index 00000000..78312d2c --- /dev/null +++ b/.vscode/analyzersettings.psd1 @@ -0,0 +1,44 @@ +@{ + CustomRulePath = '.\output\RequiredModules\DscResource.AnalyzerRules' + includeDefaultRules = $true + IncludeRules = @( + # DSC Resource Kit style guideline rules. + 'PSAvoidDefaultValueForMandatoryParameter', + 'PSAvoidDefaultValueSwitchParameter', + 'PSAvoidInvokingEmptyMembers', + 'PSAvoidNullOrEmptyHelpMessageAttribute', + 'PSAvoidUsingCmdletAliases', + 'PSAvoidUsingComputerNameHardcoded', + 'PSAvoidUsingDeprecatedManifestFields', + 'PSAvoidUsingEmptyCatchBlock', + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidShouldContinueWithoutForce', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingWriteHost', + 'PSDSCReturnCorrectTypesForDSCFunctions', + 'PSDSCStandardDSCFunctionsInResource', + 'PSDSCUseIdenticalMandatoryParametersForDSC', + 'PSDSCUseIdenticalParametersForDSC', + 'PSMisleadingBacktick', + 'PSMissingModuleManifestField', + 'PSPossibleIncorrectComparisonWithNull', + 'PSProvideCommentHelp', + 'PSReservedCmdletChar', + 'PSReservedParams', + 'PSUseApprovedVerbs', + 'PSUseCmdletCorrectly', + 'PSUseOutputTypeCorrectly', + 'PSAvoidGlobalVars', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSDSCUseVerboseMessageInDSCResource', + 'PSShouldProcess', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUsePSCredentialType', + + 'Measure-*' + ) + +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 4be48be1..1d65efdd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,19 +4,16 @@ "files.encoding": "utf8bom", "files.autoGuessEncoding": true }, - "powershell.codeFormatting.preset": "OTBS", - "powershell.codeFormatting.useConstantStrings": true, - "powershell.codeFormatting.trimWhitespaceAroundPipe": true, - "powershell.codeFormatting.useCorrectCasing": true, - "powershell.codeFormatting.whitespaceBeforeOpenParen": true, - "powershell.codeFormatting.whitespaceBetweenParameters": true, - "powershell.codeFormatting.addWhitespaceAroundPipe": true, - "powershell.codeFormatting.alignPropertyValuePairs": true, - "powershell.codeFormatting.autoCorrectAliases": true, - "powershell.codeFormatting.avoidSemicolonsAsLineTerminators": true, - "powershell.codeFormatting.ignoreOneLineBlock": true, + "powershell.codeFormatting.openBraceOnSameLine": false, + "powershell.codeFormatting.newLineAfterOpenBrace": true, + "powershell.codeFormatting.newLineAfterCloseBrace": true, "powershell.codeFormatting.whitespaceBeforeOpenBrace": true, + "powershell.codeFormatting.whitespaceBeforeOpenParen": true, "powershell.codeFormatting.whitespaceAroundOperator": true, "powershell.codeFormatting.whitespaceAfterSeparator": true, - "powershell.codeFormatting.whitespaceInsideBrace": true + "powershell.codeFormatting.ignoreOneLineBlock": false, + "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationAfterEveryPipeline", + "powershell.codeFormatting.preset": "Custom", + "powershell.codeFormatting.alignPropertyValuePairs": true, + "powershell.scriptAnalysis.settingsPath": ".vscode\\analyzersettings.psd1", } diff --git a/resources/Microsoft.Windows.Settings/.gitattributes b/resources/Microsoft.Windows.Settings/.gitattributes new file mode 100644 index 00000000..96c2e0d3 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/.gitattributes @@ -0,0 +1,15 @@ +# Needed for publishing of examples, build worker defaults to core.autocrlf=input. +* text eol=autocrlf + +*.mof text eol=crlf +*.sh text eol=lf +*.svg eol=lf + +# Ensure any exe files are treated as binary +*.exe binary +*.jpg binary +*.xl* binary +*.pfx binary +*.png binary +*.dll binary +*.so binary diff --git a/resources/Microsoft.Windows.Settings/CHANGELOG.md b/resources/Microsoft.Windows.Settings/CHANGELOG.md new file mode 100644 index 00000000..ebe373fe --- /dev/null +++ b/resources/Microsoft.Windows.Settings/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog for Microsoft.Windows.Settings + +The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial release of module with the following settings: + - General + - FindMyDevice diff --git a/resources/Microsoft.Windows.Settings/GitVersion.yml b/resources/Microsoft.Windows.Settings/GitVersion.yml new file mode 100644 index 00000000..8a1d5ff0 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/GitVersion.yml @@ -0,0 +1,40 @@ +mode: ContinuousDelivery +next-version: 0.0.1 +major-version-bump-message: '(breaking\schange|breaking|major)\b' +minor-version-bump-message: '(adds?|features?|minor)\b' +patch-version-bump-message: '\s?(fix|patch)' +no-bump-message: '\+semver:\s?(none|skip)' +assembly-informational-format: '{NuGetVersionV2}+Sha.{Sha}.Date.{CommitDate}' +branches: + master: + tag: preview + regex: ^main$ + pull-request: + tag: PR + feature: + tag: useBranchName + increment: Minor + regex: f(eature(s)?)?[\/-] + source-branches: ['master'] + hotfix: + tag: fix + increment: Patch + regex: (hot)?fix(es)?[\/-] + source-branches: ['master'] + +ignore: + sha: [] +merge-message-formats: {} + + +# feature: +# tag: useBranchName +# increment: Minor +# regex: f(eature(s)?)?[/-] +# source-branches: ['master'] +# hotfix: +# tag: fix +# increment: Patch +# regex: (hot)?fix(es)?[/-] +# source-branches: ['master'] + diff --git a/resources/Microsoft.Windows.Settings/README.md b/resources/Microsoft.Windows.Settings/README.md new file mode 100644 index 00000000..e920da20 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/README.md @@ -0,0 +1,16 @@ +# Microsoft.Windows.Settings + +The DSC resource module `Microsoft.Windows.Settings` provides a set of Desired State Configuration (DSC) resources for managing Windows settings. These resources allow you to configure and manage settings such as app preferences, device experiences, and USB configurations through PowerShell DSC. + +## Wiki and module under construction :construction: + +## Resources :construction: + +- **AdvancedAppSettings**: Manage advanced application settings including app source preferences and device sharing experiences. +- **USB**: Manage USB device settings including enabling/disabling devices, allowing wake, and selective suspend options. + +## Usage :construction: + +To use these resources, import the module and invoke the desired DSC resource with the appropriate parameters. + +For more detailed examples and parameter descriptions, refer to the individual resource documentation. diff --git a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 new file mode 100644 index 00000000..72133d05 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 @@ -0,0 +1,30 @@ +@{ + <# + This is only required if you need to use the method PowerShellGet & PSDepend + It is not required for PSResourceGet or ModuleFast (and will be ignored). + See Resolve-Dependency.psd1 on how to enable methods. + #> + #PSDependOptions = @{ + # AddToPath = $true + # Target = 'output\RequiredModules' + # Parameters = @{ + # Repository = 'PSGallery' + # } + #} + + InvokeBuild = '5.12.2' + PSScriptAnalyzer = '1.24.0' + Pester = '5.7.1' + ModuleBuilder = '3.1.7' + ChangelogManagement = '3.1.0' + Sampler = '0.118.2' + 'Sampler.GitHubTasks' = '0.3.4' + 'DscResource.Base' = '1.3.0' + 'DscResource.Common' = '0.21.0' + 'DscResource.Test' = '0.17.2' + 'DscResource.AnalyzerRules' = '0.2.0' + xDscResourceDesigner = '1.13.0.0' + 'DscResource.DocGenerator' = '0.13.0' + PSDesiredStateConfiguration = '2.0.7' +} + diff --git a/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 b/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 new file mode 100644 index 00000000..9e0afae1 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 @@ -0,0 +1,1062 @@ +<# + .DESCRIPTION + Bootstrap script for PSDepend. + + .PARAMETER DependencyFile + Specifies the configuration file for the this script. The default value is + 'RequiredModules.psd1' relative to this script's path. + + .PARAMETER PSDependTarget + Path for PSDepend to be bootstrapped and save other dependencies. + Can also be CurrentUser or AllUsers if you wish to install the modules in + such scope. The default value is 'output/RequiredModules' relative to + this script's path. + + .PARAMETER Proxy + Specifies the URI to use for Proxy when attempting to bootstrap + PackageProvider and PowerShellGet. + + .PARAMETER ProxyCredential + Specifies the credential to contact the Proxy when provided. + + .PARAMETER Scope + Specifies the scope to bootstrap the PackageProvider and PSGet if not available. + THe default value is 'CurrentUser'. + + .PARAMETER Gallery + Specifies the gallery to use when bootstrapping PackageProvider, PSGet and + when calling PSDepend (can be overridden in Dependency files). The default + value is 'PSGallery'. + + .PARAMETER GalleryCredential + Specifies the credentials to use with the Gallery specified above. + + .PARAMETER AllowOldPowerShellGetModule + Allow you to use a locally installed version of PowerShellGet older than + 1.6.0 (not recommended). Default it will install the latest PowerShellGet + if an older version than 2.0 is detected. + + .PARAMETER MinimumPSDependVersion + Allow you to specify a minimum version fo PSDepend, if you're after specific + features. + + .PARAMETER AllowPrerelease + Not yet written. + + .PARAMETER WithYAML + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER ModuleFastBleedingEdge + Specifies to use ModuleFast code that is in the ModuleFast's main branch + in its GitHub repository. The parameter UseModuleFast must also be set to + true. + + .PARAMETER UsePSResourceGet + Specifies to use the new PSResourceGet module instead of the (now legacy) PowerShellGet module. + + .PARAMETER PSResourceGetVersion + String specifying the module version for PSResourceGet if the `UsePSResourceGet` switch is utilized. + + .NOTES + Load defaults for parameters values from Resolve-Dependency.psd1 if not + provided as parameter. +#> +[CmdletBinding()] +param +( + [Parameter()] + [System.String] + $DependencyFile = 'RequiredModules.psd1', + + [Parameter()] + [System.String] + $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'), + + [Parameter()] + [System.Uri] + $Proxy, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ProxyCredential, + + [Parameter()] + [ValidateSet('CurrentUser', 'AllUsers')] + [System.String] + $Scope = 'CurrentUser', + + [Parameter()] + [System.String] + $Gallery = 'PSGallery', + + [Parameter()] + [System.Management.Automation.PSCredential] + $GalleryCredential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowOldPowerShellGetModule, + + [Parameter()] + [System.String] + $MinimumPSDependVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AllowPrerelease, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $WithYAML, + + [Parameter()] + [System.Collections.Hashtable] + $RegisterGallery, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ModuleFastBleedingEdge, + + [Parameter()] + [System.String] + $ModuleFastVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.String] + $PSResourceGetVersion, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule, + + [Parameter()] + [System.String] + $UsePowerShellGetCompatibilityModuleVersion +) + +try +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + if (-not (Get-Command -Name 'Import-PowerShellDataFile' -ErrorAction 'SilentlyContinue')) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0' + } + } + + Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.' + + $resolveDependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath '.\Resolve-Dependency.psd1' -Resolve -ErrorAction 'Stop' + + $resolveDependencyDefaults = Import-PowerShellDataFile -Path $resolveDependencyConfigPath + + $parameterToDefault = $MyInvocation.MyCommand.ParameterSets.Where{ $_.Name -eq $PSCmdlet.ParameterSetName }.Parameters.Keys + + if ($parameterToDefault.Count -eq 0) + { + $parameterToDefault = $MyInvocation.MyCommand.Parameters.Keys + } + + # Set the parameters available in the Parameter Set, or it's not possible to choose yet, so all parameters are an option. + foreach ($parameterName in $parameterToDefault) + { + if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName)) + { + Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'." + + try + { + $variableValue = $resolveDependencyDefaults[$parameterName] + + if ($variableValue -is [System.String]) + { + $variableValue = $ExecutionContext.InvokeCommand.ExpandString($variableValue) + } + + $PSBoundParameters.Add($parameterName, $variableValue) + + Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue' + } + catch + { + Write-Verbose -Message "Error adding default for $parameterName : $($_.Exception.Message)." + } + } + } +} +catch +{ + Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)." +} + +# Handle when both ModuleFast and PSResourceGet is configured or/and passed as parameter. +if ($UseModuleFast -and $UsePSResourceGet) +{ + Write-Information -MessageData 'Both ModuleFast and PSResourceGet is configured or/and passed as parameter.' -InformationAction 'Continue' + + if ($PSVersionTable.PSVersion -ge '7.2') + { + $UsePSResourceGet = $false + + Write-Information -MessageData 'PowerShell 7.2 or higher being used, prefer ModuleFast over PSResourceGet.' -InformationAction 'Continue' + } + else + { + $UseModuleFast = $false + + Write-Information -MessageData 'Windows PowerShell or PowerShell <=7.1 is being used, prefer PSResourceGet since ModuleFast is not supported on this version of PowerShell.' -InformationAction 'Continue' + } +} + +# Only bootstrap ModuleFast if it is not already imported. +if ($UseModuleFast -and -not (Get-Module -Name 'ModuleFast')) +{ + try + { + $moduleFastBootstrapScriptBlockParameters = @{} + + if ($ModuleFastBleedingEdge) + { + Write-Information -MessageData 'ModuleFast is configured to use Bleeding Edge (directly from ModuleFast''s main branch).' -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.UseMain = $true + } + elseif ($ModuleFastVersion) + { + if ($ModuleFastVersion -notmatch 'v') + { + $ModuleFastVersion = 'v{0}' -f $ModuleFastVersion + } + + Write-Information -MessageData ('ModuleFast is configured to use version {0}.' -f $ModuleFastVersion) -InformationAction 'Continue' + + $moduleFastBootstrapScriptBlockParameters.Release = $ModuleFastVersion + } + else + { + Write-Information -MessageData 'ModuleFast is configured to use latest released version.' -InformationAction 'Continue' + } + + $moduleFastBootstrapUri = 'bit.ly/modulefast' # cSpell: disable-line + + Write-Debug -Message ('Using bootstrap script at {0}' -f $moduleFastBootstrapUri) + + $invokeWebRequestParameters = @{ + Uri = $moduleFastBootstrapUri + ErrorAction = 'Stop' + } + + $moduleFastBootstrapScript = Invoke-WebRequest @invokeWebRequestParameters + + $moduleFastBootstrapScriptBlock = [ScriptBlock]::Create($moduleFastBootstrapScript) + + & $moduleFastBootstrapScriptBlock @moduleFastBootstrapScriptBlockParameters + } + catch + { + Write-Warning -Message ('ModuleFast could not be bootstrapped. Reverting to PSResourceGet. Error: {0}' -f $_.Exception.Message) + + $UseModuleFast = $false + $UsePSResourceGet = $true + } +} + +if ($UsePSResourceGet) +{ + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + # If PSResourceGet was used prior it will be locked and we can't replace it. + if ((Test-Path -Path "$PSDependTarget/$psResourceGetModuleName" -PathType 'Container') -and (Get-Module -Name $psResourceGetModuleName)) + { + Write-Information -MessageData ('{0} is already bootstrapped and imported into the session. If there is a need to refresh the module, open a new session and resolve dependencies again.' -f $psResourceGetModuleName) -InformationAction 'Continue' + } + else + { + Write-Debug -Message ('{0} do not exist, saving the module to RequiredModules.' -f $psResourceGetModuleName) + + $psResourceGetDownloaded = $false + + try + { + if (-not $PSResourceGetVersion) + { + # Default to latest version if no version is passed in parameter or specified in configuration. + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName" + } + else + { + $psResourceGetUri = "https://www.powershellgallery.com/api/v2/package/$psResourceGetModuleName/$PSResourceGetVersion" + } + + $invokeWebRequestParameters = @{ + # TODO: Should support proxy parameters passed to the script. + Uri = $psResourceGetUri + OutFile = "$PSDependTarget/$psResourceGetModuleName.nupkg" # cSpell: ignore nupkg + ErrorAction = 'Stop' + } + + $previousProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + # Bootstrapping Microsoft.PowerShell.PSResourceGet. + Invoke-WebRequest @invokeWebRequestParameters + + $ProgressPreference = $previousProgressPreference + + $psResourceGetDownloaded = $true + } + catch + { + Write-Warning -Message ('{0} could not be bootstrapped. Reverting to PowerShellGet. Error: {1}' -f $psResourceGetModuleName, $_.Exception.Message) + } + + $UsePSResourceGet = $false + + if ($psResourceGetDownloaded) + { + # On Windows PowerShell the command Expand-Archive do not like .nupkg as a zip archive extension. + $zipFileName = ((Split-Path -Path $invokeWebRequestParameters.OutFile -Leaf) -replace 'nupkg', 'zip') + + $renameItemParameters = @{ + Path = $invokeWebRequestParameters.OutFile + NewName = $zipFileName + Force = $true + } + + Rename-Item @renameItemParameters + + $psResourceGetZipArchivePath = Join-Path -Path (Split-Path -Path $invokeWebRequestParameters.OutFile -Parent) -ChildPath $zipFileName + + $expandArchiveParameters = @{ + Path = $psResourceGetZipArchivePath + DestinationPath = "$PSDependTarget/$psResourceGetModuleName" + Force = $true + } + + Expand-Archive @expandArchiveParameters + + Remove-Item -Path $psResourceGetZipArchivePath + + Import-Module -Name $expandArchiveParameters.DestinationPath -Force + + # Successfully bootstrapped PSResourceGet, so let's use it. + $UsePSResourceGet = $true + } + } + + if ($UsePSResourceGet) + { + $psResourceGetModule = Get-Module -Name $psResourceGetModuleName + + $psResourceGetModuleVersion = $psResourceGetModule.Version.ToString() + + if ($psResourceGetModule.PrivateData.PSData.Prerelease) + { + $psResourceGetModuleVersion += '-{0}' -f $psResourceGetModule.PrivateData.PSData.Prerelease + } + + Write-Information -MessageData ('Using {0} v{1}.' -f $psResourceGetModuleName, $psResourceGetModuleVersion) -InformationAction 'Continue' + + if ($UsePowerShellGetCompatibilityModule) + { + $savePowerShellGetParameters = @{ + Name = 'PowerShellGet' + Path = $PSDependTarget + Repository = 'PSGallery' + TrustRepository = $true + } + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $savePowerShellGetParameters.Version = $UsePowerShellGetCompatibilityModuleVersion + + # Check if the version is a prerelease. + if ($UsePowerShellGetCompatibilityModuleVersion -match '\d+\.\d+\.\d+-.*') + { + $savePowerShellGetParameters.Prerelease = $true + } + } + + Save-PSResource @savePowerShellGetParameters + + Import-Module -Name "$PSDependTarget/PowerShellGet" + } + } +} + +# Check if legacy PowerShellGet and PSDepend must be bootstrapped. +if (-not ($UseModuleFast -or $UsePSResourceGet)) +{ + if ($PSVersionTable.PSVersion.Major -le 5) + { + <# + Making sure the imported PackageManagement module is not from PS7 module + path. The VSCode PS extension is changing the $env:PSModulePath and + prioritize the PS7 path. This is an issue with PowerShellGet because + it loads an old version if available (or fail to load latest). + #> + Get-Module -ListAvailable PackageManagement | + Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | + Select-Object -First 1 | + Import-Module -Force + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' + + $importModuleParameters = @{ + Name = 'PowerShellGet' + MinimumVersion = '2.0' + MaximumVersion = '2.8.999' + ErrorAction = 'SilentlyContinue' + PassThru = $true + } + + if ($AllowOldPowerShellGetModule) + { + $importModuleParameters.Remove('MinimumVersion') + } + + $powerShellGetModule = Import-Module @importModuleParameters + + # Install the package provider if it is not available. + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' | + Select-Object -First 1 + + if (-not $powerShellGetModule -and -not $nuGetProvider) + { + $providerBootstrapParameters = @{ + Name = 'NuGet' + Force = $true + ForceBootstrap = $true + ErrorAction = 'Stop' + Scope = $Scope + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $providerBootstrapParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'AllowPrerelease' + { + $providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease) + } + } + + Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' + + $null = Install-PackageProvider @providerBootstrapParameters + + $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 + + $nuGetProviderVersion = $nuGetProvider.Version.ToString() + + Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." + + $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force + } + + if ($RegisterGallery) + { + if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name)) + { + $Gallery = $RegisterGallery.Name + } + else + { + $RegisterGallery.Name = $Gallery + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed + + $previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue' + + if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation) + { + if ($previousRegisteredRepository) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed + + Unregister-PSRepository -Name $Gallery + + $unregisteredPreviousRepository = $true + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed + } + + Register-PSRepository @RegisterGallery + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" + + # Fail if the given PSGallery is not registered. + $previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').Trusted + + $updatedGalleryInstallationPolicy = $false + + if ($previousGalleryInstallationPolicy -ne $true) + { + $updatedGalleryInstallationPolicy = $true + + # Only change policy if the repository is not trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' + } +} + +try +{ + # Check if legacy PowerShellGet and PSDepend must be used. + if (-not ($UseModuleFast -or $UsePSResourceGet)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' + + # Ensure the module is loaded and retrieve the version you have. + $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version + + Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" + + # Versions below 2.0 are considered old, unreliable & not recommended + if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet' + + # PowerShellGet module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery." + + Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope." + + $installPowerShellGetParameters = @{ + Name = 'PowerShellGet' + Force = $true + SkipPublisherCheck = $true + AllowClobber = $true + Scope = $Scope + Repository = $Gallery + MaximumVersion = '2.8.999' + } + + switch ($PSBoundParameters.Keys) + { + 'Proxy' + { + $installPowerShellGetParameters.Add('Proxy', $Proxy) + } + + 'ProxyCredential' + { + $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) + } + + 'GalleryCredential' + { + $installPowerShellGetParameters.Add('Credential', $GalleryCredential) + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' + + Install-Module @installPowerShellGetParameters + } + else + { + Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PowerShellGet' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + MaximumVersion = '2.8.999' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope" + + Save-Module @saveModuleParameters + } + + Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session' + + Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue' + Get-Module -Name 'PackageManagement' -All | Remove-Module -Force + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet' + + Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session' + + if ($AllowOldPowerShellGetModule) + { + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru + } + else + { + Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force + + $powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru + } + + $powerShellGetVersion = $powerShellGetModule.Version.ToString() + + Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" + } + + # Try to import the PSDepend module from the available modules. + $getModuleParameters = @{ + Name = 'PSDepend' + ListAvailable = $true + } + + $psDependModule = Get-Module @getModuleParameters + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + try + { + $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } + } + catch + { + throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) + } + } + + if (-not $psDependModule) + { + Write-Debug -Message 'PSDepend module not found.' + + # PSDepend module not found, installing or saving it. + if ($PSDependTarget -in 'CurrentUser', 'AllUsers') + { + Write-Debug -Message "Attempting to install from Gallery '$Gallery'." + + Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." + + $installPSDependParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Force = $true + Scope = $PSDependTarget + SkipPublisherCheck = $true + AllowClobber = $true + } + + if ($MinimumPSDependVersion) + { + $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" + + Install-Module @installPSDependParameters + } + else + { + Write-Debug -Message "Attempting to Save from Gallery $Gallery to $PSDependTarget" + + $saveModuleParameters = @{ + Name = 'PSDepend' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + if ($MinimumPSDependVersion) + { + $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $PSDependTarget" + + Save-Module @saveModuleParameters + } + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Importing PSDepend' + + $importModulePSDependParameters = @{ + Name = 'PSDepend' + ErrorAction = 'Stop' + Force = $true + } + + if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) + { + $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) + } + + # We should have successfully bootstrapped PSDepend. Fail if not available. + $null = Import-Module @importModulePSDependParameters + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 81 -CurrentOperation 'Invoke PSDepend' + + if ($WithYAML) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' + + if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' + + Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'." + + $SaveModuleParam = @{ + Name = 'PowerShell-Yaml' + Repository = $Gallery + Path = $PSDependTarget + Force = $true + } + + Save-Module @SaveModuleParam + } + else + { + Write-Verbose -Message 'PowerShell-Yaml is already available' + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml' + } + } + + if (Test-Path -Path $DependencyFile) + { + if ($UseModuleFast -or $UsePSResourceGet) + { + $requiredModules = Import-PowerShellDataFile -Path $DependencyFile + + $requiredModules = $requiredModules.GetEnumerator() | + Where-Object -FilterScript { $_.Name -ne 'PSDependOptions' } + + if ($UseModuleFast) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking ModuleFast' + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $modulesToSave = @( + 'PSDepend' # Always include PSDepend for backward compatibility. + ) + + if ($WithYAML) + { + $modulesToSave += 'PowerShell-Yaml' + } + + if ($UsePowerShellGetCompatibilityModule) + { + Write-Debug -Message 'PowerShellGet compatibility module is configured to be used.' + + # This is needed to ensure that the PowerShellGet compatibility module works. + $psResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' + + if ($PSResourceGetVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $psResourceGetModuleName, $PSResourceGetVersion) + } + else + { + $modulesToSave += $psResourceGetModuleName + } + + $powerShellGetCompatibilityModuleName = 'PowerShellGet' + + if ($UsePowerShellGetCompatibilityModuleVersion) + { + $modulesToSave += ('{0}:[{1}]' -f $powerShellGetCompatibilityModuleName, $UsePowerShellGetCompatibilityModuleVersion) + } + else + { + $modulesToSave += $powerShellGetCompatibilityModuleName + } + } + + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + if (-not $requiredModule.Value.Version) + { + $requiredModuleVersion = 'latest' + } + else + { + $requiredModuleVersion = $requiredModule.Value.Version + } + + if ($requiredModuleVersion -eq 'latest') + { + $moduleNameSuffix = '' + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + <# + Adding '!' to the module name indicate to ModuleFast + that is should also evaluate pre-releases. + #> + $moduleNameSuffix = '!' + } + + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $moduleNameSuffix) + } + else + { + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModuleVersion) + } + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += $requiredModule.Name + } + else + { + # Handle different nuget version operators already present. + if ($requiredModule.Value -match '[!|:|[|(|,|>|<|=]') + { + $modulesToSave += ('{0}{1}' -f $requiredModule.Name, $requiredModule.Value) + } + else + { + # Assuming the version is a fixed version. + $modulesToSave += ('{0}:[{1}]' -f $requiredModule.Name, $requiredModule.Value) + } + } + } + } + + Write-Debug -Message ("Required modules to retrieve plan for:`n{0}" -f ($modulesToSave | Out-String)) + + $installModuleFastParameters = @{ + Destination = $PSDependTarget + DestinationOnly = $true + NoPSModulePathUpdate = $true + NoProfileUpdate = $true + Update = $true + Confirm = $false + } + + $moduleFastPlan = Install-ModuleFast -Specification $modulesToSave -Plan @installModuleFastParameters + + Write-Debug -Message ("Missing modules that need to be saved:`n{0}" -f ($moduleFastPlan | Out-String)) + + if ($moduleFastPlan) + { + # Clear all modules in plan from the current session so they can be fetched again. + $moduleFastPlan.Name | Get-Module | Remove-Module -Force + + $moduleFastPlan | Install-ModuleFast @installModuleFastParameters + } + else + { + Write-Verbose -Message 'All required modules were already up to date' + } + + Write-Progress -Activity 'ModuleFast:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + + if ($UsePSResourceGet) + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSResourceGet' + + $modulesToSave = @( + @{ + Name = 'PSDepend' # Always include PSDepend for backward compatibility. + Version = '0.3.8' + } + ) + + if ($WithYAML) + { + $modulesToSave += @{ + Name = 'PowerShell-Yaml' + Version = '0.4.12' + } + } + + # Prepare hashtable that can be concatenated to the Save-PSResource parameters. + foreach ($requiredModule in $requiredModules) + { + # If the RequiredModules.psd1 entry is an Hashtable then special handling is needed. + if ($requiredModule.Value -is [System.Collections.Hashtable]) + { + $saveModuleHashtable = @{ + Name = $requiredModule.Name + } + + if ($requiredModule.Value.Version -and $requiredModule.Value.Version -ne 'latest') + { + $saveModuleHashtable.Version = $requiredModule.Value.Version + } + + if ($requiredModule.Value.Parameters.AllowPrerelease -eq $true) + { + $saveModuleHashtable.Prerelease = $true + } + + $modulesToSave += $saveModuleHashtable + } + else + { + if ($requiredModule.Value -eq 'latest') + { + $modulesToSave += @{ + Name = $requiredModule.Name + } + } + else + { + $modulesToSave += @{ + Name = $requiredModule.Name + Version = $requiredModule.Value + } + } + } + } + + $percentagePerModule = [System.Math]::Floor(100 / $modulesToSave.Length) + + $progressPercentage = 0 + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' + + foreach ($currentModule in $modulesToSave) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Saving module {0}' -f $savePSResourceParameters.Name) + + $savePSResourceParameters = @{ + Path = $PSDependTarget + TrustRepository = $true + Confirm = $false + } + + # Concatenate the module parameters to the Save-PSResource parameters. + $savePSResourceParameters += $currentModule + + # Modules that Sampler depend on that cannot be refreshed without a new session. + $skipModule = @('PowerShell-Yaml') + + if ($savePSResourceParameters.Name -in $skipModule -and (Get-Module -Name $savePSResourceParameters.Name)) + { + Write-Progress -Activity 'PSResourceGet:' -PercentComplete $progressPercentage -CurrentOperation 'Restoring Build Dependencies' -Status ('Skipping module {0}' -f $savePSResourceParameters.Name) + + Write-Information -MessageData ('Skipping the module {0} since it cannot be refresh while loaded into the session. To refresh the module open a new session and resolve dependencies again.' -f $savePSResourceParameters.Name) -InformationAction 'Continue' + } + else + { + # Clear all module from the current session so any new version fetched will be re-imported. + Get-Module -Name $savePSResourceParameters.Name | Remove-Module -Force + + Save-PSResource @savePSResourceParameters -ErrorVariable 'savePSResourceError' + + if ($savePSResourceError) + { + Write-Warning -Message 'Save-PSResource could not save (replace) one or more dependencies. This can be due to the module is loaded into the session (and referencing assemblies). Close the current session and open a new session and try again.' + } + } + + $progressPercentage += $percentagePerModule + } + + Write-Progress -Activity 'PSResourceGet:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + else + { + Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoking PSDepend' + + Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' + + $psDependParameters = @{ + Force = $true + Path = $DependencyFile + } + + # TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified. + Invoke-PSDepend @psDependParameters + + Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed + } + } + else + { + Write-Warning -Message "The dependency file '$DependencyFile' could not be found." + } + + Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed +} +finally +{ + if ($RegisterGallery) + { + Write-Verbose -Message "Removing private package repository '$Gallery'." + Unregister-PSRepository -Name $Gallery + } + + if ($unregisteredPreviousRepository) + { + Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s." + + $registerPSRepositoryParameters = @{ + Name = $previousRegisteredRepository.Name + InstallationPolicy = $previousRegisteredRepository.InstallationPolicy + } + + if ($previousRegisteredRepository.SourceLocation) + { + $registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation + } + + if ($previousRegisteredRepository.PublishLocation) + { + $registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation + } + + if ($previousRegisteredRepository.ScriptSourceLocation) + { + $registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation + } + + if ($previousRegisteredRepository.ScriptPublishLocation) + { + $registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation + } + + Register-PSRepository @registerPSRepositoryParameters + } + + if ($updatedGalleryInstallationPolicy -eq $true -and $previousGalleryInstallationPolicy -ne $true) + { + # Only try to revert installation policy if the repository exist + if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue')) + { + # Reverting the Installation Policy for the given gallery if it was not already trusted + Set-PSRepository -Name $Gallery -InstallationPolicy 'Untrusted' + } + } + + Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.' +} diff --git a/resources/Microsoft.Windows.Settings/Resolve-Dependency.psd1 b/resources/Microsoft.Windows.Settings/Resolve-Dependency.psd1 new file mode 100644 index 00000000..c72178de --- /dev/null +++ b/resources/Microsoft.Windows.Settings/Resolve-Dependency.psd1 @@ -0,0 +1,76 @@ +@{ + <# + Default parameter values to be loaded by the Resolve-Dependency.ps1 script (unless set in bound parameters + when calling the script). + #> + + #PSDependTarget = './output/modules' + #Proxy = '' + #ProxyCredential = '$MyCredentialVariable' #TODO: find a way to support credentials in build (resolve variable) + + Gallery = 'PSGallery' + + # To use a private nuget repository change the following to your own feed. The locations must be a Nuget v2 feed due + # to limitation in PowerShellGet v2.x. Example below is for a Azure DevOps Server project-scoped feed. While resolving + # dependencies it will be registered as a trusted repository with the name specified in the property 'Gallery' above, + # unless property 'Name' is provided in the hashtable below, if so it will override the property 'Gallery' above. The + # registered repository will be removed when dependencies has been resolved, unless it was already registered to begin + # with. If repository is registered already but with different URL:s the repository will be re-registered and reverted + # after dependencies has been resolved. Currently only Windows integrated security works with private Nuget v2 feeds + # (or if it is a public feed with no security), it is not possible yet to securely provide other credentials for the feed. + # Private repositories will currently only work using PowerShellGet. + #RegisterGallery = @{ + # #Name = 'MyPrivateFeedName' + # GallerySourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryScriptSourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # GalleryScriptPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' + # #InstallationPolicy = 'Trusted' + #} + + #AllowOldPowerShellGetModule = $true + #MinimumPSDependVersion = '0.3.0' + AllowPrerelease = $false + WithYAML = $true # Will also bootstrap PowerShell-Yaml to read other config files + + <# + Enable ModuleFast to be the default method of resolving dependencies by setting + UseModuleFast to the value $true. ModuleFast requires PowerShell 7.2 or higher. + If UseModuleFast is not configured or set to $false then PowerShellGet (or + PSResourceGet if enabled) will be used to as the default method of resolving + dependencies. You can always use the parameter `-UseModuleFast` of the + Resolve-Dependency.ps1 or build.ps1 script even when this is not configured + or set to $false. + + You can use ModuleFastVersion to specify a specific version of ModuleFast to use. + This will also affect the use of parameter `-UseModuleFast` of the Resolve-Dependency.ps1 + or build.ps1 script. If ModuleFastVersion is not configured then the latest + (non-preview) released version is used. + + ModuleFastBleedingEdge will override ModuleFastVersion and use the absolute latest + code from the ModuleFast repository. This is useful if you want to test the absolute + latest changes in ModuleFast repository. This is not recommended for production use. + By enabling ModuleFastBleedingEdge the pipeline can encounter breaking changes or + problems by code that is merged in the ModuleFast repository, this could affect the + pipeline negatively. Make sure to use a clean PowerShell session after changing + the value of ModuleFastBleedingEdge so that ModuleFast uses the correct bootstrap + script and correct parameter values. This will also affect the use of parameter + `-UseModuleFast` of the Resolve-Dependency.ps1 or build.ps1 script. + #> + #UseModuleFast = $true + #ModuleFastVersion = '0.1.2' + #ModuleFastBleedingEdge = $true + + <# + Enable PSResourceGet to be the default method of resolving dependencies by setting + UsePSResourceGet to the value $true. If UsePSResourceGet is not configured or + set to $false then PowerShellGet will be used to resolve dependencies. + #> + UsePSResourceGet = $true + PSResourceGetVersion = '1.0.1' + + # PowerShellGet compatibility module only works when using PSResourceGet or ModuleFast. + UsePowerShellGetCompatibilityModule = $true + UsePowerShellGetCompatibilityModuleVersion = '3.0.23-beta23' +} + diff --git a/resources/Microsoft.Windows.Settings/azure-pipelines.yml b/resources/Microsoft.Windows.Settings/azure-pipelines.yml new file mode 100644 index 00000000..f132bf4f --- /dev/null +++ b/resources/Microsoft.Windows.Settings/azure-pipelines.yml @@ -0,0 +1,292 @@ +trigger: + branches: + include: + - main + paths: + include: + - source/* + tags: + include: + - "v*" + exclude: + - "*-*" + +variables: + buildFolderName: output + buildArtifactName: output + testResultFolderName: testResults + testArtifactName: testResults + sourceFolderName: source + defaultBranch: main + Agent.Source.Git.ShallowFetchDepth: 0 # override ShallowFetchDepth + +stages: + - stage: Build + jobs: + - job: Package_Module + displayName: 'Package Module' + pool: + vmImage: 'ubuntu-latest' + steps: + - pwsh: | + dotnet tool install --global GitVersion.Tool --version 5.* + $gitVersionObject = dotnet-gitversion | ConvertFrom-Json + $gitVersionObject.PSObject.Properties.ForEach{ + Write-Host -Object "Setting Task Variable '$($_.Name)' with value '$($_.Value)'." + Write-Host -Object "##vso[task.setvariable variable=$($_.Name);]$($_.Value)" + } + Write-Host -Object "##vso[build.updatebuildnumber]$($gitVersionObject.FullSemVer)" + displayName: Calculate ModuleVersion (GitVersion) + - task: PowerShell@2 + name: package + displayName: 'Build & Package Module' + inputs: + filePath: './build.ps1' + arguments: '-ResolveDependency -tasks pack' + pwsh: true + env: + ModuleVersion: $(NuGetVersionV2) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Build Artifact' + inputs: + targetPath: '$(buildFolderName)/' + artifact: $(buildArtifactName) + publishLocation: 'pipeline' + parallel: true + + - stage: Test + dependsOn: Build + jobs: + - job: Test_HQRM + displayName: 'HQRM' + pool: + vmImage: 'windows-latest' + timeoutInMinutes: 0 + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run HQRM Test' + inputs: + filePath: './build.ps1' + arguments: '-Tasks hqrmtest' + pwsh: false + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: 'output/testResults/NUnit*.xml' + testRunTitle: 'HQRM' + + # If several pipeline jobs are running test, consider renaming this job: + - job: Test_Unit + displayName: 'Unit' + pool: + vmImage: 'windows-latest' + timeoutInMinutes: 0 + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: test + displayName: 'Run Unit Test' + inputs: + filePath: './build.ps1' + arguments: "-Tasks test -PesterScript 'tests/Unit'" + pwsh: true + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + # If several pipeline jobs are generating test result, consider renaming this title: + testRunTitle: 'Unit' + - task: PublishPipelineArtifact@1 + displayName: 'Publish Test Artifact' + inputs: + targetPath: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: $(testArtifactName) + # If several pipeline jobs are generating code coverage, replace above with this: + #artifactName: 'CodeCoverageWindows' # Can be any, in the pipeline, unique name + parallel: true + + # If several pipeline jobs should run unit tests, uncomment this: + ## If several pipeline jobs are generating test result consider renaming this: + #- job: Test_Unit_Linux + # displayName: 'Unit Linux' + # pool: + # vmImage: 'ubuntu-latest' + # timeoutInMinutes: 0 + # steps: + # - task: DownloadPipelineArtifact@2 + # displayName: 'Download Build Artifact' + # inputs: + # buildType: 'current' + # artifactName: $(buildArtifactName) + # targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + # - task: PowerShell@2 + # name: test + # displayName: 'Run Unit Test' + # inputs: + # filePath: './build.ps1' + # arguments: "-Tasks test -PesterScript 'tests/Unit'" + # pwsh: true + # - task: PublishTestResults@2 + # displayName: 'Publish Test Results' + # condition: succeededOrFailed() + # inputs: + # testResultsFormat: 'NUnit' + # testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + # # If several pipeline jobs are generating test result consider renaming this: + # testRunTitle: 'Unit Linux' + # - task: PublishPipelineArtifact@1 + # displayName: 'Publish Test Artifact' + # inputs: + # targetPath: '$(buildFolderName)/$(testResultFolderName)/' + # # If several pipeline jobs are generating code coverage, consider renaming this: + # artifactName: 'CodeCoverageLinux' # Can be any, in the pipeline, unique name + # parallel: true + + - job: Test_Integration + displayName: 'Integration' + pool: + vmImage: 'windows-latest' + timeoutInMinutes: 0 + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: PowerShell@2 + name: configureWinRM + displayName: 'Configure WinRM' + inputs: + targetType: 'inline' + script: 'winrm quickconfig -quiet' + pwsh: false + - task: PowerShell@2 + name: test + displayName: 'Run Integration Test' + inputs: + filePath: './build.ps1' + arguments: "-Tasks test -CodeCoverageThreshold 0 -PesterScript 'tests/Integration'" + pwsh: false + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Integration' + + - job: Code_Coverage + displayName: 'Publish Code Coverage' + dependsOn: Test_Unit + pool: + vmImage: 'ubuntu-latest' + timeoutInMinutes: 0 + steps: + - pwsh: | + $repositoryOwner,$repositoryName = $env:BUILD_REPOSITORY_NAME -split '/' + echo "##vso[task.setvariable variable=RepositoryOwner;isOutput=true]$repositoryOwner" + echo "##vso[task.setvariable variable=RepositoryName;isOutput=true]$repositoryName" + name: dscBuildVariable + displayName: 'Set Environment Variables' + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' + - task: DownloadPipelineArtifact@2 + # If several pipeline jobs are generating code coverage, consider renaming this display name: + displayName: 'Download Test Artifact' + inputs: + buildType: 'current' + # If several pipeline jobs are generating code coverage, set the correct artifact name: + artifactName: $(testArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + # If several pipeline jobs are generating code coverage, uncomment these tasks: + #- task: DownloadPipelineArtifact@2 + # displayName: 'Download Test Artifact Linux' + # inputs: + # buildType: 'current' + # artifactName: 'CodeCoverageLinux' + # targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)' + #- task: PowerShell@2 # Merges the code coverage files. + # name: merge + # displayName: 'Merge Code Coverage files' + # inputs: + # filePath: './build.ps1' + # arguments: '-tasks merge' + # pwsh: true + - task: PublishCodeCoverageResults@1 + displayName: 'Publish Code Coverage to Azure DevOps' + inputs: + codeCoverageTool: 'JaCoCo' + summaryFileLocation: '$(Build.SourcesDirectory)/$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml' + pathToSources: '$(Build.SourcesDirectory)/$(sourceFolderName)/' + - script: | + bash <(curl -s https://codecov.io/bash) -f "./$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml" + displayName: 'Publish Code Coverage to Codecov.io' + + - stage: Deploy + dependsOn: Test + condition: | + and( + succeeded(), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/tags/') + ), + contains(variables['System.TeamFoundationCollectionUri'], 'microsoft') + ) + jobs: + - job: Deploy_Module + displayName: 'Deploy Module' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Pipeline Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildArtifactName)' + - task: PowerShell@2 + name: publishRelease + displayName: 'Publish Release' + inputs: + filePath: './build.ps1' + arguments: '-tasks publish' + pwsh: true + env: + GitHubToken: $(GitHubToken) + GalleryApiToken: $(GalleryApiToken) + ReleaseBranch: $(defaultBranch) + MainGitBranch: $(defaultBranch) + - task: PowerShell@2 + name: sendChangelogPR + displayName: 'Send Changelog PR' + inputs: + filePath: './build.ps1' + arguments: '-tasks Create_ChangeLog_GitHub_PR' + pwsh: true + env: + GitHubToken: $(GitHubToken) + ReleaseBranch: $(defaultBranch) + MainGitBranch: $(defaultBranch) + diff --git a/resources/Microsoft.Windows.Settings/build.ps1 b/resources/Microsoft.Windows.Settings/build.ps1 new file mode 100644 index 00000000..f4a0faec --- /dev/null +++ b/resources/Microsoft.Windows.Settings/build.ps1 @@ -0,0 +1,538 @@ +<# + .DESCRIPTION + Bootstrap and build script for PowerShell module CI/CD pipeline. + + .PARAMETER Tasks + The task or tasks to run. The default value is '.' (runs the default task). + + .PARAMETER CodeCoverageThreshold + The code coverage target threshold to uphold. Set to 0 to disable. + The default value is '' (empty string). + + .PARAMETER BuildConfig + Not yet written. + + .PARAMETER OutputDirectory + Specifies the folder to build the artefact into. The default value is 'output'. + + .PARAMETER BuiltModuleSubdirectory + Subdirectory name to build the module (under $OutputDirectory). The default + value is '' (empty string). + + .PARAMETER RequiredModulesDirectory + Can be a path (relative to $PSScriptRoot or absolute) to tell Resolve-Dependency + and PSDepend where to save the required modules. It is also possible to use + 'CurrentUser' och 'AllUsers' to install missing dependencies. You can override + the value for PSDepend in the Build.psd1 build manifest. The default value is + 'output/RequiredModules'. + + .PARAMETER PesterScript + One or more paths that will override the Pester configuration in build + configuration file when running the build task Invoke_Pester_Tests. + + If running Pester 5 test, use the alias PesterPath to be future-proof. + + .PARAMETER PesterTag + Filter which tags to run when invoking Pester tests. This is used in the + Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER PesterExcludeTag + Filter which tags to exclude when invoking Pester tests. This is used in + the Invoke-Pester.pester.build.ps1 tasks. + + .PARAMETER DscTestTag + Filter which tags to run when invoking DSC Resource tests. This is used + in the DscResource.Test.build.ps1 tasks. + + .PARAMETER DscTestExcludeTag + Filter which tags to exclude when invoking DSC Resource tests. This is + used in the DscResource.Test.build.ps1 tasks. + + .PARAMETER ResolveDependency + Not yet written. + + .PARAMETER BuildInfo + The build info object from ModuleBuilder. Defaults to an empty hashtable. + + .PARAMETER AutoRestore + Not yet written. + + .PARAMETER UseModuleFast + Specifies to use ModuleFast instead of PowerShellGet to resolve dependencies + faster. + + .PARAMETER UsePSResourceGet + Specifies to use PSResourceGet instead of PowerShellGet to resolve dependencies + faster. This can also be configured in Resolve-Dependency.psd1. + + .PARAMETER UsePowerShellGetCompatibilityModule + Specifies to use the compatibility module PowerShellGet. This parameter + only works then the method of downloading dependencies is PSResourceGet. + This can also be configured in Resolve-Dependency.psd1. +#> +[CmdletBinding()] +param +( + [Parameter(Position = 0)] + [System.String[]] + $Tasks = '.', + + [Parameter()] + [System.String] + $CodeCoverageThreshold = '', + + [Parameter()] + [System.String] + [ValidateScript( + { Test-Path -Path $_ } + )] + $BuildConfig, + + [Parameter()] + [System.String] + $OutputDirectory = 'output', + + [Parameter()] + [System.String] + $BuiltModuleSubdirectory = '', + + [Parameter()] + [System.String] + $RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'), + + [Parameter()] + # This alias is to prepare for the rename of this parameter to PesterPath when Pester 4 support is removed + [Alias('PesterPath')] + [System.Object[]] + $PesterScript, + + [Parameter()] + [System.String[]] + $PesterTag, + + [Parameter()] + [System.String[]] + $PesterExcludeTag, + + [Parameter()] + [System.String[]] + $DscTestTag, + + [Parameter()] + [System.String[]] + $DscTestExcludeTag, + + [Parameter()] + [Alias('bootstrap')] + [System.Management.Automation.SwitchParameter] + $ResolveDependency, + + [Parameter(DontShow)] + [AllowNull()] + [System.Collections.Hashtable] + $BuildInfo, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $AutoRestore, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseModuleFast, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePSResourceGet, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UsePowerShellGetCompatibilityModule +) + +<# + The BEGIN block (at the end of this file) handles the Bootstrap of the Environment + before Invoke-Build can run the tasks if the parameter ResolveDependency (or + parameter alias Bootstrap) is specified. +#> + +process +{ + if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') + { + # Only run the process block through InvokeBuild (look at the Begin block at the bottom of this script). + return + } + + # Execute the Build process from the .build.ps1 path. + Push-Location -Path $PSScriptRoot -StackName 'BeforeBuild' + + try + { + Write-Host -Object "[build] Parsing defined tasks" -ForeGroundColor Magenta + + # Load the default BuildInfo if the parameter BuildInfo is not set. + if (-not $PSBoundParameters.ContainsKey('BuildInfo')) + { + try + { + if (Test-Path -Path $BuildConfig) + { + $configFile = Get-Item -Path $BuildConfig + + Write-Host -Object "[build] Loading Configuration from $configFile" + + $BuildInfo = switch -Regex ($configFile.Extension) + { + # Native Support for PSD1 + '\.psd1' + { + if (-not (Get-Command -Name Import-PowerShellDataFile -ErrorAction SilentlyContinue)) + { + Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion 3.1.0.0 + } + + Import-PowerShellDataFile -Path $BuildConfig + } + + # Support for yaml when module PowerShell-Yaml is available + '\.[yaml|yml]' + { + Import-Module -Name 'powershell-yaml' -ErrorAction Stop + + ConvertFrom-Yaml -Yaml (Get-Content -Raw $configFile) + } + + # Support for JSON and JSONC (by Removing comments) when module PowerShell-Yaml is available + '\.[json|jsonc]' + { + $jsonFile = Get-Content -Raw -Path $configFile + + $jsonContent = $jsonFile -replace '(?m)\s*//.*?$' -replace '(?ms)/\*.*?\*/' + + # Yaml is superset of JSON. + ConvertFrom-Yaml -Yaml $jsonContent + } + + # Unknown extension, return empty hashtable. + default + { + Write-Error -Message "Extension '$_' not supported. using @{}" + + @{ } + } + } + } + else + { + Write-Host -Object "Configuration file '$($BuildConfig.FullName)' not found" -ForegroundColor Red + + # No config file was found, return empty hashtable. + $BuildInfo = @{ } + } + } + catch + { + $logMessage = "Error loading Config '$($BuildConfig.FullName)'.`r`nAre you missing dependencies?`r`nMake sure you run './build.ps1 -ResolveDependency -tasks noop' before running build to restore the required modules." + + Write-Host -Object $logMessage -ForegroundColor Yellow + + $BuildInfo = @{ } + + Write-Error -Message $_.Exception.Message + } + } + + # If the Invoke-Build Task Header is specified in the Build Info, set it. + if ($BuildInfo.TaskHeader) + { + Set-BuildHeader -Script ([scriptblock]::Create($BuildInfo.TaskHeader)) + } + + <# + Add BuildModuleOutput to PSModule Path environment variable. + Moved here (not in begin block) because build file can contains BuiltSubModuleDirectory value. + #> + if ($BuiltModuleSubdirectory) + { + if (-not (Split-Path -IsAbsolute -Path $BuiltModuleSubdirectory)) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuiltModuleSubdirectory + } + else + { + $BuildModuleOutput = $BuiltModuleSubdirectory + } + } # test if BuiltModuleSubDirectory set in build config file + elseif ($BuildInfo.ContainsKey('BuiltModuleSubDirectory')) + { + $BuildModuleOutput = Join-Path -Path $OutputDirectory -ChildPath $BuildInfo['BuiltModuleSubdirectory'] + } + else + { + $BuildModuleOutput = $OutputDirectory + } + + # Pre-pending $BuildModuleOutput folder to PSModulePath to resolve built module from this folder. + if ($powerShellModulePaths -notcontains $BuildModuleOutput) + { + Write-Host -Object "[build] Pre-pending '$BuildModuleOutput' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $BuildModuleOutput + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + <# + Import Tasks from modules via their exported aliases when defined in Build Manifest. + https://github.com/nightroman/Invoke-Build/tree/master/Tasks/Import#example-2-import-from-a-module-with-tasks + #> + if ($BuildInfo.ContainsKey('ModuleBuildTasks')) + { + foreach ($module in $BuildInfo['ModuleBuildTasks'].Keys) + { + try + { + Write-Host -Object "Importing tasks from module $module" -ForegroundColor DarkGray + + $loadedModule = Import-Module -Name $module -PassThru -ErrorAction Stop + + foreach ($TaskToExport in $BuildInfo['ModuleBuildTasks'].($module)) + { + $loadedModule.ExportedAliases.GetEnumerator().Where{ + Write-Host -Object "`t Loading $($_.Key)..." -ForegroundColor DarkGray + + # Using -like to support wildcard. + $_.Key -like $TaskToExport + }.ForEach{ + # Dot-sourcing the Tasks via their exported aliases. + . (Get-Alias $_.Key) + } + } + } + catch + { + Write-Host -Object "Could not load tasks for module $module." -ForegroundColor Red + + Write-Error -Message $_ + } + } + } + + # Loading Build Tasks defined in the .build/ folder (will override the ones imported above if same task name). + Get-ChildItem -Path '.build/' -Recurse -Include '*.ps1' -ErrorAction Ignore | + ForEach-Object { + "Importing file $($_.BaseName)" | Write-Verbose + + . $_.FullName + } + + # Synopsis: Empty task, useful to test the bootstrap process. + task noop { } + + # Define default task sequence ("."), can be overridden in the $BuildInfo. + task . { + Write-Build -Object 'No sequence currently defined for the default task' -ForegroundColor Yellow + } + + Write-Host -Object 'Adding Workflow from configuration:' -ForegroundColor DarkGray + + # Load Invoke-Build task sequences/workflows from $BuildInfo. + foreach ($workflow in $BuildInfo.BuildWorkflow.keys) + { + Write-Verbose -Message "Creating Build Workflow '$Workflow' with tasks $($BuildInfo.BuildWorkflow.($Workflow) -join ', ')." + + $workflowItem = $BuildInfo.BuildWorkflow.($workflow) + + if ($workflowItem.Trim() -match '^\{(?[\w\W]*)\}$') + { + $workflowItem = [ScriptBlock]::Create($Matches['sb']) + } + + Write-Host -Object " +-> $workflow" -ForegroundColor DarkGray + + task $workflow $workflowItem + } + + Write-Host -Object "[build] Executing requested workflow: $($Tasks -join ', ')" -ForeGroundColor Magenta + + } + finally + { + Pop-Location -StackName 'BeforeBuild' + } +} + +begin +{ + # Find build config if not specified. + if (-not $BuildConfig) + { + $config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction Ignore + + if (-not $config -or ($config -is [System.Array] -and $config.Length -le 0)) + { + throw 'No build configuration found. Specify path via parameter BuildConfig.' + } + elseif ($config -is [System.Array]) + { + if ($config.Length -gt 1) + { + throw 'More than one build configuration found. Specify which path to use via parameter BuildConfig.' + } + + $BuildConfig = $config[0] + } + else + { + $BuildConfig = $config + } + } + + # Bootstrapping the environment before using Invoke-Build as task runner + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Host -Object "[pre-build] Starting Build Init" -ForegroundColor Green + + Push-Location $PSScriptRoot -StackName 'BuildModule' + } + + if ($RequiredModulesDirectory -in @('CurrentUser', 'AllUsers')) + { + # Installing modules instead of saving them. + Write-Host -Object "[pre-build] Required Modules will be installed to the PowerShell module path that is used for $RequiredModulesDirectory." -ForegroundColor Green + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $RequiredModulesDirectory + } + else + { + if (-not (Split-Path -IsAbsolute -Path $OutputDirectory)) + { + $OutputDirectory = Join-Path -Path $PSScriptRoot -ChildPath $OutputDirectory + } + + # Resolving the absolute path to save the required modules to. + if (-not (Split-Path -IsAbsolute -Path $RequiredModulesDirectory)) + { + $RequiredModulesDirectory = Join-Path -Path $PSScriptRoot -ChildPath $RequiredModulesDirectory + } + + # Create the output/modules folder if not exists, or resolve the Absolute path otherwise. + if (Resolve-Path -Path $RequiredModulesDirectory -ErrorAction SilentlyContinue) + { + Write-Debug -Message "[pre-build] Required Modules path already exist at $RequiredModulesDirectory" + + $requiredModulesPath = Convert-Path -Path $RequiredModulesDirectory + } + else + { + Write-Host -Object "[pre-build] Creating required modules directory $RequiredModulesDirectory." -ForegroundColor Green + + $requiredModulesPath = (New-Item -ItemType Directory -Force -Path $RequiredModulesDirectory).FullName + } + + $powerShellModulePaths = $env:PSModulePath -split [System.IO.Path]::PathSeparator + + # Pre-pending $requiredModulesPath folder to PSModulePath to resolve from this folder FIRST. + if ($RequiredModulesDirectory -notin @('CurrentUser', 'AllUsers') -and + ($powerShellModulePaths -notcontains $RequiredModulesDirectory)) + { + Write-Host -Object "[pre-build] Pre-pending '$RequiredModulesDirectory' folder to PSModulePath" -ForegroundColor Green + + $env:PSModulePath = $RequiredModulesDirectory + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + $powerShellYamlModule = Get-Module -Name 'powershell-yaml' -ListAvailable + $invokeBuildModule = Get-Module -Name 'InvokeBuild' -ListAvailable + $psDependModule = Get-Module -Name 'PSDepend' -ListAvailable + + # Checking if the user should -ResolveDependency. + if (-not ($powerShellYamlModule -and $invokeBuildModule -and $psDependModule) -and -not $ResolveDependency) + { + if ($AutoRestore -or -not $PSBoundParameters.ContainsKey('Tasks') -or $Tasks -contains 'build') + { + Write-Host -Object "[pre-build] Dependency missing, running './build.ps1 -ResolveDependency -Tasks noop' for you `r`n" -ForegroundColor Yellow + + $ResolveDependency = $true + } + else + { + Write-Warning -Message "Some required Modules are missing, make sure you first run with the '-ResolveDependency' parameter. Running 'build.ps1 -ResolveDependency -Tasks noop' will pull required modules without running the build task." + } + } + + <# + The variable $PSDependTarget will be used below when building the splatting + variable before calling Resolve-Dependency.ps1, unless overridden in the + file Resolve-Dependency.psd1. + #> + $PSDependTarget = $requiredModulesPath + } + + if ($ResolveDependency) + { + Write-Host -Object "[pre-build] Resolving dependencies using preferred method." -ForegroundColor Green + + $resolveDependencyParams = @{ } + + # If BuildConfig is a Yaml file, bootstrap powershell-yaml via ResolveDependency. + if ($BuildConfig -match '\.[yaml|yml]$') + { + $resolveDependencyParams.Add('WithYaml', $true) + } + + $resolveDependencyAvailableParams = (Get-Command -Name '.\Resolve-Dependency.ps1').Parameters.Keys + + foreach ($cmdParameter in $resolveDependencyAvailableParams) + { + # The parameter has been explicitly used for calling the .build.ps1 + if ($MyInvocation.BoundParameters.ContainsKey($cmdParameter)) + { + $paramValue = $MyInvocation.BoundParameters.Item($cmdParameter) + + Write-Debug " adding $cmdParameter :: $paramValue [from user-provided parameters to Build.ps1]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + # Use defaults parameter value from Build.ps1, if any + else + { + $paramValue = Get-Variable -Name $cmdParameter -ValueOnly -ErrorAction Ignore + + if ($paramValue) + { + Write-Debug " adding $cmdParameter :: $paramValue [from default Build.ps1 variable]" + + $resolveDependencyParams.Add($cmdParameter, $paramValue) + } + } + } + + Write-Host -Object "[pre-build] Starting bootstrap process." -ForegroundColor Green + + .\Resolve-Dependency.ps1 @resolveDependencyParams + } + + if ($MyInvocation.ScriptName -notlike '*Invoke-Build.ps1') + { + Write-Verbose -Message "Bootstrap completed. Handing back to InvokeBuild." + + if ($PSBoundParameters.ContainsKey('ResolveDependency')) + { + Write-Verbose -Message "Dependency already resolved. Removing task." + + $null = $PSBoundParameters.Remove('ResolveDependency') + } + + Write-Host -Object "[build] Starting build with InvokeBuild." -ForegroundColor Green + + Invoke-Build @PSBoundParameters -Task $Tasks -File $MyInvocation.MyCommand.Path + + Pop-Location -StackName 'BuildModule' + + return + } +} diff --git a/resources/Microsoft.Windows.Settings/build.yaml b/resources/Microsoft.Windows.Settings/build.yaml new file mode 100644 index 00000000..99c1c937 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/build.yaml @@ -0,0 +1,183 @@ +--- +#################################################### +# ModuleBuilder Configuration # +#################################################### + +BuiltModuleSubdirectory: module +CopyPaths: + - en-US + - DSCResources + # - Modules +Prefix: prefix.ps1 +Encoding: UTF8 +# Can be used to manually specify module's semantic version if the preferred method of +# using GitVersion is not available, and it is not possible to set the session environment +# variable `$env:ModuleVersion`, nor setting the variable `$ModuleVersion`, in the +# PowerShell session (parent scope) before running the task `build`. +#SemVer: '99.0.0-preview1' + +VersionedOutputDirectory: true + +#################################################### +# ModuleBuilder Submodules Configuration # +#################################################### + +NestedModule: + DscResource.Common: + CopyOnly: true + Path: ./output/RequiredModules/DscResource.Common + AddToManifest: false + Exclude: PSGetModuleInfo.xml + DscResource.Base: + CopyOnly: true + Path: ./output/RequiredModules/DscResource.Base + AddToManifest: false + Exclude: PSGetModuleInfo.xml + +#################################################### +# Sampler Pipeline Configuration # +#################################################### +BuildWorkflow: + ".": + - build + - test + + build: + - Clean + - Build_Module_ModuleBuilder + - Build_NestedModules_ModuleBuilder + - Create_changelog_release_output + - Generate_Conceptual_Help + - Generate_Wiki_Content + + pack: + - build + - package_module_nupkg + + hqrmtest: + - DscResource_Tests_Stop_On_Fail + + # Defining test task to be run when invoking `./build.ps1 -Tasks test` + test: + # Uncomment to modify the PSModulePath in the test pipeline (also requires the build configuration section SetPSModulePath). + #- Set_PSModulePath + - Pester_Tests_Stop_On_Fail + # Use this task if pipeline uses code coverage and the module is using the + # pattern of Public, Private, Enum, Classes. + #- Convert_Pester_Coverage + - Pester_if_Code_Coverage_Under_Threshold + + # Use this task when you have multiple parallel tests, which produce multiple + # code coverage files and needs to get merged into one file. + #merge: + #- Merge_CodeCoverage_Files + + publish: + - Publish_Release_To_GitHub # Runs first, if token is expired it will fail early + - Publish_GitHub_Wiki_Content + + - publish_module_to_gallery + +#################################################### +# PESTER Configuration # +#################################################### + +Pester: + Configuration: + Run: + Path: + - tests/Unit + Output: + Verbosity: Detailed + StackTraceVerbosity: Full + CIFormat: Auto + CodeCoverage: + CoveragePercentTarget: 80 + OutputPath: JaCoCo_coverage.xml + OutputEncoding: ascii + UseBreakpoints: false + TestResult: + OutputFormat: NUnitXML + OutputEncoding: ascii + ExcludeFromCodeCoverage: + - Modules/DscResource.Common + - Modules/DscResource.Base + +DscTest: + Pester: + Configuration: + Filter: + ExcludeTag: + - "Common Tests - New Error-Level Script Analyzer Rules" + Output: + Verbosity: Detailed + CIFormat: Auto + TestResult: + Enabled: true + OutputFormat: NUnitXML + OutputEncoding: ascii + OutputPath: ./output/testResults/NUnitXml_HQRM_Tests.xml + Script: + ExcludeSourceFile: + - output + ExcludeModuleFile: + - Modules/DscResource.Common + - Modules/DscResource.Base + # Must exclude built module file because it should not be tested like MOF-based resources + - Microsoft.Windows.Settings.psm1 + MainGitBranch: main + +# Import ModuleBuilder tasks from a specific PowerShell module using the build +# task's alias. Wildcard * can be used to specify all tasks that has a similar +# prefix and or suffix. The module contain the task must be added as a required +# module in the file RequiredModules.psd1. +ModuleBuildTasks: + Sampler: + - "*.build.Sampler.ib.tasks" + Sampler.GitHubTasks: + - "*.ib.tasks" + DscResource.DocGenerator: + - "Task.*" + +# Invoke-Build Header to be used to 'decorate' the terminal output of the tasks. +TaskHeader: | + param($Path) + "" + "=" * 79 + Write-Build Cyan "`t`t`t$($Task.Name.replace("_"," ").ToUpper())" + Write-Build DarkGray "$(Get-BuildSynopsis $Task)" + "-" * 79 + Write-Build DarkGray " $Path" + Write-Build DarkGray " $($Task.InvocationInfo.ScriptName):$($Task.InvocationInfo.ScriptLineNumber)" + "" + +GitHubConfig: + GitHubFilesToAdd: + - "CHANGELOG.md" + GitHubConfigUserName: dscbot + GitHubConfigUserEmail: dsccommunity@outlook.com + UpdateChangelogOnPrerelease: false + +#################################################### +# DscResource.DocGenerator Configuration # +#################################################### +DscResource.DocGenerator: + Generate_Conceptual_Help: + MarkdownCodeRegularExpression: + - '\`(.+?)\`' # Match inline code-block + - '\\(\\)' # Match escaped backslash + - '\[[^\[]+\]\((.+?)\)' # Match markdown URL + - "_(.+?)_" # Match Italic (underscore) + - '\*\*(.+?)\*\*' # Match bold + - '\*(.+?)\*' # Match Italic (asterisk) + +#################################################### +# Setting Sampler PSModulePath # +#################################################### +#SetPSModulePath: +#Sub expressions in the PSModulePath are supported but not part of the template as it does not work with Plaster +#PSModulePath: C:\Program Files\WindowsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules;c:\Users\Install\.vscode\extensions\ms-vscode.powershell-2022.5.1\modules; +#RemovePersonal: false +#RemoveProgramFiles: false +#RemoveWindows: false +#SetSystemDefault: false diff --git a/resources/Microsoft.Windows.Settings/source/Classes/001.WindowsReason.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/001.WindowsReason.ps1 new file mode 100644 index 00000000..1828b94c --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/001.WindowsReason.ps1 @@ -0,0 +1,21 @@ +<# + .SYNOPSIS + The reason a property of a DSC resource is not in desired state. + + .DESCRIPTION + A DSC resource can have a read-only property `Reasons` that the compliance + part (audit via Azure Policy) of Azure AutoManage Machine Configuration + uses. The property Reasons holds an array of WindowsReason. Each WindowsReason + explains why a property of a DSC resource is not in desired state. +#> + +class WindowsReason +{ + [DscProperty()] + [System.String] + $Code + + [DscProperty()] + [System.String] + $Phrase +} diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 new file mode 100644 index 00000000..a5f88181 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -0,0 +1,114 @@ +<# + .SYNOPSIS + The `SettingsBase` class is the base class for all settings resources. + + .DESCRIPTION + This class is the base class for all settings resources. It provides the basic functionality + for getting, setting, and testing the properties of the settings resources. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes'. +#> +class SettingsBase : ResourceBase +{ + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance = 'Yes' + + hidden [System.String] $ClassName + + SettingsBase() : base ($PSScriptRoot) + { + # These properties will not be enforced. + $this.ExcludeDscProperties = @( + 'IsSingleInstance' + ) + + $this.ClassName = $this.GetType().Name + } + + [SettingsBase] Get() + { + # Call the base method to return the properties. + return ([ResourceBase] $this).Get() + } + + # Base method Get() call this method to get the current state as a Hashtable. + [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) + { + # Use the Get-DscProperty cmdlet to get the properties to search instead of properties from GetCurrentState + $propsToSearch = Get-DscProperty -InputObject $this ` + -Attribute Optional ` + -ExcludeName @('SID', 'IsSingleInstance') + + # Initialize the state + $state = @{} + + # Get the registry key data + $keyData = Get-RegistryKeyData -Key $this.ClassName + + $propsToSearch.Keys | ForEach-Object { + # Get the property name + $property = $_ + + # Get the key data for the property + $key = $keyData | Where-Object -Property PropertyName -EQ $property + + # Remove the PropertyName key from the key data + $key.Remove('PropertyName') + + # Get the status of the property + $settingState = Get-RegistryStatus @key + + # Add the property and status to the state + $state.Add($_, [SettingStatus]$settingState) + } + + return $state + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([ResourceBase] $this).Set() + } + + <# + Base method Set() call this method with the properties that should be + enforced and that are not in desired state. + #> + hidden [void] Modify([System.Collections.Hashtable] $properties) + { + $properties.GetEnumerator() | ForEach-Object { + # Capture the property name to filter on + $propertyName = $_.Key -as [System.String] + + # The value as string else it can be empty + $propertyValue = $_.Value -as [System.String] + + # Search the entry in the registry key data + $entry = Get-RegistryKeyData -Key $this.ClassName | Where-Object { $_.PropertyName -eq $propertyName } + + # Build the parameters for Set-RegistryStatus + $params = @{ + Name = $entry.Name + Path = $entry.Path + Value = $entry.Status[$propertyValue] + } + + # Add the Type parameter if it exists + if ($entry.Type) + { + $params.Add('Type', $entry.Type) + } + + Set-RegistryStatus @params + } + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } +} diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 new file mode 100644 index 00000000..70873075 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -0,0 +1,60 @@ +<# + .SYNOPSIS + The `FindMyDevice` DSC resource is used to manage the Find My Device setting. + + .DESCRIPTION + This resource is used to enable or disable the Find My Device setting on a Windows device. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes' + + .PARAMETER FindMyDevice + Specifies whether the Find My Device setting should be enabled or disabled. + + .PARAMETER Reasons + Returns the reason a property is not in the desired state. +#> +[DscResource()] +class FindMyDevice : SettingsBase +{ + [DscProperty(Key)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance = 'Yes' + + [DscProperty()] + [SettingStatus] $FindMyDevice + + [DscProperty(NotConfigurable)] + [WindowsReason[]] + $Reasons + + FindMyDevice() + { + # These properties will not be enforced. + $this.ExcludeDscProperties = @( + 'IsSingleInstance' + ) + + # Opt in to the optional enums feature + $this.FeatureOptionalEnums = $true + } + + [FindMyDevice] Get() + { + # Call the base method to return the properties. + return ([SettingsBase] $this).Get() + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([SettingsBase] $this).Set() + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([SettingsBase] $this).Test() + } +} diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 new file mode 100644 index 00000000..fa4e83cc --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -0,0 +1,90 @@ + +<# + .SYNOPSIS + The `General` DSC resource is used to manage the General settings under Privacy & security. + + .DESCRIPTION + This resource is used to enable or disable the General settings under Privacy & security on a Windows device. + + .PARAMETER IsSingleInstance + Specifies the resource is a single instance, the value must be 'Yes' + + .PARAMETER EnablePersonalizedAds + Specifies whether personalized ads should be enabled or disabled. + + .PARAMETER EnableLocalContentByLanguageList + Specifies whether local content by language list should be enabled or disabled. + + .PARAMETER EnableAppLaunchTracking + Specifies whether app launch tracking should be enabled or disabled. + + .PARAMETER ShowContentSuggestion + Specifies whether content suggestions should be shown or not. + + .PARAMETER EnableAccountNotifications + Specifies whether account notifications should be enabled or disabled. + + .PARAMETER Reasons + Returns the reason a property is not in the desired state. + + .EXAMPLE + Invoke-DscResource -Name General -ModuleName Microsoft.Windows.Settings -Method Set -Property @{ EnablePersonalizedAds = $true } + + # This example demonstrates how to use the General DSC resource to enable personalized ads. +#> +[DSCResource()] +class General : SettingsBase +{ + [DscProperty(Key)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance = 'Yes' + + [DscProperty()] + [SettingStatus] $EnablePersonalizedAds + + [DscProperty()] + [SettingStatus] $EnableLocalContentByLanguageList + + [DscProperty()] + [SettingStatus] $EnableAppLaunchTracking + + [DscProperty()] + [SettingStatus] $ShowContentSuggestion + + [DscProperty()] + [SettingStatus] $EnableAccountNotifications + + [DscProperty(NotConfigurable)] + [WindowsReason[]] + $Reasons + + General() + { + # These properties will not be enforced. + $this.ExcludeDscProperties = @( + 'IsSingleInstance' + ) + + # Opt in to the optional enums feature + $this.FeatureOptionalEnums = $true + } + + [General] Get() + { + # Call the base method to return the properties. + return ([SettingsBase] $this).Get() + } + + [void] Set() + { + # Call the base method to enforce the properties. + ([SettingsBase] $this).Set() + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([SettingsBase] $this).Test() + } +} diff --git a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 new file mode 100644 index 00000000..3964660e --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -0,0 +1,70 @@ +<# + .SYNOPSIS + The data for the registry keys to be set by the relevant DSC resource + + This file should only contain data for the registry keys to be set by the DSC resource +#> +@{ + "FindMyDevice" = @{ + PropertyName = 'FindMyDevice' + Name = 'LocationSyncEnabled' + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 0 + } + } + "General" = @( + @{ + PropertyName = 'EnablePersonalizedAds' + Name = 'Enabled' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 0 + } + }, + @{ + PropertyName = 'EnableLocalContentByLanguageList' + Name = 'HttpAcceptLanguageOptOut' + Path = 'HKCU:\Control Panel\International\User Profile\' + Status = @{ + Enabled = 0 + Disabled = 1 + Default = 0 + } + }, + @{ + PropertyName = 'EnableAppLaunchTracking' + Name = 'Start_TrackProgs' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 1 + } + } + @{ + PropertyName = 'ShowContentSuggestion' + Name = @('SubscribedContent-338393Enabled', 'SubscribedContent-353694Enabled', 'SubscribedContent-353696Enabled') + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 1 + } + } + @{ + PropertyName = 'EnableAccountNotifications' + Name = 'EnableAccountNotifications' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SystemSettings\AccountNotifications\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 1 + } + } + ) +} diff --git a/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 new file mode 100644 index 00000000..a5a2e182 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 @@ -0,0 +1,12 @@ +<# + .SYNOPSIS + The possible states for the DSC resource parameter SettingStatus. + This enum is used to specify the desired state of settings that can be enabled or disabled e.g. have a value of 0 or 1. + + To opt-in for enums through the DscResource.Base class, set the property FeatureOptionalEnums to $true in the constructor of the DSC resource class. +#> +enum SettingStatus +{ + Enabled = 1 + Disabled +} diff --git a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 new file mode 100644 index 00000000..f81cee61 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -0,0 +1,131 @@ +# +# Module manifest for module 'Microsoft.Windows.Settings' +# +# Generated by: Microsoft Corporation +# +# Generated on: 16/02/2025 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.Windows.Settings.psm1' + + # Version number of this module. + ModuleVersion = '0.0.1' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '2c3faffd-05bc-4178-a8e2-e42618bdce6b' + + # Author of this module + Author = 'Microsoft Corporation' + + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' + + # Copyright statement for this module + Copyright = '(c) Microsoft Corporation. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'The DSC module for Windows Settings' + + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.0' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + # AliasesToExport = @() + + # DSC resources to export from this module + DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # 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 = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @(@('PSDscResource_WindowsSettings')) + + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = '' + + # Prerelease string of this module + Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' + +} \ No newline at end of file diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 new file mode 100644 index 00000000..055e7a53 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 @@ -0,0 +1,48 @@ +<# + .SYNOPSIS + Retrieves data from the registry key data file for a specific registry key + + It uses the localized data 'RegistryKeyData.psd1' to retrieve the data + + .PARAMETER Key + The key in the data file to retrieve registry key data for + + .PARAMETER OnlyKeyPath + If specified, only the path and name of the registry key will be returned + + .EXAMPLE + PS C:\> Get-RegistryKeyData -Key 'FindMyDevice' + + .EXAMPLE + PS C:\> Get-RegistryKeyData -Key 'General' -OnlyKeyPath +#> +function Get-RegistryKeyData +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $true)] + [System.String] + $Key, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $OnlyKeyPath + ) + + $registryDataPath = Join-Path -Path $PSScriptRoot -ChildPath 'DSCResources\RegistryKeyData.psd1' + Write-Verbose -Message ($script:localizedData.GetRegistryKeyData_SearchMessage -f $Key, $registryDataPath) + $import = Import-PowerShellDataFile -Path $registryDataPath + + $entry = $import.$Key + + if ($OnlyKeyPath) + { + $entry = @{ + Path = $entry.Path + Name = $entry.Name + } + } + + return $entry +} diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 new file mode 100644 index 00000000..16601d02 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -0,0 +1,87 @@ +<# + .SYNOPSIS + Retrieve the current status of a registry using key path and value + + .PARAMETER Path + The path of the registry key. + + .PARAMETER Name + The name(s) of the registry values to check + + .PARAMETER Status + A hashtable containing possible statuses and their corresponding values. + + .EXAMPLE + PS C:\> Get-RegistryStatus -Path 'HKLM:\1\2\3' -Name 'Setting1' + + .EXAMPLE + PS C:\> Get-RegistryStatus -Path 'HKLM:\1\2\3' -Name 'Setting1', 'Setting2' -Status @{ 'Enabled' = 1; 'Disabled' = 0; 'Default' = 0 } +#> +function Get-RegistryStatus +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String[]] + $Name, + + [Parameter()] + [AllowNull()] + [System.Collections.Hashtable] + $Status + ) + + # Grab the default value from the one matching in the status hashtable e.g. @{ 'Enabled' = 1; 'Disabled' = 0; 'Default' = 0 } <-- 'Disabled' will be returned + $key = $status.GetEnumerator() | Where-Object { $_.Value -eq $Status.Default -and $_.Key -ne 'Default' } | Select-Object -ExpandProperty Key + + # For class we create variable + New-Variable -Name 'registryValue' -Value $null -Scope Local -Force + + try + { + $registryValue = foreach ($NameItem in $Name) + { + Get-ItemPropertyValue -Path $Path -Name $NameItem -ErrorAction Stop + } + } + catch + { + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_DefaultMessage -f $Status.Default, $Name) + } + + if (-not ([string]::IsNullOrEmpty($registryValue))) + { + if ($Status.ContainsKey('Default')) + { + # Remove the default value from the hashtable as it is returned by default if nothing is found + $Status.Remove('Default') + } + + if ($registryValue.Count -gt 1) + { + # Using Group-Object to count the number of unique values + # If the count is greater than 1, then the registry key has been manually manipulated + $groupCount = $registryValue | Group-Object + if ($groupCount.Name.Count -ne 1) + { + $errorMessage = $script:localizedData.RegistryManualManipulationError -f $Path + New-InvalidDataException ` + -ErrorId 'RegistryManualManipulationError' ` + -Message $errorMessage + } + + # Return the first value as the registry value should be the same + $registryValue = $registryValue[0] + } + + $key = $Status.GetEnumerator() | Where-Object -Property Value -EQ $registryValue | Select-Object -ExpandProperty Key + } + + return $Key +} diff --git a/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 new file mode 100644 index 00000000..b3a3678e --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 @@ -0,0 +1,72 @@ +<# + .SYNOPSIS + Sets the status of specified registry value + + .PARAMETER Path + The path of the registry key. + + .PARAMETER Name + The names of the registry values to set. + + .PARAMETER Value + The value to set for the registry values. + + .PARAMETER Type + The type of the registry values. Valid values are 'String', 'DWord', 'QWord', 'Binary', 'MultiString', 'Unknown'. + + .EXAMPLE + PS C:\> Set-RegistryStatus -Path 'HKLM:\1\2\3' -Name 'Setting1' -Value '1' + + .EXAMPLE + PS C:\> Set-RegistryStatus -Path 'HKLM:\Software\MyApp' -Name 'Setting1', 'Setting2' -Value '1' -Type 'DWord' +#> +function Set-RegistryStatus +{ + param ( + [Parameter(Mandatory = $true)] + [string] $Path, + + [Parameter(Mandatory = $true)] + [string[]] $Name, + + [Parameter(Mandatory = $true)] + [string] $Value, + + [Parameter()] + [ValidateSet('String', 'DWord', 'QWord', 'Binary', 'MultiString', 'Unknown')] + [AllowNull()] + [string] $Type + ) + + if (-not (Test-Path $Path)) + { + New-Item -Path $Path -Force | Out-Null + } + + $Name | ForEach-Object { + $Params = @{ + Path = $Path + Value = $Value + Name = $_ + Force = $true + + } + + if (-not (Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue)) + { + if ([string]::IsNullOrEmpty($Type)) + { + $Type = 'Dword' + } + + # Include the property type + $Params.Add('PropertyType', $Type) + # TODO: Add localizations + New-ItemProperty @Params | Out-Null + } + else + { + Set-ItemProperty @Params + } + } +} diff --git a/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 b/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 new file mode 100644 index 00000000..da9ab4d2 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 @@ -0,0 +1,14 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource FindMyDevice class. This file should only contain + localized strings for private functions, public command, and + classes (that are not a DSC resource). +#> + +ConvertFrom-StringData @' + ## Strings overrides for the ResourceBase's default strings. + # None + + ## Strings directly used by the derived class FindMyDevice. +'@ diff --git a/resources/Microsoft.Windows.Settings/source/en-US/General.strings.psd1 b/resources/Microsoft.Windows.Settings/source/en-US/General.strings.psd1 new file mode 100644 index 00000000..94644af9 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/General.strings.psd1 @@ -0,0 +1,15 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource General class. This file should only contain + localized strings for private functions, public command, and + classes (that are not a DSC resource). +#> + +ConvertFrom-StringData @' + ## Strings overrides for the ResourceBase's default strings. + # None + + ## Strings directly used by the derived class General. + +'@ diff --git a/resources/Microsoft.Windows.Settings/source/en-US/Microsoft.Windows.Settings.strings.psd1 b/resources/Microsoft.Windows.Settings/source/en-US/Microsoft.Windows.Settings.strings.psd1 new file mode 100644 index 00000000..bfa354ca --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/Microsoft.Windows.Settings.strings.psd1 @@ -0,0 +1,17 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource Microsoft.Windows.Settings module. This file should only contain + localized strings for private and public functions. +#> + +ConvertFrom-StringData @' + ## Get-RegistryStatus + GetRegistryStatusRegistryManualManipulation_ErrorMessage = "The registry path '{0}' is not in the expected state. Entries have been manually modified. Cannot determine the current state." + GetRegistryStatus_SearchMessage = "Searching '{0}' key in '{1}'." + GetRegistryStatus_FoundMessage = "Found '{0}' key in '{1}'." + GetRegistryStatus_DefaultMessage = "Returning default value '{0}' for '{1}'." + + ## Get-RegistryKeyData + GetRegistryKeyData_SearchMessage = Searching '{0}' key in '{1}'. +'@ diff --git a/resources/Microsoft.Windows.Settings/source/en-US/SettingsBase.strings.psd1 b/resources/Microsoft.Windows.Settings/source/en-US/SettingsBase.strings.psd1 new file mode 100644 index 00000000..643288ab --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/SettingsBase.strings.psd1 @@ -0,0 +1,10 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource SettingsBase class. This file should only contain + localized strings for private functions, public command, and + classes (that are not a DSC resource). +#> + +ConvertFrom-StringData @' +'@ diff --git a/resources/Microsoft.Windows.Settings/source/en-US/about_Microsoft.Windows.Settings.help.txt b/resources/Microsoft.Windows.Settings/source/en-US/about_Microsoft.Windows.Settings.help.txt new file mode 100644 index 00000000..8ab2cc30 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/about_Microsoft.Windows.Settings.help.txt @@ -0,0 +1,24 @@ +TOPIC + about_Microsoft.Windows.Settings + +SHORT DESCRIPTION + The DSC module for Windows Settings + +LONG DESCRIPTION + The DSC module for Windows Settings + +EXAMPLES + PS C:\> Get-DscResource -Module Microsoft.Windows.Settings + +NOTE: + Thank you to all those who contributed to this module, by writing code, sharing opinions, and provided feedback. + +TROUBLESHOOTING NOTE: + Look out on the repository for issues and new releases. + +SEE ALSO + - https://github.com/microsoft/winget-dsc/ + +KEYWORDS + DSC, Windows, Settings, Class-based + diff --git a/resources/Microsoft.Windows.Settings/source/prefix.ps1 b/resources/Microsoft.Windows.Settings/source/prefix.ps1 new file mode 100644 index 00000000..168dd747 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/prefix.ps1 @@ -0,0 +1,7 @@ +using module .\Modules\DscResource.Base + +# Import nested, 'DscResource.Common' module +$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules\DscResource.Common' +Import-Module -Name $script:dscResourceCommonModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' diff --git a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 new file mode 100644 index 00000000..36713c07 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 @@ -0,0 +1,74 @@ +#Requires -RunAsAdministrator + +<# + .SYNOPSIS + Integration test for FindMyDevice DSC class resource. +#> + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'PSDesiredStateConfiguration')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'PSDesiredStateConfiguration' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'PSDesiredStateConfiguration' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'PSDesiredStateConfiguration module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + $script:dscResourceName = 'FindMyDevice' + + Import-Module -Name $script:dscModuleName -Force + + $script:instance = & (Import-Module $script:dscModuleName -PassThru) ([scriptblock]::Create("'$script:dscResourceName' -as 'type'")) + $script:currentState = $script:instance::new().Get() + + $PSDefaultParameterValues['Invoke-DscResource:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Invoke-DscResource:Name'] = $script:instance.Name +} + +AfterAll { + $PSDefaultParameterValues.Remove('Invoke-DscResource:ModuleName') + $PSDefaultParameterValues.Remove('Invoke-DscResource:Name') + + Write-Verbose -Message "Restoring the original state of the $($script:dscResourceName) setting." + $script:currentState.Set() +} + +Describe "FindMyDevice\Generic" { + Context "List available DSC resources" { + It 'Shows DSC resources' { + $expectedDscResources = @('FindMyDevice') + + $availableDscResources = (Get-DscResource -Module $script:dscModuleName -Name $expectedDscResources).Name + $availableDscResources.count | Should -Be 1 + $availableDscResources | Where-Object { $expectedDscResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } + } +} + +Describe "FindMyDevice\Integration" { + Context "FindMyDevice\Set" { + It 'Sets DSC resource' { + $desiredState = @{ FindMyDevice = 'Enabled' } + + Invoke-DscResource -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Method Get -Property $desiredState + $finalState.FindMyDevice | Should -Be 'Enabled' + } + } +} \ No newline at end of file diff --git a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/General.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/General.tests.ps1 new file mode 100644 index 00000000..23fdc734 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/General.tests.ps1 @@ -0,0 +1,79 @@ +<# + .SYNOPSIS + Integration test for FindMyDevice DSC class resource. +#> + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'PSDesiredStateConfiguration')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'PSDesiredStateConfiguration' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'PSDesiredStateConfiguration' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'PSDesiredStateConfiguration module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + $script:dscResourceName = 'General' + + Import-Module -Name $script:dscModuleName -Force + + $script:instance = & (Import-Module $script:dscModuleName -PassThru) ([scriptblock]::Create("'$script:dscResourceName' -as 'type'")) + $script:currentState = $script:instance::new().Get() + + $PSDefaultParameterValues['Invoke-DscResource:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Invoke-DscResource:Name'] = $script:instance.Name +} + +AfterAll { + $PSDefaultParameterValues.Remove('Invoke-DscResource:ModuleName') + $PSDefaultParameterValues.Remove('Invoke-DscResource:Name') + + Write-Verbose -Message "Restoring the original state of the $($script:dscResourceName) setting." + $script:currentState.Set() +} + +Describe "General\Generic" { + Context "List available DSC resources" { + It 'Shows DSC resources' { + $expectedDscResources = @('General') + + $availableDscResources = (Get-DscResource -Module $script:dscModuleName -Name $expectedDscResources).Name + $availableDscResources.count | Should -Be 1 + $availableDscResources | Where-Object { $expectedDscResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } + } +} + +Describe "Generic\Integration" { + Context "Generic\Set" { + It 'Sets to ' -TestCases @( + @{ Property = 'EnablePersonalizedAds'; Value = 'Disabled' }, + @{ Property = 'EnableLocalContentByLanguageList'; Value = 'Enabled' }, + @{ Property = 'EnableAppLaunchTracking'; Value = 'Enabled' }, + @{ Property = 'ShowContentSuggestion'; Value = 'Enabled' }, + @{ Property = 'EnableAccountNotifications'; Value = 'Disabled' } + ) { + param ($Property, $Value) + $desiredState = @{ $Property = $Value } + + Invoke-DscResource -Method Set -Property $desiredState + + $finalState = Invoke-DscResource -Method Get -Property $desiredState + $finalState.$Property | Should -Be $Value + } + } +} \ No newline at end of file diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 new file mode 100644 index 00000000..d29b0fad --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -0,0 +1,323 @@ +<# + .SYNOPSIS + Unit test for FindMyDevice DSC class resource. +#> + +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'FindMyDevice' { + Context 'When class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + { [FindMyDevice]::new() } | Should -Not -Throw + } + } + + It 'Should have a default or empty constructor' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $instance = [FindMyDevice]::new() + $instance | Should -Not -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $instance = [FindMyDevice]::new() + $instance.GetType().Name | Should -Be 'FindMyDevice' + } + } + } +} + +Describe 'FindMyDevice\Get()' -Tag 'Get' { + Context 'When getting the status of find my device' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice]@{ + IsSingleInstance = 'Yes' + } + + <# + This mocks the method GetCurrentState(). + + Method Get() will call the base method Get() which will + call back to the derived class method GetCurrentState() + to get the result to return from the derived method Get(). + #> + $script:mockInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return @{ + IsSingleInstance = 'Yes' + FindMyDevice = [SettingStatus]::Enabled + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.Get() + + $currentState.IsSingleInstance | Should -Be 'Yes' + $currentState.FindMyDevice | Should -Be 'Enabled' + + $currentState.Reasons | Should -BeNullOrEmpty + } + } + } +} + +Describe 'FindMyDevice\Set()' -Tag 'Set' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice] @{ + IsSingleInstance = 'Yes' + } | + # Mock method Modify which is called by the case method Set(). + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { + $script:methodModifyCallCount += 1 + } -PassThru + } + } + + BeforeEach { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:methodModifyCallCount = 0 + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should not call method Modify()' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + + $script:methodModifyCallCount | Should -Be 0 + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @( + @{ + Property = 'FindMyDevice' + ExpectedValue = 'Disabled' + ActualValue = 'Enabled' + } + ) + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should call method Modify()' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + + $script:methodModifyCallCount | Should -Be 1 + } + } + } +} + +Describe 'FindMyDevice\Test()' -Tag 'Test' { + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Test() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $true' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Test() | Should -BeTrue + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Test() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @( + @{ + Property = 'FindMyDevice' + ExpectedValue = $false + ActualValue = $true + }) + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $false' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Test() | Should -BeFalse + } + } + } +} + +Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { + Context 'When object is missing in the current state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice] @{ + FindMyDevice = 'Disabled' + } + } + } + + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.GetCurrentState( + @{ + IsSingleInstance = 'Yes' + } + ) + + $currentState.IsSingleInstance | Should -BeNullOrEmpty + $currentState.FindMyDevice | Should -Be 'Disabled' + } + } +} + + +Describe 'FindMyDevice\Set()' -Tag 'HiddenMember' { + BeforeAll { + Mock -CommandName Set-RegistryStatus + Mock -CommandName New-ItemProperty -MockWith { return $true } + } + + Context 'When setting the registry key for "Find My Device"' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice]@{ + FindMyDevice = 'Enabled' + } + } + } + + It 'Should call the correct mock' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + } + + Should -Invoke -CommandName Set-RegistryStatus -Exactly -Times 1 -Scope It + } + } +} diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 new file mode 100644 index 00000000..936f64cd --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 @@ -0,0 +1,323 @@ +<# + .SYNOPSIS + Unit test for FindMyDevice DSC class resource. +#> + +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'General' { + Context 'When class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + { [General]::new() } | Should -Not -Throw + } + } + + It 'Should have a default or empty constructor' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $instance = [General]::new() + $instance | Should -Not -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $instance = [General]::new() + $instance.GetType().Name | Should -Be 'General' + } + } + } +} + +Describe 'General\Get()' -Tag 'Get' { + Context 'When getting the status of general settings' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [General]@{ + IsSingleInstance = 'Yes' + } + + <# + This mocks the method GetCurrentState(). + + Method Get() will call the base method Get() which will + call back to the derived class method GetCurrentState() + to get the result to return from the derived method Get(). + #> + $script:mockInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { + return @{ + IsSingleInstance = 'Yes' + EnablePersonalizedAds = [SettingStatus]::Disabled + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.Get() + + $currentState.IsSingleInstance | Should -Be 'Yes' + $currentState.EnablePersonalizedAds | Should -Be 'Disabled' + + $currentState.Reasons | Should -BeNullOrEmpty + } + } + } +} + +Describe 'General\Set()' -Tag 'Set' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [General]@{ + IsSingleInstance = 'Yes' + } | + # Mock method Modify which is called by the case method Set(). + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { + $script:methodModifyCallCount += 1 + } -PassThru + } + } + + BeforeEach { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:methodModifyCallCount = 0 + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should not call method Modify()' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + + $script:methodModifyCallCount | Should -Be 0 + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Set() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @( + @{ + Property = 'EnablePersonalizedAds' + ExpectedValue = 'Disabled' + ActualValue = 'Enabled' + } + ) + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should call method Modify()' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + + $script:methodModifyCallCount | Should -Be 1 + } + } + } +} + +Describe 'General\Test()' -Tag 'Test' { + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Test() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $true' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Test() | Should -BeTrue + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance | + # Mock method Compare() which is called by the base method Test() + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @( + @{ + Property = 'EnablePersonalizedAds' + ExpectedValue = $false + ActualValue = $true + }) + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { + return + } + } + } + + It 'Should return $false' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Test() | Should -BeFalse + } + } + } +} + +Describe 'General\GetCurrentState()' -Tag 'HiddenMember' { + Context 'When object is missing in the current state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [General] @{ + EnablePersonalizedAds = 'Disabled' + } + } + } + + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.GetCurrentState( + @{ + IsSingleInstance = 'Yes' + } + ) + + $currentState.IsSingleInstance | Should -BeNullOrEmpty + $currentState.EnablePersonalizedAds | Should -Be 'Disabled' + } + } +} + + +Describe 'General\Set()' -Tag 'HiddenMember' { + BeforeAll { + Mock -CommandName Set-RegistryStatus + Mock -CommandName New-ItemProperty -MockWith { return $true } + } + + Context 'When setting the registry key for "EnablePersonalizedAds"' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [General]@{ + EnablePersonalizedAds = 'Enabled' + } + } + } + + It 'Should call the correct mock' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.Set() + } + + Should -Invoke -CommandName Set-RegistryStatus -Exactly -Times 1 -Scope It + } + } +} diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/WindowsReason.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/WindowsReason.tests.ps1 new file mode 100644 index 00000000..8620792d --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/WindowsReason.tests.ps1 @@ -0,0 +1,76 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'WindowsReason' -Tag 'WindowsReason' { + Context 'When instantiating the class' { + It 'Should not throw an error' { + $script:mockWindowsReasonInstance = InModuleScope -ScriptBlock { + [WindowsReason]::new() + } + } + + It 'Should be of the correct type' { + $mockWindowsReasonInstance | Should -Not -BeNullOrEmpty + $mockWindowsReasonInstance.GetType().Name | Should -Be 'WindowsReason' + } + } + + Context 'When setting an reading values' { + It 'Should be able to set value in instance' { + $script:mockWindowsReasonInstance = InModuleScope -ScriptBlock { + $WindowsReasonInstance = [WindowsReason]::new() + + $WindowsReasonInstance.Code = 'WindowsReason:WindowsReason:Ensure' + $WindowsReasonInstance.Phrase = 'The property Ensure should be "Present", but was "Absent"' + + return $WindowsReasonInstance + } + } + + It 'Should be able to read the values from instance' { + $mockWindowsReasonInstance.Code | Should -Be 'WindowsReason:WindowsReason:Ensure' + $mockWindowsReasonInstance.Phrase | Should -Be 'The property Ensure should be "Present", but was "Absent"' + } + } +} diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 new file mode 100644 index 00000000..11668ef5 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 @@ -0,0 +1,99 @@ +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'Get-RegistryKeyData' -Tag 'Private' { + Context 'Get entry from key data' { + BeforeAll { + Mock -CommandName Import-PowerShellDataFile -MockWith { + return @{ + General = @{ + PropertyName = 'General' + Name = 'Key' + Path = 'HKCU:\1\2\3' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 0 + } + } + } + } + } + + It 'Should return a result set' { + InModuleScope -ScriptBlock { + $result = Get-RegistryKeyData -Key General + $result | Should -BeOfType 'System.Collections.Hashtable' + } + + Should -Invoke -CommandName Import-PowerShellDataFile -Exactly -Times 1 + } + } + + Context 'Get only path and name' { + BeforeAll { + Mock -CommandName Import-PowerShellDataFile -MockWith { + return @{ + General = @{ + PropertyName = 'General' + Name = 'Key' + Path = 'HKCU:\1\2\3' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 0 + } + } + } + } + } + + It 'Should return key path and name' { + InModuleScope -ScriptBlock { + $result = Get-RegistryKeyData -Key General -OnlyKeyPath + $result.Path | Should -Not -BeNullOrEmpty + $result.Name | Should -Not -BeNullOrEmpty + } + } + } +} diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 new file mode 100644 index 00000000..c0a79610 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 @@ -0,0 +1,129 @@ +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + + + +Describe 'Get-RegistryStatus' -Tag 'Private' { + Context 'When the registry key exists' { + BeforeAll { + Mock -CommandName Get-ItemPropertyValue -MockWith { + return 1 + } + } + + BeforeDiscovery { + $testCases = @( + @{ + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues' + Name = 'LocationSyncEnabled' + Status = @{ + 'Enabled' = '1' + 'Disabled' = '0' + 'Default' = '0' + } + ExpectedValue = '1' + ExpectedKey = 'Enabled' + } + ) + } + + It 'Should return the registry value '''' on ''''' -ForEach $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + $result = Get-RegistryStatus -Path $Path -Name $Name -Status $Status + $result | Should -Be $ExpectedKey + } + + Should -Invoke -CommandName Get-ItemPropertyValue -Exactly -Times 1 + } + } + + Context 'When multiple registry keys are entered' { + BeforeAll { + Mock -CommandName Get-ItemPropertyValue -MockWith { + return 1 + } + } + + BeforeDiscovery { + $testCases = @( + @{ + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues' + Name = @('LocationSyncEnabled', 'LocationSyncDisabled') + Status = @{ + 'Enabled' = '1' + 'Disabled' = '0' + 'Default' = '0' + } + ExpectedValue = '1' + ExpectedKey = 'Enabled' + } + ) + } + + It 'Should return the registry value '''' on ''''' -ForEach $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + $result = Get-RegistryStatus -Path $Path -Name $Name -Status $Status + $result | Should -Be $ExpectedKey + } + + Should -Invoke -CommandName Get-ItemPropertyValue -Exactly -Times 2 + } + } + + Context 'When the registry key does not exist' { + BeforeAll { + Mock -CommandName Get-ItemPropertyValue -MockWith { + return $null + } + } + + It 'Should return empty result' { + InModuleScope -ScriptBlock { + $result = Get-RegistryStatus -Path 'HKCU:\1\2\3' -Name 'Empty' -Status @{} + $result | Should -BeNullOrEmpty + } + + Should -Invoke -CommandName Get-ItemPropertyValue -Exactly -Times 1 + } + } +} diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 new file mode 100644 index 00000000..3739f54c --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 @@ -0,0 +1,105 @@ +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'Microsoft.Windows.Settings' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + + +Describe 'Set-RegistryStatus' -Tag 'Private' { + Context 'Set registry key when it already exist' { + BeforeAll { + Mock -CommandName Set-ItemProperty -MockWith { + return 1 + } + } + + BeforeDiscovery { + $testCases = @( + @{ + Path = 'HKCU:\Environment' + Name = 'Path' + Value = 'C:\Windows\System32' + ExpectedKey = @{ + 'Path' = '1' + } + } + ) + } + + It 'Should set the registry key' -ForEach $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + $result = Set-RegistryStatus -Path $Path -Name $Name -Value $Value + $result | Should -Be $ExpectedKey.Path + } + + Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 + } + } + + Context 'Set registry key when it does not exist' { + BeforeAll { + Mock -CommandName New-Item + Mock -CommandName New-ItemProperty -MockWith { + return 0 + } + } + + BeforeDiscovery { + $testCases = @( + @{ + Path = 'HKCU:\1\2\3' + Name = 'MyValue' + Value = '0' + ExpectedKey = $null + } + ) + } + + It 'Should set the registry key' -ForEach $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + $result = Set-RegistryStatus -Path $Path -Name $Name -Value $Value + $result | Should -Be $ExpectedKey + } + + Should -Invoke -CommandName New-Item -Exactly -Times 1 + Should -Invoke -CommandName New-ItemProperty -Exactly -Times 1 + } + } +}