From 5987abc537acc2c3c0bf809fdf441ba744370a18 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 07:28:52 +0100 Subject: [PATCH 01/21] Init Microsoft.Windows.Settings --- .gitignore | 3 + .vscode/analyzersettings.psd1 | 44 + .vscode/settings.json | 21 +- .../Microsoft.Windows.Settings/.gitattributes | 15 + .../Microsoft.Windows.Settings/CHANGELOG.md | 31 + .../Microsoft.Windows.Settings/GitVersion.yml | 40 + .../Microsoft.Windows.Settings/README.md | 46 + .../RequiredModules.psd1 | 30 + .../Resolve-Dependency.ps1 | 1060 +++++++++++++++++ .../Resolve-Dependency.psd1 | 76 ++ .../azure-pipelines.yml | 292 +++++ .../Microsoft.Windows.Settings/build.ps1 | 538 +++++++++ .../Microsoft.Windows.Settings/build.yaml | 184 +++ .../Microsoft.Windows.Settings/codecov.yml | 35 + .../source/Classes/001.WindowsReason.ps1 | 21 + .../source/Classes/010.SettingsBase.ps1 | 121 ++ .../source/Classes/020.FindMyDevice.ps1 | 97 ++ .../source/Classes/020.General.ps1 | 88 ++ .../source/DSCResources/RegistryKeyData.psd1 | 63 + .../source/Enum/001.SettingStatus.ps1 | 6 + .../source/Microsoft.Windows.Settings.psd1 | 143 +++ .../source/Private/Get-RegistryKeyData.ps1 | 31 + .../source/Private/Get-RegistryStatus.ps1 | 69 ++ .../source/Private/Set-RegistryStatus.ps1 | 51 + .../source/en-US/FindMyDevice.strings.psd1 | 15 + .../source/en-US/General.strings.psd1 | 15 + .../Microsoft.Windows.Settings.strings.psd1 | 16 + .../source/en-US/SettingsBase.strings.psd1 | 10 + .../about_Microsoft.Windows.Settings.help.txt | 24 + .../source/prefix.ps1 | 7 + .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 326 +++++ .../Unit/Classes/WindowsReason.tests.ps1 | 76 ++ .../Private/Get-RegistryKeyData.tests.ps1 | 46 + .../Unit/Private/Get-RegistryStatus.tests.ps1 | 96 ++ .../Unit/Private/Set-RegistryStatus.tests.ps1 | 105 ++ 35 files changed, 3829 insertions(+), 12 deletions(-) create mode 100644 .vscode/analyzersettings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/.gitattributes create mode 100644 resources/Microsoft.Windows.Settings/CHANGELOG.md create mode 100644 resources/Microsoft.Windows.Settings/GitVersion.yml create mode 100644 resources/Microsoft.Windows.Settings/README.md create mode 100644 resources/Microsoft.Windows.Settings/RequiredModules.psd1 create mode 100644 resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 create mode 100644 resources/Microsoft.Windows.Settings/Resolve-Dependency.psd1 create mode 100644 resources/Microsoft.Windows.Settings/azure-pipelines.yml create mode 100644 resources/Microsoft.Windows.Settings/build.ps1 create mode 100644 resources/Microsoft.Windows.Settings/build.yaml create mode 100644 resources/Microsoft.Windows.Settings/codecov.yml create mode 100644 resources/Microsoft.Windows.Settings/source/Classes/001.WindowsReason.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 create mode 100644 resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/en-US/General.strings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/en-US/Microsoft.Windows.Settings.strings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/en-US/SettingsBase.strings.psd1 create mode 100644 resources/Microsoft.Windows.Settings/source/en-US/about_Microsoft.Windows.Settings.help.txt create mode 100644 resources/Microsoft.Windows.Settings/source/prefix.ps1 create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Classes/WindowsReason.tests.ps1 create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 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..baf005f0 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/CHANGELOG.md @@ -0,0 +1,31 @@ +# 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 + +- For new features. + +### Changed + +- For changes in existing functionality. + +### Deprecated + +- For soon-to-be removed features. + +### Removed + +- For now removed features. + +### Fixed + +- For any bug fix. + +### Security + +- In case of vulnerabilities. + 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..18f0160e --- /dev/null +++ b/resources/Microsoft.Windows.Settings/README.md @@ -0,0 +1,46 @@ +# Microsoft.Windows.Settings + + +This module contains DSC resources for the management and +configuration of PRODUCT. + + +[![Build Status](https://dev.azure.com/dsccommunity/Microsoft.Windows.Settings/_apis/build/status/dsccommunity.Microsoft.Windows.Settings?branchName=main)](https://dev.azure.com/dsccommunity/Microsoft.Windows.Settings/_build/latest?definitionId=9999&branchName=main) +![Azure DevOps coverage (branch)](https://img.shields.io/azure-devops/coverage/dsccommunity/Microsoft.Windows.Settings/9999/main) +[![codecov](https://codecov.io/gh/dsccommunity/Microsoft.Windows.Settings/branch/main/graph/badge.svg)](https://codecov.io/gh/dsccommunity/Microsoft.Windows.Settings) +[![Azure DevOps tests](https://img.shields.io/azure-devops/tests/dsccommunity/Microsoft.Windows.Settings/9999/main)](https://dsccommunity.visualstudio.com/Microsoft.Windows.Settings/_test/analytics?definitionId=9999&contextType=build) +[![PowerShell Gallery (with prereleases)](https://img.shields.io/powershellgallery/vpre/Microsoft.Windows.Settings?label=Microsoft.Windows.Settings%20Preview)](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings/) +[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/Microsoft.Windows.Settings?label=Microsoft.Windows.Settings)](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings/) + +## Code of Conduct + +This project has adopted this [Code of Conduct](CODE_OF_CONDUCT.md). + +## Releases + +For each merge to the branch `main` a preview release will be +deployed to [PowerShell Gallery](https://www.powershellgallery.com/). +Periodically a release version tag will be pushed which will deploy a +full release to [PowerShell Gallery](https://www.powershellgallery.com/). + +## Contributing + +Please check out common DSC Community [contributing guidelines](https://dsccommunity.org/guidelines/contributing). + +## Change log + +A full list of changes in each version can be found in the [change log](CHANGELOG.md). + +## Documentation + +The documentation can be found in the [Microsoft.Windows.Settings Wiki](https://github.com/dsccommunity/Microsoft.Windows.Settings/wiki). +The DSC resources schema files is used to automatically update the +documentation on each PR merge. + +### Examples + +You can review the [Examples](/source/Examples) directory in the Microsoft.Windows.Settings module +for some general use scenarios for all of the resources that are in the module. + +The resource examples are also available in the [Microsoft.Windows.Settings Wiki](https://github.com/dsccommunity/Microsoft.Windows.Settings/wiki). + diff --git a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 new file mode 100644 index 00000000..baf6d306 --- /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 = 'latest' + PSScriptAnalyzer = 'latest' + Pester = 'latest' + ModuleBuilder = 'latest' + ChangelogManagement = 'latest' + Sampler = 'latest' + 'Sampler.GitHubTasks' = 'latest' + MarkdownLinkCheck = 'latest' + 'DscResource.Base' = 'latest' + 'DscResource.Common' = 'latest' + 'DscResource.Test' = 'latest' + 'DscResource.AnalyzerRules' = 'latest' + xDscResourceDesigner = 'latest' + 'DscResource.DocGenerator' = 'latest' +} + diff --git a/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 b/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 new file mode 100644 index 00000000..17cc98ec --- /dev/null +++ b/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 @@ -0,0 +1,1060 @@ +<# + .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. + } + ) + + if ($WithYAML) + { + $modulesToSave += @{ + Name = 'PowerShell-Yaml' + } + } + + # 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..8d9a03d6 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/build.yaml @@ -0,0 +1,184 @@ +--- +#################################################### +# 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: 85 + 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/codecov.yml b/resources/Microsoft.Windows.Settings/codecov.yml new file mode 100644 index 00000000..4489cf25 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/codecov.yml @@ -0,0 +1,35 @@ +codecov: + require_ci_to_pass: no + # master should be the baseline for reporting + branch: main + +comment: + layout: "reach, diff, flags, files" + behavior: default + +coverage: + range: 50..80 + round: down + precision: 0 + + status: + project: + default: + # Set the overall project code coverage requirement to 70% + target: 70 + patch: + default: + # Set the pull request requirement to not regress overall coverage by more than 5% + # and let codecov.io set the goal for the code changed in the patch. + target: auto + threshold: 5 + +# This is not needed if the module only contain class-based resources. +fixes: + - '^\d+\.\d+\.\d+::source' # move path "X.Y.Z" => "source" + +# Use this if there are paths that should not be part of the code coverage, for +# example a deprecated function where tests has been removed. +#ignore: +# - 'source/Public/Get-Deprecated.ps1' + 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..45aaccdb --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -0,0 +1,121 @@ +<# + .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 SettingsBase : ResourceBase +{ + [DscProperty(Key)] + [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 '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 { $_.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..2247194b --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -0,0 +1,97 @@ +<# + .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 : ResourceBase +{ + [DscProperty(Key)] + [ValidateSet('Yes')] + [System.String] + $IsSingleInstance = 'Yes' + + [DscProperty()] + [nullable[System.Boolean]] + $FindMyDevice + + [DscProperty(NotConfigurable)] + [WindowsReason[]] + $Reasons + + FindMyDevice() : base ($PSScriptRoot) + { + # These properties will not be enforced. + $this.ExcludeDscProperties = @( + 'IsSingleInstance' + ) + } + + [FindMyDevice] 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) + { + $state = @{} + + # Get the registry key data + $keyData = Get-RegistryKeyData -Key 'FindMyDevice' + + # Check if the registry path and key exists including the value + $settingState = Get-RegistryStatus @keyData + + $state.Add('FindMyDevice', [System.Boolean]$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) + { + if ($null -ne $this.FindMyDevice) + { + $this.SetFindMyDevice($properties.FindMyDevice) + } + } + + [System.Boolean] Test() + { + # Call the base method to test all of the properties that should be enforced. + return ([ResourceBase] $this).Test() + } + + hidden [void] SetFindMyDevice() + { + $keyData = Get-RegistryKeyData -Key 'FindMyDevice' -OnlyKeyPath + + # Add the value to the key data + $keyData.Add('Value', [System.Int32]$this.Value) + + # Set the registry with the new value + Set-RegistryStatus @keyData + } +} 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..9b53b63a --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -0,0 +1,88 @@ + +<# + .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 FindMyDevice + Specifies whether the Find My Device setting should be enabled or disabled. + + .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. +#> +[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..e68efddf --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -0,0 +1,63 @@ +@{ + "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 = 'Disabled' + } + }, + @{ + PropertyName = 'EnableLocalContentByLanguageList' + Name = 'HttpAcceptLanguageOptOut' + Path = 'HKCU:\Control Panel\International\User Profile\' + Status = @{ + Enabled = 0 + Disabled = 1 + Default = 'Enabled' + } + }, + @{ + PropertyName = 'EnableAppLaunchTracking' + Name = 'Start_TrackProgs' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 'Enabled' + } + } + @{ + PropertyName = 'ShowContentSuggestion' + Name = @('SubscribedContent-338393Enabled', 'SubscribedContent-353694Enabled', 'SubscribedContent-353696Enabled') + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 'Enabled' + } + } + @{ + PropertyName = 'EnableAccountNotifications' + Name = 'EnableAccountNotifications' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SystemSettings\AccountNotifications\' + Status = @{ + Enabled = 1 + Disabled = 0 + Default = 'Enabled' + } + } + ) +} 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..cb935d19 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 @@ -0,0 +1,6 @@ + +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..72a35726 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -0,0 +1,143 @@ +# +# 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 = '5.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 = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # 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 = '' + +} + + + + + + + + + + + + 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..e326c17b --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 @@ -0,0 +1,31 @@ +# TODO: Add description +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..7d99bb08 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -0,0 +1,69 @@ +# TODO: Add description +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 + ) + + try + { + $registryValue = if ($Name.Count -gt 1) + { + $Name | ForEach-Object { + Get-ItemPropertyValue -Path $Path -Name $_ -ErrorAction Stop + } + } + else + { + Get-ItemPropertyValue -Path $Path -Name $Name -ErrorAction Stop + } + } + catch + { + # TODO: Localize the verbose message, not error + Write-Verbose -Message $_.Exception.Message -Verbose + } + + $Key = if ($registryValue -or $registryValue -eq 0) + { + 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.RegistryManualManipulation_ErrorMessage -f $Path + New-InvalidDataException ` + -ErrorId 'RegistryManualManipulationError' ` + -Message $errorMessage + } + + # Return the first value as the registry value is the same + $registryValue = $registryValue[0] + } + + $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key + } + else + { + Write-Verbose -Message ($script:localizedData.GetRegistryKeyData_DefaultMessage -f $Status.Default, $Path) + $Status.Default + } + + 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..e259da4e --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 @@ -0,0 +1,51 @@ +# TODO: Add description +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..cdff2091 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 @@ -0,0 +1,15 @@ +<# + .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. + FindMyDeviceRegistryKey = Searching '{0}' key in '{1}' (0001). +'@ 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..01299acc --- /dev/null +++ b/resources/Microsoft.Windows.Settings/source/en-US/Microsoft.Windows.Settings.strings.psd1 @@ -0,0 +1,16 @@ +<# + .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 + RegistryManualManipulation_ErrorMessage = "The registry path '{0}' is not in the expected state. Entries have been manually modified. Cannot determine the current state." + GetRegistryKeyData_DefaultMessage = "Returning default value '{0}' for '{1}'." + + ## Get-RegistryKeyData + GetRegistryKeyData_SearchMessage = Searching '{0}' key in '{1}' (0001). +'@ 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..f6c24a27 --- /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:\> {{ add examples here }} + +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 Github repository for issues and new releases. + +SEE ALSO + - {{ Please add Project URI such as github }}} + +KEYWORDS + {{ Add comma separated keywords here }} + 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/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 new file mode 100644 index 00000000..99ff1564 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -0,0 +1,326 @@ +<# + .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 'Microsoft.Windows.Settings' { + 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 the system is in the desired state' { + 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 = [System.Boolean] $true + } + } -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 $true + + $currentState.Reasons | Should -BeNullOrEmpty + } + } + } + } +} + +Describe 'Microsoft.Windows.Settings\Set()' -Tag 'Set' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice] @{ + IsSingleInstance = 'Yes' + FindMyDevice = $null # The default desired state + } | + # 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 = $false + ActualValue = $true + } + ) + } -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 'Microsoft.Windows.Settings\GetCurrentState()' -Tag 'HiddenMember' { + Context 'When object is missing in the current state' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [FindMyDevice] @{ + FindMyDevice = $false + } + } + } + + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.GetCurrentState( + @{} + ) + + $currentState.IsSingleInstance | Should -BeNullOrEmpty + $currentState.FindMyDevice | Should -Be $false + } + } +} + + +Describe 'FindMyDevice\SetFindMyDevice()' -Tag 'HiddenMember' { + BeforeAll { + Mock -CommandName Set-RegistryStatus + Mock -CommandName Set-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 = $false + } + + + } + } + + It 'Should call the correct mock' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $script:mockInstance.SetFindMyDevice() + } + + 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..6de31c03 --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 @@ -0,0 +1,46 @@ +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' { + # TODO: Add tests +} 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..f503fd6f --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 @@ -0,0 +1,96 @@ +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' = 'Unknown' + } + 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 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 'HKLM:\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..406dcbba --- /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 = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues' + Name = 'LocationSyncEnabled' + Value = '1' + ExpectedKey = @{ + 'LocationSyncEnabled' = '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.LocationSyncEnabled + } + + Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 + } + } + + Context 'Set registry key when it does not exist' { + BeforeAll { + Mock -CommandName New-Item + Mock -CommandName Set-ItemProperty -MockWith { + return 0 + } + } + + BeforeDiscovery { + $testCases = @( + @{ + Path = 'HKCU:\1\2\3' + Name = 'MyValue' + Value = '0' + ExpectedKey = '0' + } + ) + } + + 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 Set-ItemProperty -Exactly -Times 1 + } + } +} From d55478ed3563636ac55c4e5e8b893cf0b5f98a9e Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 08:34:55 +0100 Subject: [PATCH 02/21] Fix test for FindMyDevice --- .../Microsoft.Windows.Settings/build.yaml | 2 +- .../source/Classes/010.SettingsBase.ps1 | 6 +- .../source/Classes/020.FindMyDevice.ps1 | 55 +++------------- .../source/DSCResources/RegistryKeyData.psd1 | 63 ++++++++++--------- .../source/Private/Get-RegistryStatus.ps1 | 18 ++++-- .../Microsoft.Windows.Settings.strings.psd1 | 8 ++- .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 21 +++---- .../Private/Get-RegistryKeyData.tests.ps1 | 55 +++++++++++++++- .../Unit/Private/Get-RegistryStatus.tests.ps1 | 2 +- .../Unit/Private/Set-RegistryStatus.tests.ps1 | 4 +- 10 files changed, 130 insertions(+), 104 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/build.yaml b/resources/Microsoft.Windows.Settings/build.yaml index 8d9a03d6..aba032ac 100644 --- a/resources/Microsoft.Windows.Settings/build.yaml +++ b/resources/Microsoft.Windows.Settings/build.yaml @@ -92,7 +92,7 @@ Pester: StackTraceVerbosity: Full CIFormat: Auto CodeCoverage: - CoveragePercentTarget: 85 + CoveragePercentTarget: 80 OutputPath: JaCoCo_coverage.xml OutputEncoding: ascii UseBreakpoints: false diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 index 45aaccdb..8ee3a006 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -63,7 +63,7 @@ class SettingsBase : ResourceBase # Remove the PropertyName key from the key data $key.Remove('PropertyName') - + # Get the status of the property $settingState = Get-RegistryStatus @key @@ -98,8 +98,8 @@ class SettingsBase : ResourceBase # Build the parameters for Set-RegistryStatus $params = @{ - Name = $entry.Name - Path = $entry.Path + Name = $entry.Name + Path = $entry.Path Value = $entry.Status[$propertyValue] } diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 index 2247194b..57e62c24 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -15,7 +15,7 @@ Returns the reason a property is not in the desired state. #> [DscResource()] -class FindMyDevice : ResourceBase +class FindMyDevice : SettingsBase { [DscProperty(Key)] [ValidateSet('Yes')] @@ -23,75 +23,38 @@ class FindMyDevice : ResourceBase $IsSingleInstance = 'Yes' [DscProperty()] - [nullable[System.Boolean]] - $FindMyDevice + [SettingStatus] $FindMyDevice [DscProperty(NotConfigurable)] [WindowsReason[]] $Reasons - FindMyDevice() : base ($PSScriptRoot) + 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 ([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) - { - $state = @{} - - # Get the registry key data - $keyData = Get-RegistryKeyData -Key 'FindMyDevice' - - # Check if the registry path and key exists including the value - $settingState = Get-RegistryStatus @keyData - - $state.Add('FindMyDevice', [System.Boolean]$settingState) - - return $state + return ([SettingsBase] $this).Get() } [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) - { - if ($null -ne $this.FindMyDevice) - { - $this.SetFindMyDevice($properties.FindMyDevice) - } + ([SettingsBase] $this).Set() } [System.Boolean] Test() { # Call the base method to test all of the properties that should be enforced. - return ([ResourceBase] $this).Test() - } - - hidden [void] SetFindMyDevice() - { - $keyData = Get-RegistryKeyData -Key 'FindMyDevice' -OnlyKeyPath - - # Add the value to the key data - $keyData.Add('Value', [System.Int32]$this.Value) - - # Set the registry with the new value - Set-RegistryStatus @keyData + return ([SettingsBase] $this).Test() } } diff --git a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 index e68efddf..d5df7910 100644 --- a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -1,62 +1,63 @@ @{ "FindMyDevice" = @{ - Name = 'LocationSyncEnabled' - Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' - Status = @{ - Enabled = 1 + PropertyName = 'FindMyDevice' + Name = 'LocationSyncEnabled' + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' + Status = @{ + Enabled = 1 Disabled = 0 - Default = 0 + Default = 0 } } - "General" = @( + "General" = @( @{ PropertyName = 'EnablePersonalizedAds' - Name = 'Enabled' - Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo\' - Status = @{ - Enabled = 1 + Name = 'Enabled' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\AdvertisingInfo\' + Status = @{ + Enabled = 1 Disabled = 0 - Default = 'Disabled' + Default = 'Disabled' } }, @{ PropertyName = 'EnableLocalContentByLanguageList' - Name = 'HttpAcceptLanguageOptOut' - Path = 'HKCU:\Control Panel\International\User Profile\' - Status = @{ - Enabled = 0 + Name = 'HttpAcceptLanguageOptOut' + Path = 'HKCU:\Control Panel\International\User Profile\' + Status = @{ + Enabled = 0 Disabled = 1 - Default = 'Enabled' + Default = 'Enabled' } }, @{ PropertyName = 'EnableAppLaunchTracking' - Name = 'Start_TrackProgs' - Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' - Status = @{ - Enabled = 1 + Name = 'Start_TrackProgs' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced\' + Status = @{ + Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 'Enabled' } } @{ PropertyName = 'ShowContentSuggestion' - Name = @('SubscribedContent-338393Enabled', 'SubscribedContent-353694Enabled', 'SubscribedContent-353696Enabled') - Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\' - Status = @{ - Enabled = 1 + Name = @('SubscribedContent-338393Enabled', 'SubscribedContent-353694Enabled', 'SubscribedContent-353696Enabled') + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager\' + Status = @{ + Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 'Enabled' } } @{ PropertyName = 'EnableAccountNotifications' - Name = 'EnableAccountNotifications' - Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SystemSettings\AccountNotifications\' - Status = @{ - Enabled = 1 + Name = 'EnableAccountNotifications' + Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\SystemSettings\AccountNotifications\' + Status = @{ + Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 'Enabled' } } ) diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 index 7d99bb08..9ade7fd5 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -24,11 +24,13 @@ function Get-RegistryStatus $registryValue = if ($Name.Count -gt 1) { $Name | ForEach-Object { + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_SearchMessage -f $_, $Path) Get-ItemPropertyValue -Path $Path -Name $_ -ErrorAction Stop } } else { + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_SearchMessage -f $Name, $Path) Get-ItemPropertyValue -Path $Path -Name $Name -ErrorAction Stop } } @@ -38,7 +40,7 @@ function Get-RegistryStatus Write-Verbose -Message $_.Exception.Message -Verbose } - $Key = if ($registryValue -or $registryValue -eq 0) + [System.String]$Key = if ($registryValue -or $registryValue -eq 0) { if ($registryValue.Count -gt 1) { @@ -47,23 +49,31 @@ function Get-RegistryStatus $groupCount = $registryValue | Group-Object if ($groupCount.Name.Count -ne 1) { - $errorMessage = $script:localizedData.RegistryManualManipulation_ErrorMessage -f $Path + $errorMessage = $script:localizedData.GetRegistryStatusRegistryManualManipulation_ErrorMessage -f $Path New-InvalidDataException ` -ErrorId 'RegistryManualManipulationError' ` -Message $errorMessage } - # Return the first value as the registry value is the same + # Return the first value as the registry value "should" be the same $registryValue = $registryValue[0] } + # Remove the default + if ($Status.ContainsKey('Default')) + { + $Status.Remove('Default') + } + + # Search the possible two values $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key } else { - Write-Verbose -Message ($script:localizedData.GetRegistryKeyData_DefaultMessage -f $Status.Default, $Path) + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_DefaultMessage -f $Status.Default, $Path) $Status.Default } + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_FoundMessage -f $Key, $Path) return $Key } 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 index 01299acc..617b9ee0 100644 --- 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 @@ -8,9 +8,11 @@ ConvertFrom-StringData @' ## Get-RegistryStatus - RegistryManualManipulation_ErrorMessage = "The registry path '{0}' is not in the expected state. Entries have been manually modified. Cannot determine the current state." - GetRegistryKeyData_DefaultMessage = "Returning default value '{0}' for '{1}'." + 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}' (0001). + GetRegistryKeyData_SearchMessage = Searching '{0}' key in '{1}'. '@ diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 index 99ff1564..ac174bfd 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -98,7 +98,7 @@ Describe 'FindMyDevice\Get()' -Tag 'Get' { Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return @{ IsSingleInstance = 'Yes' - FindMyDevice = [System.Boolean] $true + FindMyDevice = [SettingStatus]::Enabled } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -114,7 +114,7 @@ Describe 'FindMyDevice\Get()' -Tag 'Get' { $currentState = $script:mockInstance.Get() $currentState.IsSingleInstance | Should -Be 'Yes' - $currentState.FindMyDevice | Should -Be $true + $currentState.FindMyDevice | Should -Be 'Enabled' $currentState.Reasons | Should -BeNullOrEmpty } @@ -130,7 +130,6 @@ Describe 'Microsoft.Windows.Settings\Set()' -Tag 'Set' { $script:mockInstance = [FindMyDevice] @{ IsSingleInstance = 'Yes' - FindMyDevice = $null # The default desired state } | # Mock method Modify which is called by the case method Set(). Add-Member -Force -MemberType 'ScriptMethod' -Name 'Modify' -Value { @@ -185,8 +184,8 @@ Describe 'Microsoft.Windows.Settings\Set()' -Tag 'Set' { return @( @{ Property = 'FindMyDevice' - ExpectedValue = $false - ActualValue = $true + ExpectedValue = 'Disabled' + ActualValue = 'Enabled' } ) } -PassThru | @@ -272,7 +271,7 @@ Describe 'Microsoft.Windows.Settings\GetCurrentState()' -Tag 'HiddenMember' { Set-StrictMode -Version 1.0 $script:mockInstance = [FindMyDevice] @{ - FindMyDevice = $false + FindMyDevice = 'Disabled' } } } @@ -288,13 +287,13 @@ Describe 'Microsoft.Windows.Settings\GetCurrentState()' -Tag 'HiddenMember' { ) $currentState.IsSingleInstance | Should -BeNullOrEmpty - $currentState.FindMyDevice | Should -Be $false + $currentState.FindMyDevice | Should -Be 'Disabled' } } } -Describe 'FindMyDevice\SetFindMyDevice()' -Tag 'HiddenMember' { +Describe 'FindMyDevice\Set()' -Tag 'HiddenMember' { BeforeAll { Mock -CommandName Set-RegistryStatus Mock -CommandName Set-ItemProperty -MockWith { return $true } @@ -306,10 +305,8 @@ Describe 'FindMyDevice\SetFindMyDevice()' -Tag 'HiddenMember' { Set-StrictMode -Version 1.0 $script:mockInstance = [FindMyDevice]@{ - FindMyDevice = $false + FindMyDevice = 'Enabled' } - - } } @@ -317,7 +314,7 @@ Describe 'FindMyDevice\SetFindMyDevice()' -Tag 'HiddenMember' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 - $script:mockInstance.SetFindMyDevice() + $script:mockInstance.Set() } Should -Invoke -CommandName Set-RegistryStatus -Exactly -Times 1 -Scope It 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 index 6de31c03..11668ef5 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryKeyData.tests.ps1 @@ -42,5 +42,58 @@ AfterAll { } Describe 'Get-RegistryKeyData' -Tag 'Private' { - # TODO: Add tests + 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 index f503fd6f..7018621c 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 @@ -86,7 +86,7 @@ Describe 'Get-RegistryStatus' -Tag 'Private' { It 'Should return empty result' { InModuleScope -ScriptBlock { - $result = Get-RegistryStatus -Path 'HKLM:\1\2\3' -Name 'Empty' -Status @{} + $result = Get-RegistryStatus -Path 'HKCU:\1\2\3' -Name 'Empty' -Status @{} $result | Should -BeNullOrEmpty } 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 index 406dcbba..635034e6 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 @@ -87,7 +87,7 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { Path = 'HKCU:\1\2\3' Name = 'MyValue' Value = '0' - ExpectedKey = '0' + ExpectedKey = $null } ) } @@ -99,7 +99,7 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { } Should -Invoke -CommandName New-Item -Exactly -Times 1 - Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 + # Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 } } } From c2d9ea82bcc830f103dd26b284e36ab6d532f1a4 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 12:44:53 +0100 Subject: [PATCH 03/21] Update wiki --- .../Microsoft.Windows.Settings/README.md | 53 ++---- .../Microsoft.Windows.Settings/codecov.yml | 35 ---- .../source/Microsoft.Windows.Settings.psd1 | 158 +++++++++--------- .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 6 +- .../tests/Unit/Classes/General.tests.ps1 | 77 +++++++++ .../Unit/Private/Get-RegistryStatus.tests.ps1 | 35 +++- 6 files changed, 210 insertions(+), 154 deletions(-) delete mode 100644 resources/Microsoft.Windows.Settings/codecov.yml create mode 100644 resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 diff --git a/resources/Microsoft.Windows.Settings/README.md b/resources/Microsoft.Windows.Settings/README.md index 18f0160e..b654ff22 100644 --- a/resources/Microsoft.Windows.Settings/README.md +++ b/resources/Microsoft.Windows.Settings/README.md @@ -1,46 +1,27 @@ # Microsoft.Windows.Settings - -This module contains DSC resources for the management and -configuration of PRODUCT. +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. - -[![Build Status](https://dev.azure.com/dsccommunity/Microsoft.Windows.Settings/_apis/build/status/dsccommunity.Microsoft.Windows.Settings?branchName=main)](https://dev.azure.com/dsccommunity/Microsoft.Windows.Settings/_build/latest?definitionId=9999&branchName=main) -![Azure DevOps coverage (branch)](https://img.shields.io/azure-devops/coverage/dsccommunity/Microsoft.Windows.Settings/9999/main) -[![codecov](https://codecov.io/gh/dsccommunity/Microsoft.Windows.Settings/branch/main/graph/badge.svg)](https://codecov.io/gh/dsccommunity/Microsoft.Windows.Settings) -[![Azure DevOps tests](https://img.shields.io/azure-devops/tests/dsccommunity/Microsoft.Windows.Settings/9999/main)](https://dsccommunity.visualstudio.com/Microsoft.Windows.Settings/_test/analytics?definitionId=9999&contextType=build) -[![PowerShell Gallery (with prereleases)](https://img.shields.io/powershellgallery/vpre/Microsoft.Windows.Settings?label=Microsoft.Windows.Settings%20Preview)](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings/) -[![PowerShell Gallery](https://img.shields.io/powershellgallery/v/Microsoft.Windows.Settings?label=Microsoft.Windows.Settings)](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings/) +## Wiki and module under construction :construction: -## Code of Conduct +## Resources :construction: -This project has adopted this [Code of Conduct](CODE_OF_CONDUCT.md). +- **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. -## Releases +## Usage :construction: -For each merge to the branch `main` a preview release will be -deployed to [PowerShell Gallery](https://www.powershellgallery.com/). -Periodically a release version tag will be pushed which will deploy a -full release to [PowerShell Gallery](https://www.powershellgallery.com/). +To use these resources, import the module and invoke the desired DSC resource with the appropriate parameters. -## Contributing +```powershell +Import-Module -Name Microsoft.Windows.Settings -Please check out common DSC Community [contributing guidelines](https://dsccommunity.org/guidelines/contributing). - -## Change log - -A full list of changes in each version can be found in the [change log](CHANGELOG.md). - -## Documentation - -The documentation can be found in the [Microsoft.Windows.Settings Wiki](https://github.com/dsccommunity/Microsoft.Windows.Settings/wiki). -The DSC resources schema files is used to automatically update the -documentation on each PR merge. - -### Examples - -You can review the [Examples](/source/Examples) directory in the Microsoft.Windows.Settings module -for some general use scenarios for all of the resources that are in the module. - -The resource examples are also available in the [Microsoft.Windows.Settings Wiki](https://github.com/dsccommunity/Microsoft.Windows.Settings/wiki). +Invoke-DscResource -ModuleName Microsoft.Windows.Settings -Name AdvancedAppSettings -Method Set -Property @{ + SID = 'S-1-5-21-1234567890-123456789-1234567890-1001'; + AppSourcePreference = 'PreferStore'; + ShareDeviceExperience = 'Everyone'; + ArchiveApp = $true; +} +``` +For more detailed examples and parameter descriptions, refer to the individual resource documentation. \ No newline at end of file diff --git a/resources/Microsoft.Windows.Settings/codecov.yml b/resources/Microsoft.Windows.Settings/codecov.yml deleted file mode 100644 index 4489cf25..00000000 --- a/resources/Microsoft.Windows.Settings/codecov.yml +++ /dev/null @@ -1,35 +0,0 @@ -codecov: - require_ci_to_pass: no - # master should be the baseline for reporting - branch: main - -comment: - layout: "reach, diff, flags, files" - behavior: default - -coverage: - range: 50..80 - round: down - precision: 0 - - status: - project: - default: - # Set the overall project code coverage requirement to 70% - target: 70 - patch: - default: - # Set the pull request requirement to not regress overall coverage by more than 5% - # and let codecov.io set the goal for the code changed in the patch. - target: auto - threshold: 5 - -# This is not needed if the module only contain class-based resources. -fixes: - - '^\d+\.\d+\.\d+::source' # move path "X.Y.Z" => "source" - -# Use this if there are paths that should not be part of the code coverage, for -# example a deprecated function where tests has been removed. -#ignore: -# - 'source/Public/Get-Deprecated.ps1' - diff --git a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 index 72a35726..d466eb5f 100644 --- a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -8,125 +8,125 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'Microsoft.Windows.Settings.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'Microsoft.Windows.Settings.psm1' -# Version number of this module. -ModuleVersion = '0.0.1' + # Version number of this module. + ModuleVersion = '0.0.1' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = '2c3faffd-05bc-4178-a8e2-e42618bdce6b' + # ID used to uniquely identify this module + GUID = '2c3faffd-05bc-4178-a8e2-e42618bdce6b' -# Author of this module -Author = 'Microsoft Corporation' + # Author of this module + Author = 'Microsoft Corporation' -# Company or vendor of this module -CompanyName = 'Microsoft Corporation' + # Company or vendor of this module + CompanyName = 'Microsoft Corporation' -# Copyright statement for this module -Copyright = '(c) Microsoft Corporation. All rights reserved.' + # 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' + # 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 = '5.0' + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.0' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # 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 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 = '' + # 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 = '' + # 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 = @() + # 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 = @() + # 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 = @() + # 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 = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # 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 = @() + # 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 = @() + # 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 = @() + # 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 = @() + # 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 = @() + # 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 = @() + # DSC resources to export from this module + DscResourcesToExport = @() -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # 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 = @{ + # 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 = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # 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 = '' + # 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 = '' + # 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 = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - ReleaseNotes = '' + # ReleaseNotes of this module + ReleaseNotes = '' - # Prerelease string of this module - Prerelease = '' + # Prerelease string of this module + Prerelease = '' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 index ac174bfd..50751ea0 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -46,7 +46,7 @@ AfterAll { Get-Module -Name $script:dscModuleName -All | Remove-Module -Force } -Describe 'Microsoft.Windows.Settings' { +Describe 'FindMyDevice' { Context 'When class is instantiated' { It 'Should not throw an exception' { InModuleScope -ScriptBlock { @@ -123,7 +123,7 @@ Describe 'FindMyDevice\Get()' -Tag 'Get' { } } -Describe 'Microsoft.Windows.Settings\Set()' -Tag 'Set' { +Describe 'FindMyDevice\Set()' -Tag 'Set' { BeforeAll { InModuleScope -Scriptblock { Set-StrictMode -Version 1.0 @@ -264,7 +264,7 @@ Describe 'FindMyDevice\Test()' -Tag 'Test' { } } -Describe 'Microsoft.Windows.Settings\GetCurrentState()' -Tag 'HiddenMember' { +Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { Context 'When object is missing in the current state' { BeforeAll { InModuleScope -Scriptblock { 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..c1313c2f --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 @@ -0,0 +1,77 @@ +<# + .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' + } + } + } +} \ No newline at end of file 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 index 7018621c..b4d4e984 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 @@ -67,7 +67,7 @@ Describe 'Get-RegistryStatus' -Tag 'Private' { ) } - It 'Should return the registry value '''' on ''''' -ForEach $testCases { + 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 @@ -77,6 +77,39 @@ Describe 'Get-RegistryStatus' -Tag 'Private' { } } + 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' = 'Unknown' + } + 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 { From d206fb997bca243ee1c87849896433152cee5539 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 12:46:08 +0100 Subject: [PATCH 04/21] Update markdownlinter --- resources/Microsoft.Windows.Settings/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/Microsoft.Windows.Settings/CHANGELOG.md b/resources/Microsoft.Windows.Settings/CHANGELOG.md index baf005f0..74ee4ea4 100644 --- a/resources/Microsoft.Windows.Settings/CHANGELOG.md +++ b/resources/Microsoft.Windows.Settings/CHANGELOG.md @@ -28,4 +28,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - In case of vulnerabilities. - From 168126bce2af8c47898dc1af898dd957f8b5b440 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 12:47:24 +0100 Subject: [PATCH 05/21] Update spelling --- .github/actions/spelling/allow.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 1c720079..f825e458 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -67,4 +67,22 @@ websites wekyb Hmmss MMdd -MMdd \ No newline at end of file +azdoserver +Codecov +eature +HQRM +hqrmtest +modulefast +MOF +nupkg +powershellgallery +quickconfig +QWord +registrering +schange +setvariable +sourcing +splatting +updatebuildnumber +vso +winrm \ No newline at end of file From 61a685551c884abfbe0a1127dfb2be9c5af50793 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 13:04:34 +0100 Subject: [PATCH 06/21] Add synopsis and description --- .../Microsoft.Windows.Settings/README.md | 2 +- .../source/DSCResources/RegistryKeyData.psd1 | 13 +++++-- .../source/Enum/001.SettingStatus.ps1 | 6 +++ .../source/Private/Get-RegistryKeyData.ps1 | 21 ++++++++++- .../source/Private/Get-RegistryStatus.ps1 | 37 +++++++++++-------- .../source/Private/Set-RegistryStatus.ps1 | 27 ++++++++++++-- 6 files changed, 81 insertions(+), 25 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/README.md b/resources/Microsoft.Windows.Settings/README.md index b654ff22..6c253239 100644 --- a/resources/Microsoft.Windows.Settings/README.md +++ b/resources/Microsoft.Windows.Settings/README.md @@ -24,4 +24,4 @@ Invoke-DscResource -ModuleName Microsoft.Windows.Settings -Name AdvancedAppSetti } ``` -For more detailed examples and parameter descriptions, refer to the individual resource documentation. \ No newline at end of file +For more detailed examples and parameter descriptions, refer to the individual resource documentation. diff --git a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 index d5df7910..f739d94f 100644 --- a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -1,9 +1,14 @@ +<# + .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 = @{ + Name = 'LocationSyncEnabled' + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' + Status = @{ Enabled = 1 Disabled = 0 Default = 0 diff --git a/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 index cb935d19..f920339f 100644 --- a/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 @@ -1,4 +1,10 @@ +<# + .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 diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 index e326c17b..57d60f9e 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 @@ -1,4 +1,21 @@ -# TODO: Add description +<# + .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()] @@ -14,7 +31,7 @@ function Get-RegistryKeyData ) $registryDataPath = Join-Path -Path $PSScriptRoot -ChildPath 'DSCResources\RegistryKeyData.psd1' - Write-Verbose -Message ($script:localizedData.GetRegistryKeyData_SearchMessage -f $Key, $registryDataPath) + Write-Verbose -Message ($script:localizedData.FindMyDeviceRegistryKey -f $Key, $registryDataPath) $import = Import-PowerShellDataFile -Path $registryDataPath $entry = $import.$Key diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 index 9ade7fd5..1b02a1bf 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -1,4 +1,22 @@ -# TODO: Add description +<# + .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 } +#> function Get-RegistryStatus { [CmdletBinding()] @@ -24,13 +42,11 @@ function Get-RegistryStatus $registryValue = if ($Name.Count -gt 1) { $Name | ForEach-Object { - Write-Verbose -Message ($script:localizedData.GetRegistryStatus_SearchMessage -f $_, $Path) Get-ItemPropertyValue -Path $Path -Name $_ -ErrorAction Stop } } else { - Write-Verbose -Message ($script:localizedData.GetRegistryStatus_SearchMessage -f $Name, $Path) Get-ItemPropertyValue -Path $Path -Name $Name -ErrorAction Stop } } @@ -40,7 +56,7 @@ function Get-RegistryStatus Write-Verbose -Message $_.Exception.Message -Verbose } - [System.String]$Key = if ($registryValue -or $registryValue -eq 0) + $Key = if ($registryValue -or $registryValue -eq 0) { if ($registryValue.Count -gt 1) { @@ -49,31 +65,22 @@ function Get-RegistryStatus $groupCount = $registryValue | Group-Object if ($groupCount.Name.Count -ne 1) { - $errorMessage = $script:localizedData.GetRegistryStatusRegistryManualManipulation_ErrorMessage -f $Path + $errorMessage = $script:localizedData.RegistryManualManipulationError -f $Path New-InvalidDataException ` -ErrorId 'RegistryManualManipulationError' ` -Message $errorMessage } - # Return the first value as the registry value "should" be the same + # Return the first value as the registry value is the same $registryValue = $registryValue[0] } - # Remove the default - if ($Status.ContainsKey('Default')) - { - $Status.Remove('Default') - } - - # Search the possible two values $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key } else { - Write-Verbose -Message ($script:localizedData.GetRegistryStatus_DefaultMessage -f $Status.Default, $Path) $Status.Default } - Write-Verbose -Message ($script:localizedData.GetRegistryStatus_FoundMessage -f $Key, $Path) return $Key } diff --git a/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 index e259da4e..b3a3678e 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Set-RegistryStatus.ps1 @@ -1,4 +1,25 @@ -# TODO: Add description +<# + .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 ( @@ -24,9 +45,9 @@ function Set-RegistryStatus $Name | ForEach-Object { $Params = @{ - Path = $Path + Path = $Path Value = $Value - Name = $_ + Name = $_ Force = $true } From 23b6a9ad51b609e8ceeb84e21e9bc15c189bfac4 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 13:08:55 +0100 Subject: [PATCH 07/21] Forbidden pattern --- .../source/Enum/001.SettingStatus.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 index f920339f..a5a2e182 100644 --- a/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Enum/001.SettingStatus.ps1 @@ -3,7 +3,7 @@ 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. + 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 { From c7d7d0e477b5612291b0cb60375d6ec791733f23 Mon Sep 17 00:00:00 2001 From: "G.Reijn" Date: Sun, 16 Feb 2025 13:09:28 +0100 Subject: [PATCH 08/21] Forbidden pattern --- .../source/en-US/about_Microsoft.Windows.Settings.help.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index f6c24a27..7df225e6 100644 --- 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 @@ -14,7 +14,7 @@ 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 Github repository for issues and new releases. + Look out on the repository for issues and new releases. SEE ALSO - {{ Please add Project URI such as github }}} From 8f48a6148a8d19381b72078bfcf919fc5fa86a38 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 09:22:53 +0200 Subject: [PATCH 09/21] Fix test and counter --- .../source/Classes/010.SettingsBase.ps1 | 1 + .../source/DSCResources/RegistryKeyData.psd1 | 7 ++++--- .../source/Private/Get-RegistryStatus.ps1 | 21 +++++++++++++++++-- .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 4 +++- .../Unit/Private/Get-RegistryStatus.tests.ps1 | 4 ++-- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 index 8ee3a006..a73ccc49 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -59,6 +59,7 @@ class SettingsBase : ResourceBase $property = $_ # Get the key data for the property + $key = $keyData | Where-Object -Property $PropertyName -EQ $property $key = $keyData | Where-Object { $_.PropertyName -eq $property } # Remove the PropertyName key from the key data diff --git a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 index f739d94f..45acb8b8 100644 --- a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -6,9 +6,10 @@ #> @{ "FindMyDevice" = @{ - Name = 'LocationSyncEnabled' - Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' - Status = @{ + PropertyName = 'FindMyDevice' + Name = 'LocationSyncEnabled' + Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues\' + Status = @{ Enabled = 1 Disabled = 0 Default = 0 diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 index 1b02a1bf..d49a2c4d 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -75,11 +75,28 @@ function Get-RegistryStatus $registryValue = $registryValue[0] } - $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key + # Check if the registry value is in the hashtable and return the corresponding key + $KeyCounter = $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key + + if ($KeyCounter.Count -gt 1) + { + $KeyCounter | Where-Object { $_ -ne 'Default' } + } + else + { + $KeyCounter + } } else { - $Status.Default + # Grab the default value + $DefaultValue = $Status.Default + + # Remove the status block + $Status.Remove('Default') + + # Search the hashtable for the default value and return disable or enable + $Key = $Status.GetEnumerator() | Where-Object -Property Value -EQ $registryValue | Select-Object -ExpandProperty Key } return $Key diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 index 50751ea0..39d67e43 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -283,7 +283,9 @@ Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { Set-StrictMode -Version 1.0 $currentState = $script:mockInstance.GetCurrentState( - @{} + @{ + IsSingleInstance = 'Yes' + } ) $currentState.IsSingleInstance | Should -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 index b4d4e984..c0a79610 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Get-RegistryStatus.tests.ps1 @@ -59,7 +59,7 @@ Describe 'Get-RegistryStatus' -Tag 'Private' { Status = @{ 'Enabled' = '1' 'Disabled' = '0' - 'Default' = 'Unknown' + 'Default' = '0' } ExpectedValue = '1' ExpectedKey = 'Enabled' @@ -92,7 +92,7 @@ Describe 'Get-RegistryStatus' -Tag 'Private' { Status = @{ 'Enabled' = '1' 'Disabled' = '0' - 'Default' = 'Unknown' + 'Default' = '0' } ExpectedValue = '1' ExpectedKey = 'Enabled' From 2d3d75a420102b3906edd5d08e4f44635f195ce4 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 11:24:38 +0200 Subject: [PATCH 10/21] Rewrite Get-RegistryStatus --- .../RequiredModules.psd1 | 1 + .../source/Classes/010.SettingsBase.ps1 | 16 ++---- .../source/Private/Get-RegistryStatus.ps1 | 51 +++++++------------ .../Classes/FindMyDevice.tests.ps1 | 47 +++++++++++++++++ .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 2 +- .../Unit/Private/Set-RegistryStatus.tests.ps1 | 14 ++--- 6 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 diff --git a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 index baf6d306..9c2b0fb4 100644 --- a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 +++ b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 @@ -26,5 +26,6 @@ 'DscResource.AnalyzerRules' = 'latest' xDscResourceDesigner = 'latest' 'DscResource.DocGenerator' = 'latest' + PSDesiredStateConfiguration = 'latest' } diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 index a73ccc49..30a7182d 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -1,18 +1,13 @@ <# .SYNOPSIS - The `FindMyDevice` DSC resource is used to manage the Find My Device setting. + The `SettingsBase` class is the base class for all settings resources. .DESCRIPTION - This resource is used to enable or disable the Find My Device setting on a Windows device. + 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'. - - .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 SettingsBase : ResourceBase @@ -59,9 +54,8 @@ class SettingsBase : ResourceBase $property = $_ # Get the key data for the property - $key = $keyData | Where-Object -Property $PropertyName -EQ $property - $key = $keyData | Where-Object { $_.PropertyName -eq $property } - + $key = $keyData | Where-Object -Property PropertyName -EQ $property + # Remove the PropertyName key from the key data $key.Remove('PropertyName') diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 index d49a2c4d..3aecef1a 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -15,7 +15,7 @@ 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 } + PS C:\> Get-RegistryStatus -Path 'HKLM:\1\2\3' -Name 'Setting1', 'Setting2' -Status @{ 'Enabled' = 1; 'Disabled' = 0; 'Default' = 0 } #> function Get-RegistryStatus { @@ -37,17 +37,17 @@ function Get-RegistryStatus $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 = if ($Name.Count -gt 1) - { - $Name | ForEach-Object { - Get-ItemPropertyValue -Path $Path -Name $_ -ErrorAction Stop - } - } - else + $registryValue = foreach ($NameItem in $Name) { - Get-ItemPropertyValue -Path $Path -Name $Name -ErrorAction Stop + Get-ItemPropertyValue -Path $Path -Name $NameItem -ErrorAction Stop } } catch @@ -56,8 +56,14 @@ function Get-RegistryStatus Write-Verbose -Message $_.Exception.Message -Verbose } - $Key = if ($registryValue -or $registryValue -eq 0) + 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 @@ -71,32 +77,11 @@ function Get-RegistryStatus -Message $errorMessage } - # Return the first value as the registry value is the same + # Return the first value as the registry value should be the same $registryValue = $registryValue[0] } - # Check if the registry value is in the hashtable and return the corresponding key - $KeyCounter = $Status.GetEnumerator() | Where-Object { $_.Value -eq $registryValue } | Select-Object -ExpandProperty Key - - if ($KeyCounter.Count -gt 1) - { - $KeyCounter | Where-Object { $_ -ne 'Default' } - } - else - { - $KeyCounter - } - } - else - { - # Grab the default value - $DefaultValue = $Status.Default - - # Remove the status block - $Status.Remove('Default') - - # Search the hashtable for the default value and return disable or enable - $Key = $Status.GetEnumerator() | Where-Object -Property Value -EQ $registryValue | Select-Object -ExpandProperty Key + $key = $Status.GetEnumerator() | Where-Object -Property Value -EQ $registryValue | Select-Object -ExpandProperty Key } return $Key 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..cb6b260e --- /dev/null +++ b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 @@ -0,0 +1,47 @@ +<# + .SYNOPSIS + Unit test for FindMyDevice DSC class resource. +#> + +param () + +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' + + 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 +} \ 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 index 39d67e43..83029177 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -298,7 +298,7 @@ Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { Describe 'FindMyDevice\Set()' -Tag 'HiddenMember' { BeforeAll { Mock -CommandName Set-RegistryStatus - Mock -CommandName Set-ItemProperty -MockWith { return $true } + Mock -CommandName New-ItemProperty -MockWith { return $true } } Context 'When setting the registry key for "Find My Device"' { 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 index 635034e6..3739f54c 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Private/Set-RegistryStatus.tests.ps1 @@ -53,11 +53,11 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { BeforeDiscovery { $testCases = @( @{ - Path = 'HKLM:\SOFTWARE\Microsoft\MdmCommon\SettingValues' - Name = 'LocationSyncEnabled' - Value = '1' + Path = 'HKCU:\Environment' + Name = 'Path' + Value = 'C:\Windows\System32' ExpectedKey = @{ - 'LocationSyncEnabled' = '1' + 'Path' = '1' } } ) @@ -66,7 +66,7 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { It 'Should set the registry key' -ForEach $testCases { InModuleScope -Parameters $_ -ScriptBlock { $result = Set-RegistryStatus -Path $Path -Name $Name -Value $Value - $result | Should -Be $ExpectedKey.LocationSyncEnabled + $result | Should -Be $ExpectedKey.Path } Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 @@ -76,7 +76,7 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { Context 'Set registry key when it does not exist' { BeforeAll { Mock -CommandName New-Item - Mock -CommandName Set-ItemProperty -MockWith { + Mock -CommandName New-ItemProperty -MockWith { return 0 } } @@ -99,7 +99,7 @@ Describe 'Set-RegistryStatus' -Tag 'Private' { } Should -Invoke -CommandName New-Item -Exactly -Times 1 - # Should -Invoke -CommandName Set-ItemProperty -Exactly -Times 1 + Should -Invoke -CommandName New-ItemProperty -Exactly -Times 1 } } } From f53e764863f3ef89dd06dc204d3395041cbb114c Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 14:33:41 +0200 Subject: [PATCH 11/21] Adding first integration test --- .../source/Classes/010.SettingsBase.ps1 | 4 +- .../source/Classes/020.FindMyDevice.ps1 | 5 +- .../source/Classes/020.General.ps1 | 5 +- .../source/Microsoft.Windows.Settings.psd1 | 8 +-- .../Classes/FindMyDevice.tests.ps1 | 63 +++++++++++++++---- .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 4 +- 6 files changed, 64 insertions(+), 25 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 index 30a7182d..81ccf2ea 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -23,7 +23,7 @@ class SettingsBase : ResourceBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'IsSingleInstance' + 'IsSingleInstance', 'SID' ) $this.ClassName = $this.GetType().Name @@ -41,7 +41,7 @@ class SettingsBase : ResourceBase # Use the Get-DscProperty cmdlet to get the properties to search instead of properties from GetCurrentState $propsToSearch = Get-DscProperty -InputObject $this ` -Attribute Optional ` - -ExcludeName 'IsSingleInstance' + -ExcludeName @('SID', 'IsSingleInstance') # Initialize the state $state = @{} diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 index 57e62c24..7a86dbf0 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -20,7 +20,7 @@ class FindMyDevice : SettingsBase [DscProperty(Key)] [ValidateSet('Yes')] [System.String] - $IsSingleInstance = 'Yes' + $SID = 'Yes' [DscProperty()] [SettingStatus] $FindMyDevice @@ -33,7 +33,8 @@ class FindMyDevice : SettingsBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'IsSingleInstance' + 'SID' + 'IsSIngleInstance' ) # Opt in to the optional enums feature diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 9b53b63a..287834f9 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -36,7 +36,7 @@ class General : SettingsBase [DscProperty(Key)] [ValidateSet('Yes')] [System.String] - $IsSingleInstance = 'Yes' + $SID = 'Yes' [DscProperty()] [SettingStatus] $EnablePersonalizedAds @@ -61,7 +61,8 @@ class General : SettingsBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'IsSingleInstance' + 'IsSingleInstance', + 'SID' ) # Opt in to the optional enums feature diff --git a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 index d466eb5f..ae8a5a1a 100644 --- a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -69,16 +69,16 @@ # 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 = @() + # 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 = @() + # CmdletsToExport = @() # Variables to export from this module - VariablesToExport = @() + # 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 = @() + # AliasesToExport = @() # DSC resources to export from this module DscResourcesToExport = @() diff --git a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 index cb6b260e..48803bad 100644 --- a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 @@ -1,10 +1,10 @@ -<# +#Requires -RunAsAdministrator + +<# .SYNOPSIS - Unit test for FindMyDevice DSC class resource. + Integration test for FindMyDevice DSC class resource. #> -param () - BeforeDiscovery { try { @@ -29,19 +29,56 @@ BeforeDiscovery { BeforeAll { $script:dscModuleName = 'Microsoft.Windows.Settings' + $script:dscResourceName = 'FindMyDevice' + + Import-Module -Name $script:dscModuleName -Force - Import-Module -Name $script:dscModuleName + $script:instance = & (Import-Module $script:dscModuleName -PassThru) ([scriptblock]::Create("'$script:dscResourceName' -as 'type'")) + + $PSDefaultParameterValues['Invoke-DscResource:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Invoke-DscResource:Name'] = $script:instance.Name - $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') + $PSDefaultParameterValues.Remove('Invoke-DscResource:ModuleName') + $PSDefaultParameterValues.Remove('Invoke-DscResource:Name') +} + +Describe "$($script:dscModuleName)_Integration" { + 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 + } + } +} - # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +Describe "$($script:dscResourceName)\IntegrationTest" { + BeforeAll { + $class = $script:instance::new() + + $script:currentState = $class.Get() + } + + Context "$($script:dscResourceName)\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 $desiredState.FindMyDevice + } + } + + AfterAll { + Write-Verbose -Message "Restoring the original state of the $($script:dscResourceName) setting." + $script:currentState.Set() + } } \ 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 index 83029177..f7cb1dc8 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -284,11 +284,11 @@ Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { $currentState = $script:mockInstance.GetCurrentState( @{ - IsSingleInstance = 'Yes' + SID = 'Yes' } ) - $currentState.IsSingleInstance | Should -BeNullOrEmpty + $currentState.SID | Should -BeNullOrEmpty $currentState.FindMyDevice | Should -Be 'Disabled' } } From d26e11d2217b8e27c661e9a49dd79965a7f22854 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 14:53:31 +0200 Subject: [PATCH 12/21] Solve todo task --- .../source/Classes/020.FindMyDevice.ps1 | 10 ++++++---- .../source/Classes/020.General.ps1 | 6 ++++-- .../source/Private/Get-RegistryKeyData.ps1 | 2 +- .../source/Private/Get-RegistryStatus.ps1 | 3 +-- .../en-US/Microsoft.Windows.Settings.strings.psd1 | 1 - 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 index 7a86dbf0..6e10e789 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -5,8 +5,10 @@ .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 SID + The SID of the setting. This is a unique identifier for the setting. The value should be 'IsSingleInstance'. + + NOTE: This property is not configurable and is used internally by the DSC resource. Using the 'IsSingleInstance' value from the base does not work as the class feature is already defined. .PARAMETER FindMyDevice Specifies whether the Find My Device setting should be enabled or disabled. @@ -18,9 +20,9 @@ class FindMyDevice : SettingsBase { [DscProperty(Key)] - [ValidateSet('Yes')] + [ValidateSet('IsSingleInstance')] [System.String] - $SID = 'Yes' + $SID = 'IsSingleInstance' [DscProperty()] [SettingStatus] $FindMyDevice diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 287834f9..27996d0b 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -6,8 +6,10 @@ .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 SID + The SID of the setting. This is a unique identifier for the setting. The value should be 'IsSingleInstance'. + + NOTE: This property is not configurable and is used internally by the DSC resource. Using the 'IsSingleInstance' value from the base does not work as the class feature is already defined. .PARAMETER FindMyDevice Specifies whether the Find My Device setting should be enabled or disabled. diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 index 57d60f9e..055e7a53 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryKeyData.ps1 @@ -31,7 +31,7 @@ function Get-RegistryKeyData ) $registryDataPath = Join-Path -Path $PSScriptRoot -ChildPath 'DSCResources\RegistryKeyData.psd1' - Write-Verbose -Message ($script:localizedData.FindMyDeviceRegistryKey -f $Key, $registryDataPath) + Write-Verbose -Message ($script:localizedData.GetRegistryKeyData_SearchMessage -f $Key, $registryDataPath) $import = Import-PowerShellDataFile -Path $registryDataPath $entry = $import.$Key diff --git a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 index 3aecef1a..16601d02 100644 --- a/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Private/Get-RegistryStatus.ps1 @@ -52,8 +52,7 @@ function Get-RegistryStatus } catch { - # TODO: Localize the verbose message, not error - Write-Verbose -Message $_.Exception.Message -Verbose + Write-Verbose -Message ($script:localizedData.GetRegistryStatus_DefaultMessage -f $Status.Default, $Name) } if (-not ([string]::IsNullOrEmpty($registryValue))) 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 index 617b9ee0..bfa354ca 100644 --- 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 @@ -6,7 +6,6 @@ #> 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}'." From 8ed46dd3d35842c9dc4e143fe3afc71e6d2af050 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 14:54:34 +0200 Subject: [PATCH 13/21] Solve spelling --- .github/actions/spelling/allow.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index ebf4af21..c0cf9e16 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -83,4 +83,9 @@ vso winrm MMdd NOLOGO - +whatif +wildcards +Windo +winget +workflows +worktree From a9c406c7e279adccf0c09f6f421205503a126045 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Tue, 8 Apr 2025 16:22:10 +0200 Subject: [PATCH 14/21] Integration test for General --- .../source/Classes/020.General.ps1 | 4 +- .../Classes/FindMyDevice.tests.ps1 | 28 +- .../Integration/Classes/General.tests.ps1 | 79 ++++++ .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 10 +- .../tests/Unit/Classes/General.tests.ps1 | 248 +++++++++++++++++- 5 files changed, 341 insertions(+), 28 deletions(-) create mode 100644 resources/Microsoft.Windows.Settings/tests/Integration/Classes/General.tests.ps1 diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 27996d0b..8d780eb4 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -36,9 +36,9 @@ class General : SettingsBase { [DscProperty(Key)] - [ValidateSet('Yes')] + [ValidateSet('IsSingleInstance')] [System.String] - $SID = 'Yes' + $SID = 'IsSingleInstance' [DscProperty()] [SettingStatus] $EnablePersonalizedAds diff --git a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 index 48803bad..36713c07 100644 --- a/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Integration/Classes/FindMyDevice.tests.ps1 @@ -34,23 +34,24 @@ BeforeAll { 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 "$($script:dscModuleName)_Integration" { +Describe "FindMyDevice\Generic" { Context "List available DSC resources" { It 'Shows DSC resources' { - $expectedDscResources = @( - 'FindMyDevice' - ) + $expectedDscResources = @('FindMyDevice') $availableDscResources = (Get-DscResource -Module $script:dscModuleName -Name $expectedDscResources).Name $availableDscResources.count | Should -Be 1 @@ -59,26 +60,15 @@ Describe "$($script:dscModuleName)_Integration" { } } -Describe "$($script:dscResourceName)\IntegrationTest" { - BeforeAll { - $class = $script:instance::new() - - $script:currentState = $class.Get() - } - - Context "$($script:dscResourceName)\Set" { +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 $desiredState.FindMyDevice + $finalState.FindMyDevice | Should -Be 'Enabled' } } - - AfterAll { - Write-Verbose -Message "Restoring the original state of the $($script:dscResourceName) setting." - $script:currentState.Set() - } } \ 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 index f7cb1dc8..aaaf5d7d 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -77,14 +77,13 @@ Describe 'FindMyDevice' { } Describe 'FindMyDevice\Get()' -Tag 'Get' { - Context 'When the system is in the desired state' { Context 'When getting the status of find my device' { BeforeAll { InModuleScope -Scriptblock { Set-StrictMode -Version 1.0 $script:mockInstance = [FindMyDevice]@{ - IsSingleInstance = 'Yes' + SID = 'IsSingleInstance' } <# @@ -97,8 +96,8 @@ Describe 'FindMyDevice\Get()' -Tag 'Get' { $script:mockInstance | Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return @{ - IsSingleInstance = 'Yes' - FindMyDevice = [SettingStatus]::Enabled + SID = 'IsSingleInstance' + FindMyDevice = [SettingStatus]::Enabled } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -113,14 +112,13 @@ Describe 'FindMyDevice\Get()' -Tag 'Get' { $currentState = $script:mockInstance.Get() - $currentState.IsSingleInstance | Should -Be 'Yes' + $currentState.SID | Should -Be 'IsSingleInstance' $currentState.FindMyDevice | Should -Be 'Enabled' $currentState.Reasons | Should -BeNullOrEmpty } } } - } } Describe 'FindMyDevice\Set()' -Tag 'Set' { diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 index c1313c2f..629cfca6 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 @@ -74,4 +74,250 @@ Describe 'General' { } } } -} \ No newline at end of file +} + +Describe 'General\Get()' -Tag 'Get' { + Context 'When getting the status of general settings' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 + + $script:mockInstance = [General]@{ + SID = 'IsSingleInstance' + } + + <# + 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 @{ + SID = 'IsSingleInstance' + EnablePersonalizedAds = [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.SID | Should -Be 'IsSingleInstance' + $currentState.EnablePersonalizedAds | Should -Be 'Enabled' + + $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 = 'Enabled' + } + } + } + + } + + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $currentState = $script:mockInstance.GetCurrentState( + @{ + SID = 'Yes' + } + ) + + $currentState.SID | Should -BeNullOrEmpty + $currentState.EnablePersonalizedAds | Should -Be 'Enabled' + } + } +} + + +Describe 'General\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 = [General]@{ + EnablePersonalizedAds = 'Disabled' + } + } + } + + 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 + } + } +} From dd1d1af0f6a77d28e56ce05b0fd67ee537cb13d8 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 9 Apr 2025 07:36:06 +0200 Subject: [PATCH 15/21] Revert base class --- .../source/Classes/010.SettingsBase.ps1 | 4 +- .../source/Classes/020.FindMyDevice.ps1 | 13 ++--- .../source/Classes/020.General.ps1 | 15 ++--- .../source/DSCResources/RegistryKeyData.psd1 | 10 ++-- .../source/Microsoft.Windows.Settings.psd1 | 6 +- .../source/en-US/FindMyDevice.strings.psd1 | 1 - .../tests/Unit/Classes/FindMyDevice.tests.ps1 | 58 +++++++++---------- .../tests/Unit/Classes/General.tests.ps1 | 22 +++---- 8 files changed, 60 insertions(+), 69 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 index 81ccf2ea..a5f88181 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/010.SettingsBase.ps1 @@ -9,10 +9,8 @@ .PARAMETER IsSingleInstance Specifies the resource is a single instance, the value must be 'Yes'. #> -[DscResource()] class SettingsBase : ResourceBase { - [DscProperty(Key)] [ValidateSet('Yes')] [System.String] $IsSingleInstance = 'Yes' @@ -23,7 +21,7 @@ class SettingsBase : ResourceBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'IsSingleInstance', 'SID' + 'IsSingleInstance' ) $this.ClassName = $this.GetType().Name diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 index 6e10e789..70873075 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.FindMyDevice.ps1 @@ -5,10 +5,8 @@ .DESCRIPTION This resource is used to enable or disable the Find My Device setting on a Windows device. - .PARAMETER SID - The SID of the setting. This is a unique identifier for the setting. The value should be 'IsSingleInstance'. - - NOTE: This property is not configurable and is used internally by the DSC resource. Using the 'IsSingleInstance' value from the base does not work as the class feature is already defined. + .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. @@ -20,9 +18,9 @@ class FindMyDevice : SettingsBase { [DscProperty(Key)] - [ValidateSet('IsSingleInstance')] + [ValidateSet('Yes')] [System.String] - $SID = 'IsSingleInstance' + $IsSingleInstance = 'Yes' [DscProperty()] [SettingStatus] $FindMyDevice @@ -35,8 +33,7 @@ class FindMyDevice : SettingsBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'SID' - 'IsSIngleInstance' + 'IsSingleInstance' ) # Opt in to the optional enums feature diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 8d780eb4..634e0f0c 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -6,11 +6,9 @@ .DESCRIPTION This resource is used to enable or disable the General settings under Privacy & security on a Windows device. - .PARAMETER SID - The SID of the setting. This is a unique identifier for the setting. The value should be 'IsSingleInstance'. - - NOTE: This property is not configurable and is used internally by the DSC resource. Using the 'IsSingleInstance' value from the base does not work as the class feature is already defined. - + .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. @@ -36,9 +34,9 @@ class General : SettingsBase { [DscProperty(Key)] - [ValidateSet('IsSingleInstance')] + [ValidateSet('Yes')] [System.String] - $SID = 'IsSingleInstance' + $IsSingleInstance = 'Yes' [DscProperty()] [SettingStatus] $EnablePersonalizedAds @@ -63,8 +61,7 @@ class General : SettingsBase { # These properties will not be enforced. $this.ExcludeDscProperties = @( - 'IsSingleInstance', - 'SID' + 'IsSingleInstance' ) # Opt in to the optional enums feature diff --git a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 index 45acb8b8..3964660e 100644 --- a/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 +++ b/resources/Microsoft.Windows.Settings/source/DSCResources/RegistryKeyData.psd1 @@ -23,7 +23,7 @@ Status = @{ Enabled = 1 Disabled = 0 - Default = 'Disabled' + Default = 0 } }, @{ @@ -33,7 +33,7 @@ Status = @{ Enabled = 0 Disabled = 1 - Default = 'Enabled' + Default = 0 } }, @{ @@ -43,7 +43,7 @@ Status = @{ Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 1 } } @{ @@ -53,7 +53,7 @@ Status = @{ Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 1 } } @{ @@ -63,7 +63,7 @@ Status = @{ Enabled = 1 Disabled = 0 - Default = 'Enabled' + Default = 1 } } ) diff --git a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 index ae8a5a1a..8126ff13 100644 --- a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -69,13 +69,13 @@ # 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 = @() + 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 = @() + CmdletsToExport = @() # Variables to export from this module - # VariablesToExport = @() + 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 = @() diff --git a/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 b/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 index cdff2091..da9ab4d2 100644 --- a/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 +++ b/resources/Microsoft.Windows.Settings/source/en-US/FindMyDevice.strings.psd1 @@ -11,5 +11,4 @@ ConvertFrom-StringData @' # None ## Strings directly used by the derived class FindMyDevice. - FindMyDeviceRegistryKey = Searching '{0}' key in '{1}' (0001). '@ diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 index aaaf5d7d..d29b0fad 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/FindMyDevice.tests.ps1 @@ -77,48 +77,48 @@ Describe 'FindMyDevice' { } Describe 'FindMyDevice\Get()' -Tag 'Get' { - Context 'When getting the status of find my device' { - BeforeAll { - InModuleScope -Scriptblock { - Set-StrictMode -Version 1.0 + Context 'When getting the status of find my device' { + BeforeAll { + InModuleScope -Scriptblock { + Set-StrictMode -Version 1.0 - $script:mockInstance = [FindMyDevice]@{ - SID = 'IsSingleInstance' - } + $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 @{ - SID = 'IsSingleInstance' - FindMyDevice = [SettingStatus]::Enabled - } - } -PassThru | - Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { - return - } - } + $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 + It 'Should return the correct values' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 - $currentState = $script:mockInstance.Get() + $currentState = $script:mockInstance.Get() - $currentState.SID | Should -Be 'IsSingleInstance' - $currentState.FindMyDevice | Should -Be 'Enabled' + $currentState.IsSingleInstance | Should -Be 'Yes' + $currentState.FindMyDevice | Should -Be 'Enabled' - $currentState.Reasons | Should -BeNullOrEmpty - } + $currentState.Reasons | Should -BeNullOrEmpty } } + } } Describe 'FindMyDevice\Set()' -Tag 'Set' { @@ -282,11 +282,11 @@ Describe 'FindMyDevice\GetCurrentState()' -Tag 'HiddenMember' { $currentState = $script:mockInstance.GetCurrentState( @{ - SID = 'Yes' + IsSingleInstance = 'Yes' } ) - $currentState.SID | Should -BeNullOrEmpty + $currentState.IsSingleInstance | Should -BeNullOrEmpty $currentState.FindMyDevice | Should -Be 'Disabled' } } diff --git a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 index 629cfca6..936f64cd 100644 --- a/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 +++ b/resources/Microsoft.Windows.Settings/tests/Unit/Classes/General.tests.ps1 @@ -83,7 +83,7 @@ Describe 'General\Get()' -Tag 'Get' { Set-StrictMode -Version 1.0 $script:mockInstance = [General]@{ - SID = 'IsSingleInstance' + IsSingleInstance = 'Yes' } <# @@ -96,8 +96,8 @@ Describe 'General\Get()' -Tag 'Get' { $script:mockInstance | Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return @{ - SID = 'IsSingleInstance' - EnablePersonalizedAds = [SettingStatus]::Enabled + IsSingleInstance = 'Yes' + EnablePersonalizedAds = [SettingStatus]::Disabled } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -112,8 +112,8 @@ Describe 'General\Get()' -Tag 'Get' { $currentState = $script:mockInstance.Get() - $currentState.SID | Should -Be 'IsSingleInstance' - $currentState.EnablePersonalizedAds | Should -Be 'Enabled' + $currentState.IsSingleInstance | Should -Be 'Yes' + $currentState.EnablePersonalizedAds | Should -Be 'Disabled' $currentState.Reasons | Should -BeNullOrEmpty } @@ -269,7 +269,7 @@ Describe 'General\GetCurrentState()' -Tag 'HiddenMember' { Set-StrictMode -Version 1.0 $script:mockInstance = [General] @{ - EnablePersonalizedAds = 'Enabled' + EnablePersonalizedAds = 'Disabled' } } } @@ -282,12 +282,12 @@ Describe 'General\GetCurrentState()' -Tag 'HiddenMember' { $currentState = $script:mockInstance.GetCurrentState( @{ - SID = 'Yes' + IsSingleInstance = 'Yes' } ) - $currentState.SID | Should -BeNullOrEmpty - $currentState.EnablePersonalizedAds | Should -Be 'Enabled' + $currentState.IsSingleInstance | Should -BeNullOrEmpty + $currentState.EnablePersonalizedAds | Should -Be 'Disabled' } } } @@ -299,13 +299,13 @@ Describe 'General\Set()' -Tag 'HiddenMember' { Mock -CommandName New-ItemProperty -MockWith { return $true } } - Context 'When setting the registry key for "Find My Device"' { + Context 'When setting the registry key for "EnablePersonalizedAds"' { BeforeAll { InModuleScope -Scriptblock { Set-StrictMode -Version 1.0 $script:mockInstance = [General]@{ - EnablePersonalizedAds = 'Disabled' + EnablePersonalizedAds = 'Enabled' } } } From fc2bc6a56152e97de5a87fb1a628113c2012a1a6 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 16 Apr 2025 08:22:48 +0200 Subject: [PATCH 16/21] Solve remark --- .../Microsoft.Windows.Settings/CHANGELOG.md | 24 +++---------------- .../source/Microsoft.Windows.Settings.psd1 | 14 +---------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/CHANGELOG.md b/resources/Microsoft.Windows.Settings/CHANGELOG.md index 74ee4ea4..ebe373fe 100644 --- a/resources/Microsoft.Windows.Settings/CHANGELOG.md +++ b/resources/Microsoft.Windows.Settings/CHANGELOG.md @@ -7,24 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- For new features. - -### Changed - -- For changes in existing functionality. - -### Deprecated - -- For soon-to-be removed features. - -### Removed - -- For now removed features. - -### Fixed - -- For any bug fix. - -### Security - -- In case of vulnerabilities. +- Initial release of module with the following settings: + - General + - FindMyDevice diff --git a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 index 8126ff13..f81cee61 100644 --- a/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 +++ b/resources/Microsoft.Windows.Settings/source/Microsoft.Windows.Settings.psd1 @@ -128,16 +128,4 @@ # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. # DefaultCommandPrefix = '' -} - - - - - - - - - - - - +} \ No newline at end of file From 207c345582c1c78673b04ccc3a9d02724db3b5f4 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Wed, 16 Apr 2025 08:23:40 +0200 Subject: [PATCH 17/21] Remove example --- resources/Microsoft.Windows.Settings/README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/README.md b/resources/Microsoft.Windows.Settings/README.md index 6c253239..e920da20 100644 --- a/resources/Microsoft.Windows.Settings/README.md +++ b/resources/Microsoft.Windows.Settings/README.md @@ -13,15 +13,4 @@ The DSC resource module `Microsoft.Windows.Settings` provides a set of Desired S To use these resources, import the module and invoke the desired DSC resource with the appropriate parameters. -```powershell -Import-Module -Name Microsoft.Windows.Settings - -Invoke-DscResource -ModuleName Microsoft.Windows.Settings -Name AdvancedAppSettings -Method Set -Property @{ - SID = 'S-1-5-21-1234567890-123456789-1234567890-1001'; - AppSourcePreference = 'PreferStore'; - ShareDeviceExperience = 'Everyone'; - ArchiveApp = $true; -} -``` - For more detailed examples and parameter descriptions, refer to the individual resource documentation. From b8e617d6313e68851c2acb8bf635b9509e80b34b Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 17 Apr 2025 10:55:08 +0200 Subject: [PATCH 18/21] Remove parameter from General example --- .../Microsoft.Windows.Settings/source/Classes/020.General.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 634e0f0c..7d3e6d1d 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -8,9 +8,6 @@ .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 EnablePersonalizedAds Specifies whether personalized ads should be enabled or disabled. From 8f549484afd868b6d4cebf83d207b362cf33d9d7 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 17 Apr 2025 12:56:15 +0200 Subject: [PATCH 19/21] Update example --- .../Microsoft.Windows.Settings/build.yaml | 47 +++++++++---------- .../source/Classes/020.General.ps1 | 5 ++ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/build.yaml b/resources/Microsoft.Windows.Settings/build.yaml index aba032ac..99c1c937 100644 --- a/resources/Microsoft.Windows.Settings/build.yaml +++ b/resources/Microsoft.Windows.Settings/build.yaml @@ -23,22 +23,22 @@ VersionedOutputDirectory: true #################################################### 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 + 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 @@ -70,7 +70,7 @@ BuildWorkflow: # 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 + #- Merge_CodeCoverage_Files publish: - Publish_Release_To_GitHub # Runs first, if token is expired it will fail early @@ -103,7 +103,6 @@ Pester: - Modules/DscResource.Common - Modules/DscResource.Base - DscTest: Pester: Configuration: @@ -134,11 +133,11 @@ DscTest: # module in the file RequiredModules.psd1. ModuleBuildTasks: Sampler: - - '*.build.Sampler.ib.tasks' + - "*.build.Sampler.ib.tasks" Sampler.GitHubTasks: - - '*.ib.tasks' + - "*.ib.tasks" DscResource.DocGenerator: - - 'Task.*' + - "Task.*" # Invoke-Build Header to be used to 'decorate' the terminal output of the tasks. TaskHeader: | @@ -154,7 +153,7 @@ TaskHeader: | GitHubConfig: GitHubFilesToAdd: - - 'CHANGELOG.md' + - "CHANGELOG.md" GitHubConfigUserName: dscbot GitHubConfigUserEmail: dsccommunity@outlook.com UpdateChangelogOnPrerelease: false @@ -168,7 +167,7 @@ DscResource.DocGenerator: - '\`(.+?)\`' # Match inline code-block - '\\(\\)' # Match escaped backslash - '\[[^\[]+\]\((.+?)\)' # Match markdown URL - - '_(.+?)_' # Match Italic (underscore) + - "_(.+?)_" # Match Italic (underscore) - '\*\*(.+?)\*\*' # Match bold - '\*(.+?)\*' # Match Italic (asterisk) @@ -176,9 +175,9 @@ DscResource.DocGenerator: # 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 +#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/020.General.ps1 b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 index 7d3e6d1d..fa4e83cc 100644 --- a/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 +++ b/resources/Microsoft.Windows.Settings/source/Classes/020.General.ps1 @@ -26,6 +26,11 @@ .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 From 2075bb860e5c2ab0aac83fc7bc3f588542681bb7 Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 17 Apr 2025 12:57:41 +0200 Subject: [PATCH 20/21] Add keywords and update example --- .../source/en-US/about_Microsoft.Windows.Settings.help.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 7df225e6..8ab2cc30 100644 --- 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 @@ -8,7 +8,7 @@ LONG DESCRIPTION The DSC module for Windows Settings EXAMPLES - PS C:\> {{ add examples here }} + 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. @@ -17,8 +17,8 @@ TROUBLESHOOTING NOTE: Look out on the repository for issues and new releases. SEE ALSO - - {{ Please add Project URI such as github }}} + - https://github.com/microsoft/winget-dsc/ KEYWORDS - {{ Add comma separated keywords here }} + DSC, Windows, Settings, Class-based From 66d1d135306b6935dc56d53e24c7becdb2fe3eee Mon Sep 17 00:00:00 2001 From: Gijs Reijn Date: Thu, 24 Apr 2025 06:45:38 +0200 Subject: [PATCH 21/21] Pin versions --- .../RequiredModules.psd1 | 29 +++++++++---------- .../Resolve-Dependency.ps1 | 12 ++++---- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 index 9c2b0fb4..72133d05 100644 --- a/resources/Microsoft.Windows.Settings/RequiredModules.psd1 +++ b/resources/Microsoft.Windows.Settings/RequiredModules.psd1 @@ -12,20 +12,19 @@ # } #} - InvokeBuild = 'latest' - PSScriptAnalyzer = 'latest' - Pester = 'latest' - ModuleBuilder = 'latest' - ChangelogManagement = 'latest' - Sampler = 'latest' - 'Sampler.GitHubTasks' = 'latest' - MarkdownLinkCheck = 'latest' - 'DscResource.Base' = 'latest' - 'DscResource.Common' = 'latest' - 'DscResource.Test' = 'latest' - 'DscResource.AnalyzerRules' = 'latest' - xDscResourceDesigner = 'latest' - 'DscResource.DocGenerator' = 'latest' - PSDesiredStateConfiguration = 'latest' + 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 index 17cc98ec..9e0afae1 100644 --- a/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 +++ b/resources/Microsoft.Windows.Settings/Resolve-Dependency.ps1 @@ -233,7 +233,7 @@ if ($UseModuleFast -and -not (Get-Module -Name 'ModuleFast')) $moduleFastBootstrapScriptBlockParameters.UseMain = $true } - elseif($ModuleFastVersion) + elseif ($ModuleFastVersion) { if ($ModuleFastVersion -notmatch 'v') { @@ -409,8 +409,8 @@ if (-not ($UseModuleFast -or $UsePSResourceGet)) #> Get-Module -ListAvailable PackageManagement | Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | - Select-Object -First 1 | - Import-Module -Force + Select-Object -First 1 | + Import-Module -Force } Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' @@ -884,14 +884,16 @@ try $modulesToSave = @( @{ - Name = 'PSDepend' # Always include PSDepend for backward compatibility. + Name = 'PSDepend' # Always include PSDepend for backward compatibility. + Version = '0.3.8' } ) if ($WithYAML) { $modulesToSave += @{ - Name = 'PowerShell-Yaml' + Name = 'PowerShell-Yaml' + Version = '0.4.12' } }