diff --git a/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.psm1 b/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.psm1 new file mode 100644 index 0000000..3b60d03 --- /dev/null +++ b/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.psm1 @@ -0,0 +1,543 @@ +# Global needed to indicate if a restart is required +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonResourceHelper.psm1') + +# Localized messages for verbose and error messages in this resource +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_WindowsFeature' + +<# + .SYNOPSIS + Retrieves the status of the role or feature with the given name on the target machine. + + .PARAMETER Name + The name of the role or feature to retrieve + + .PARAMETER Credential + The credential (if required) to retrieve the role or feature. + Optional. + + .NOTES + If the specified role or feature does not contain any subfeatures then + IncludeAllSubFeature will be set to $false. If the specified feature contains one + or more subfeatures then IncludeAllSubFeature will be set to $true only if all the + subfeatures are installed. Otherwise, IncludeAllSubFeature will be set to $false. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message ($script:localizedData.GetTargetResourceStartMessage -f $Name) + + Import-ServerManager + + Write-Verbose -Message ($script:localizedData.QueryFeature -f $Name) + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } ` + -ComputerName . ` + -Credential $Credential ` + } + else + { + $feature = Get-WindowsFeature @PSBoundParameters + } + + Assert-SingleFeatureExists -Feature $feature -Name $Name + + $includeAllSubFeature = $true + + if ($feature.SubFeatures.Count -eq 0) + { + $includeAllSubFeature = $false + } + else + { + foreach ($currentSubFeatureName in $feature.SubFeatures) + { + + $getWindowsFeatureParameters = @{ + Name = $currentSubFeatureName + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getWindowsFeatureParameters['Credential'] = $Credential + } + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } ` + -ComputerName . ` + -Credential $Credential ` + } + else + { + $subFeature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleFeatureExists -Feature $subFeature -Name $currentSubFeatureName + + if (-not $subFeature.Installed) + { + $includeAllSubFeature = $false + break + } + } + } + + if ($feature.Installed) + { + $ensureResult = 'Present' + } + else + { + $ensureResult = 'Absent' + } + + Write-Verbose -Message ($script:localizedData.GetTargetResourceEndMessage -f $Name) + + # Add all feature properties to the hash table + return @{ + Name = $Name + DisplayName = $feature.DisplayName + Ensure = $ensureResult + IncludeAllSubFeature = $includeAllSubFeature + } +} + +<# + .SYNOPSIS + Installs or uninstalls the role or feature with the given name on the target machine + with the option of installing or uninstalling all subfeatures as well. + + .PARAMETER Name + The name of the role or feature to install or uninstall. + + .PARAMETER Ensure + Specifies whether the role or feature should be installed ('Present') + or uninstalled ('Absent'). + By default this is set to Present. + + .PARAMETER IncludeAllSubFeature + Specifies whether or not all subfeatures should be installed or uninstalled with + the specified role or feature. Default is false. + If this property is true and Ensure is set to Present, all subfeatures will be installed. + If this property is false and Ensure is set to Present, subfeatures will not be installed or uninstalled. + If Ensure is set to Absent, all subfeatures will be uninstalled. + + .PARAMETER Credential + The credential (if required) to install or uninstall the role or feature. + Optional. + + .PARAMETER LogPath + The custom path to the log file to log this operation. + If not passed in, the default log path will be used (%windir%\logs\ServerManager.log). +#> +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess = $true)] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Boolean] + $IncludeAllSubFeature = $false, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateNotNullOrEmpty()] + [String] + $LogPath + ) + + Write-Verbose -Message ($script:localizedData.SetTargetResourceStartMessage -f $Name) + + Import-ServerManager + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + + if ($Ensure -eq 'Present') + { + $addWindowsFeatureParameters = @{ + Name = $Name + IncludeAllSubFeature = $IncludeAllSubFeature + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $addWindowsFeatureParameters['LogPath'] = $LogPath + } + + Write-Verbose -Message ($script:localizedData.InstallFeature -f $Name) + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Add-WindowsFeature through Invoke-Command to start a new process with + the given credential since Add-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Add-WindowsFeature @addWindowsFeatureParameters } ` + -ComputerName . ` + -Credential $Credential + } + else + { + if ($PSBoundParameters.ContainsKey('Credential')) + { + $addWindowsFeatureParameters['Credential'] = $Credential + } + + $feature = Add-WindowsFeature @addWindowsFeatureParameters + } + + if ($null -ne $feature -and $feature.Success) + { + Write-Verbose -Message ($script:localizedData.InstallSuccess -f $Name) + + # Check if reboot is required, if so notify the Local Configuration Manager. + if ($feature.RestartNeeded -eq 'Yes') + { + Write-Verbose -Message $script:localizedData.RestartNeeded + $global:DSCMachineStatus = 1 + } + } + else + { + New-InvalidOperationException -Message ($script:localizedData.FeatureInstallationFailureError -f $Name) + } + } + # Ensure = 'Absent' + else + { + $removeWindowsFeatureParameters = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('LogPath')) + { + $removeWindowsFeatureParameters['LogPath'] = $LogPath + } + + Write-Verbose -Message ($script:localizedData.UninstallFeature -f $Name) + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Remove-WindowsFeature through Invoke-Command to start a new process with + the given credential since Remove-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Remove-WindowsFeature @removeWindowsFeatureParameters } ` + -ComputerName . ` + -Credential $Credential + } + else + { + if ($PSBoundParameters.ContainsKey('Credential')) + { + $addWindowsFeatureParameters['Credential'] = $Credential + } + + $feature = Remove-WindowsFeature @removeWindowsFeatureParameters + } + + if ($null -ne $feature -and $feature.Success) + { + Write-Verbose ($script:localizedData.UninstallSuccess -f $Name) + + # Check if reboot is required, if so notify the Local Configuration Manager. + if ($feature.RestartNeeded -eq 'Yes') + { + Write-Verbose -Message $script:localizedData.RestartNeeded + $global:DSCMachineStatus = 1 + } + } + else + { + New-InvalidOperationException -Message ($script:localizedData.FeatureUninstallationFailureError -f $Name) + } + } + + Write-Verbose -Message ($script:localizedData.SetTargetResourceEndMessage -f $Name) +} + +<# + .SYNOPSIS + Tests if the role or feature with the given name is in the desired state. + + .PARAMETER Name + The name of the role or feature to test the state of. + + .PARAMETER Ensure + Specifies whether the role or feature should be installed ('Present') + or uninstalled ('Absent'). + By default this is set to Present. + + .PARAMETER IncludeAllSubFeature + Specifies whether the subfeatures of the indicated role or feature should also be checked + to ensure they are in the desired state. If Ensure is set to 'Present' and this is set to + $true then each subfeature is checked to ensure it is installed as well. If Ensure is set to + Absent and this is set to $true, then each subfeature is checked to ensure it is uninstalled. + As of now, this test can't be used to check if a feature is Installed but all of its + subfeatures are uninstalled. + By default this is set to $false. + + .PARAMETER Credential + The Credential (if required) to test the status of the role or feature. + Optional. + + .PARAMETER LogPath + The path to the log file to log this operation. + Not used in Test-TargetResource. + +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', + + [Boolean] + $IncludeAllSubFeature = $false, + + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateNotNullOrEmpty()] + [String] + $LogPath + + ) + + Write-Verbose -Message ($script:localizedData.TestTargetResourceStartMessage -f $Name) + + Import-ServerManager + + $testTargetResourceResult = $false + + $getWindowsFeatureParameters = @{ + Name = $Name + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $getWindowsFeatureParameters['Credential'] = $Credential + } + + Write-Verbose -Message ($script:localizedData.QueryFeature -f $Name) + + $isWinServer2008R2SP1 = Test-IsWinServer2008R2SP1 + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $feature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $Name } ` + -ComputerName . ` + -Credential $Credential + } + else + { + $feature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleFeatureExists -Feature $feature -Name $Name + + # Check if the feature is in the requested Ensure state. + if (($Ensure -eq 'Present' -and $feature.Installed -eq $true) -or ` + ($Ensure -eq 'Absent' -and $feature.Installed -eq $false)) + { + $testTargetResourceResult = $true + + if ($IncludeAllSubFeature) + { + # Check if each subfeature is in the requested state. + foreach ($currentSubFeatureName in $feature.SubFeatures) + { + $getWindowsFeatureParameters['Name'] = $currentSubFeatureName + + if ($isWinServer2008R2SP1 -and $PSBoundParameters.ContainsKey('Credential')) + { + <# + Calling Get-WindowsFeature through Invoke-Command to start a new process with + the given credential since Get-WindowsFeature doesn't support the Credential + attribute on this server. + #> + $subFeature = Invoke-Command -ScriptBlock { Get-WindowsFeature -Name $currentSubFeatureName } ` + -ComputerName . ` + -Credential $Credential + } + else + { + $subFeature = Get-WindowsFeature @getWindowsFeatureParameters + } + + Assert-SingleFeatureExists -Feature $subFeature -Name $currentSubFeatureName + + if (-not $subFeature.Installed -and $Ensure -eq 'Present') + { + $testTargetResourceResult = $false + break + } + + if ($subFeature.Installed -and $Ensure -eq 'Absent') + { + $testTargetResourceResult = $false + break + } + } + } + } + else + { + # Ensure is not in the correct state + $testTargetResourceResult = $false + } + + Write-Verbose -Message ($script:localizedData.TestTargetResourceEndMessage -f $Name) + + return $testTargetResourceResult +} + + +<# + .SYNOPSIS + Asserts that a single instance of the given role or feature exists. + + .PARAMETER Feature + The role or feature object to check. + + .PARAMETER Name + The name of the role or feature to include in any error messages that are thrown. + (Not used to assert validity of the feature). +#> +function Assert-SingleFeatureExists +{ + [CmdletBinding()] + param + ( + [PSObject] + $Feature, + + [String] + $Name + ) + + if ($null -eq $Feature) + { + New-InvalidOperationException -Message ($script:localizedData.FeatureNotFoundError -f $Name) + } + + if ($Feature.Count -gt 1) + { + New-InvalidOperationException -Message ($script:localizedData.MultipleFeatureInstancesError -f $Name) + } +} + +<# + .SYNOPSIS + Sets up the ServerManager module on the target node. + Throws an error if not on a machine running Windows Server. +#> +function Import-ServerManager +{ + param + () + + <# + Enable ServerManager-PSH-Cmdlets feature if OS is WS2008R2 Core. + Datacenter = 12, Standard = 13, Enterprise = 14 + #> + $serverCoreOSCodes = @( 12, 13, 14 ) + + $operatingSystem = Get-CimInstance -Class 'Win32_OperatingSystem' + + # Check if this operating system needs an update to the ServerManager cmdlets + if ($operatingSystem.Version.StartsWith('6.1.') -and ` + $serverCoreOSCodes -contains $operatingSystem.OperatingSystemSKU) + { + Write-Verbose -Message $script:localizedData.EnableServerManagerPSHCmdletsFeature + + <# + ServerManager-PSH-Cmdlets has a depndency on Powershell 2 update: MicrosoftWindowsPowerShell, + so enabling the MicrosoftWindowsPowerShell update. + #> + $null = Dism\online\enable-feature\FeatureName:MicrosoftWindowsPowerShell + $null = Dism\online\enable-feature\FeatureName:ServerManager-PSH-Cmdlets + } + + try + { + Import-Module -Name 'ServerManager' + } + catch + { + Write-Verbose -Message $script:localizedData.ServerManagerModuleNotFoundMessage + New-InvalidOperationException -Message $script:localizedData.SkuNotSupported + } +} + +<# + .SYNOPSIS + Tests if the machine is a Windows Server 2008 R2 SP1 machine. + + .NOTES + Since Assert-PrequisitesValid ensures that ServerManager is available on the machine, + this function only checks the OS version. +#> +function Test-IsWinServer2008R2SP1 +{ + param + () + + return ([Environment]::OSVersion.Version.ToString().Contains('6.1.')) +} + +Export-ModuleMember -Function *-TargetResource diff --git a/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.schema.mof b/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.schema.mof new file mode 100644 index 0000000..b53c91b --- /dev/null +++ b/DscResources/MSFT_WindowsFeature/MSFT_WindowsFeature.schema.mof @@ -0,0 +1,11 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("WindowsFeature")] +class MSFT_WindowsFeature : OMI_BaseResource +{ + [Key, Description("The name of the role or feature to install or uninstall.")] String Name; + [Write, Description("Specifies whether the role or feature should be installed or uninstalled. To install the feature, set this property to Present. To uninstall the feature, set the property to Absent."), ValueMap{"Present", "Absent"}, Values{"Present", "Absent"}] String Ensure; + [Write, Description("Specifies whether the subfeatures of the main feature should also be installed.")] Boolean IncludeAllSubFeature; + [Write, Description("The path to the log file to log this operation.")] String LogPath; + [Write, Description("A credential, if needed, to install or uninstall the role or feature."), EmbeddedInstance("MSFT_Credential")] String Credential; + [Read, Description("The display name of the retrieved role or feature.")] String DisplayName; +}; diff --git a/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.schema.mfl b/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.schema.mfl new file mode 100644 index 0000000..1ff0516 Binary files /dev/null and b/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.schema.mfl differ diff --git a/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.strings.psd1 b/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.strings.psd1 new file mode 100644 index 0000000..c64a2b2 --- /dev/null +++ b/DscResources/MSFT_WindowsFeature/en-US/MSFT_WindowsFeature.strings.psd1 @@ -0,0 +1,23 @@ +# Localized strings for MSFT_WindowsFeature.psd1 + +ConvertFrom-StringData @' + FeatureNotFoundError = The requested feature {0} could not be found on the target machine. + MultipleFeatureInstancesError = Failure to get the requested feature {0} information from the target machine. Wildcard pattern is not supported in the feature name. + FeatureInstallationFailureError = Failure to successfully install the feature {0} . + FeatureUninstallationFailureError = Failure to successfully uninstall the feature {0} . + QueryFeature = Querying for feature {0} using Server Manager cmdlet Get-WindowsFeature. + InstallFeature = Trying to install feature {0} using Server Manager cmdlet Add-WindowsFeature. + UninstallFeature = Trying to uninstall feature {0} using Server Manager cmdlet Remove-WindowsFeature. + RestartNeeded = The Target machine needs to be restarted. + GetTargetResourceStartMessage = Begin executing Get functionality on the {0} feature. + GetTargetResourceEndMessage = End executing Get functionality on the {0} feature. + SetTargetResourceStartMessage = Begin executing Set functionality on the {0} feature. + SetTargetResourceEndMessage = End executing Set functionality on the {0} feature. + TestTargetResourceStartMessage = Begin executing Test functionality on the {0} feature. + TestTargetResourceEndMessage = End executing Test functionality on the {0} feature. + ServerManagerModuleNotFoundMessage = ServerManager module is not installed on the machine. + SkuNotSupported = Installing roles and features using PowerShell Desired State Configuration is supported only on Server SKU's. It is not supported on Client SKU. + EnableServerManagerPSHCmdletsFeature = Windows Server 2008R2 Core operating system detected: ServerManager-PSH-Cmdlets feature has been enabled. + UninstallSuccess = Successfully uninstalled the feature {0}. + InstallSuccess = Successfully installed the feature {0}. +'@ diff --git a/Examples/Sample_WindowsFeature.ps1 b/Examples/Sample_WindowsFeature.ps1 new file mode 100644 index 0000000..c8f08a1 --- /dev/null +++ b/Examples/Sample_WindowsFeature.ps1 @@ -0,0 +1,45 @@ +<# + Create a custom configuration by passing in whatever values you need. + $Name is the only parameter that is required which indicates which + Windows Feature you want to install (or uninstall if you set Ensure to Absent). + LogPath and Credential are not included here, but if you would like to specify + a custom log path or need a credential just pass in the desired values and add + LogPath = $LogPath and/or Credential = $Credential to the configuration here +#> + +Configuration 'Install_Feature_Telnet_Client' +{ + param + ( + [System.String] + $Name = 'Telnet-Client', + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.Boolean] + $IncludeAllSubFeature = $false, + + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [ValidateNotNullOrEmpty()] + [System.String] + $LogPath + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node Localhost { + + WindowsFeature WindowsFeatureTest + { + Name = $Name + Ensure = $Ensure + IncludeAllSubFeature = $IncludeAllSubFeature + } + } +} + diff --git a/Tests/Integration/MSFT_WindowsFeature.Integration.Tests.ps1 b/Tests/Integration/MSFT_WindowsFeature.Integration.Tests.ps1 new file mode 100644 index 0000000..fe70d63 --- /dev/null +++ b/Tests/Integration/MSFT_WindowsFeature.Integration.Tests.ps1 @@ -0,0 +1,297 @@ +<# + Integration tests for Installing/uninstalling a Windows Feature. Currently Telnet-Client is + set as the feature to test since it's fairly small and doesn't require a restart. ADRMS + is set as the feature to test installing/uninstalling a feature with subfeatures, + but this takes a good chunk of time, so by default these tests are set to be skipped. + If there's any major changes to the resource, then set the skipLongTests variable to $false + and run those tests at least once to test the new functionality more completely. +#> + +# Suppressing this rule since we need to create a plaintext password to test this resource +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath 'CommonTestHelper.psm1') ` + -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'PSDscResources' ` + -DscResourceName 'MSFT_WindowsFeature' ` + -TestType 'Integration' + +$script:testFeatureName = 'Telnet-Client' +$script:testFeatureWithSubFeaturesName = 'ADRMS' +$script:installStateOfTestFeature +$script:installStateOfTestWithSubFeatures + +<# + If this is set to $true then the tests that test installing/uninstalling a feature with + its subfeatures will not run. +#> +$script:skipLongTests = $false + +try { + + #Saving the state so we can clean up afterwards + $testFeature = Get-WindowsFeature -Name $script:testFeatureName + $script:installStateOfTestFeature = $testFeature.Installed + + $testFeatureWithSubFeatures = Get-WindowsFeature -Name $script:testFeatureWithSubFeaturesName + $script:installStateOfTestWithSubFeatures = $testFeatureWithSubFeatures.Installed + + $configFile = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_WindowsFeature.config.ps1' + + Describe 'WindowsFeature Integration Tests' { + + $testIncludeAllSubFeature = $false + + Remove-WindowsFeature -Name $script:testFeatureName + + Context "Should Install the Windows Feature: $script:testFeatureName" { + $configurationName = 'MSFT_WindowsFeature_InstallFeature' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'InstallFeatureTest.log' + + try + { + # Ensure the feature is not already on the machine + Remove-WindowsFeature -Name $script:testFeatureName + + It 'Should compile without throwing' { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -Name $script:testFeatureName ` + -IncludeAllSubFeature $testIncludeAllSubFeature ` + -Ensure 'Present' ` + -OutputPath $configurationPath ` + -ErrorAction 'Stop' + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -ErrorAction 'Stop' + $currentConfig.Name | Should Be $script:testFeatureName + $currentConfig.IncludeAllSubFeature | Should Be $testIncludeAllSubFeature + $currentConfig.Ensure | Should Be 'Present' + } + + It 'Should be Installed' { + $feature = Get-WindowsFeature -Name $script:testFeatureName + $feature.Installed | Should Be $true + } + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + Context "Should Uninstall the Windows Feature: $script:testFeatureName" { + $configurationName = 'MSFT_WindowsFeature_UninstallFeature' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'UninstallFeatureTest.log' + + try + { + It 'Should compile without throwing' { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -Name $script:testFeatureName ` + -IncludeAllSubFeature $testIncludeAllSubFeature ` + -Ensure 'Absent' ` + -OutputPath $configurationPath ` + -ErrorAction 'Stop' + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { Get-DscConfiguration -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' { + $currentConfig = Get-DscConfiguration -ErrorAction 'Stop' + $currentConfig.Name | Should Be $script:testFeatureName + $currentConfig.IncludeAllSubFeature | Should Be $testIncludeAllSubFeature + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not be installed' { + $feature = Get-WindowsFeature -Name $script:testFeatureName + $feature.Installed | Should Be $false + } + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + Context "Should Install the Windows Feature: $script:testFeatureWithSubFeaturesName" { + $configurationName = 'MSFT_WindowsFeature_InstallFeatureWithSubFeatures' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'InstallSubFeatureTest.log' + + try + { + if (-not $script:skipLongTests) + { + # Ensure that the feature is not already installed + Remove-WindowsFeature -Name $script:testFeatureWithSubFeaturesName + } + + It 'Should compile without throwing' -Skip:$script:skipLongTests { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -Name $script:testFeatureWithSubFeaturesName ` + -IncludeAllSubFeature $true ` + -Ensure 'Present' ` + -OutputPath $configurationPath ` + -ErrorAction 'Stop' + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' -Skip:$script:skipLongTests { + { Get-DscConfiguration -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' -Skip:$script:skipLongTests { + $currentConfig = Get-DscConfiguration -ErrorAction 'Stop' + $currentConfig.Name | Should Be $script:testFeatureWithSubFeaturesName + $currentConfig.IncludeAllSubFeature | Should Be $true + $currentConfig.Ensure | Should Be 'Present' + } + + It 'Should be Installed (includes check for subFeatures)' -Skip:$script:skipLongTests { + $feature = Get-WindowsFeature -Name $script:testFeatureWithSubFeaturesName + $feature.Installed | Should Be $true + + foreach ($subFeatureName in $feature.SubFeatures) + { + $subFeature = Get-WindowsFeature -Name $subFeatureName + $subFeature.Installed | Should Be $true + } + } + + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + Context "Should Uninstall the Windows Feature: $script:testFeatureWithSubFeaturesName" { + $configurationName = 'MSFT_WindowsFeature_UninstallFeatureWithSubFeatures' + $configurationPath = Join-Path -Path $TestDrive -ChildPath $configurationName + + $logPath = Join-Path -Path $TestDrive -ChildPath 'UninstallSubFeatureTest.log' + + try + { + It 'Should compile without throwing' -Skip:$script:skipLongTests { + { + . $configFile -ConfigurationName $configurationName + & $configurationName -Name $script:testFeatureWithSubFeaturesName ` + -IncludeAllSubFeature $true ` + -Ensure 'Absent' ` + -OutputPath $configurationPath ` + -ErrorAction 'Stop' + Start-DscConfiguration -Path $configurationPath -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' -Skip:$script:skipLongTests { + { Get-DscConfiguration -ErrorAction 'Stop' } | Should Not Throw + } + + It 'Should return the correct configuration' -Skip:$script:skipLongTests { + $currentConfig = Get-DscConfiguration -ErrorAction 'Stop' + $currentConfig.Name | Should Be $script:testFeatureWithSubFeaturesName + $currentConfig.IncludeAllSubFeature | Should Be $false + $currentConfig.Ensure | Should Be 'Absent' + } + + It 'Should not be installed (includes check for subFeatures)' -Skip:$script:skipLongTests { + $feature = Get-WindowsFeature -Name $script:testFeatureWithSubFeaturesName + $feature.Installed | Should Be $false + + foreach ($subFeatureName in $feature.SubFeatures) + { + $subFeature = Get-WindowsFeature -Name $subFeatureName + $subFeature.Installed | Should Be $false + } + } + + } + finally + { + if (Test-Path -Path $logPath) { + Remove-Item -Path $logPath -Recurse -Force + } + + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + } +} +finally +{ + # Ensure that features used for testing are re-installed/uninstalled + if ($script:installStateOfTestFeature) + { + Add-WindowsFeature -Name $script:testFeatureName + } + else + { + Remove-WindowsFeature -Name $script:testFeatureName + } + + if (-not $script:skipLongTests) + { + if ($script:installStateOfTestWithSubFeatures) + { + Add-WindowsFeature -Name $script:testFeatureWithSubFeaturesName -IncludeAllSubFeature + } + else + { + Remove-WindowsFeature -Name $script:testFeatureWithSubFeaturesName + } + } + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} + + diff --git a/Tests/Integration/MSFT_WindowsFeature.config.ps1 b/Tests/Integration/MSFT_WindowsFeature.config.ps1 new file mode 100644 index 0000000..12cc66e --- /dev/null +++ b/Tests/Integration/MSFT_WindowsFeature.config.ps1 @@ -0,0 +1,37 @@ + +param +( + [Parameter(Mandatory)] + [System.String] + $ConfigurationName +) + + +Configuration $ConfigurationName +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [System.Boolean] + $IncludeAllSubFeature = $false + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node Localhost { + + WindowsFeature WindowsFeatureTest + { + Name = $Name + Ensure = $Ensure + IncludeAllSubFeature = $IncludeAllSubFeature + } + } +} diff --git a/Tests/Unit/MSFT_WindowsFeature.Tests.ps1 b/Tests/Unit/MSFT_WindowsFeature.Tests.ps1 new file mode 100644 index 0000000..3cfc7e6 --- /dev/null +++ b/Tests/Unit/MSFT_WindowsFeature.Tests.ps1 @@ -0,0 +1,597 @@ +# Needed to create a fake credential +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +param () + +Import-Module -Name (Join-Path -Path (Split-Path $PSScriptRoot -Parent) ` + -ChildPath (Join-Path -Path 'TestHelpers' ` + -ChildPath 'CommonTestHelper.psm1')) ` + -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DSCResourceModuleName 'PSDscResources' ` + -DSCResourceName 'MSFT_WindowsFeature' ` + -TestType Unit + +try { + InModuleScope 'MSFT_WindowsFeature' { + + $testUserName = 'TestUserName12345' + $testUserPassword = 'StrongOne7.' + $testSecurePassword = ConvertTo-SecureString -String $testUserPassword -AsPlainText -Force + $testCredential = New-Object -TypeName PSCredential -ArgumentList ($testUserName, $testSecurePassword) + + $testWindowsFeatureName1 = 'Test1' + $testWindowsFeatureName2 = 'Test2' + $testSubFeatureName1 = 'SubTest1' + $testSubFeatureName2 = 'SubTest2' + $testSubFeatureName3 = 'SubTest3' + + + $mockWindowsFeatures = @{ + Test1 = @{ + Name = 'Test1' + DisplayName = 'Test Feature 1' + Description = 'Test Feature with 3 subfeatures' + Installed = $false + InstallState = 'Available' + FeatureType = 'Role Service' + Path = 'Test1' + Depth = 1 + DependsOn = @() + Parent = '' + ServerComponentDescriptor = 'ServerComponent_Test_Cert_Authority' + Subfeatures = @('SubTest1','SubTest2','SubTest3') + SystemService = @() + Notification = @() + BestPracticesModelId = $null + EventQuery = $null + PostConfigurationNeeded = $false + AdditionalInfo = @('MajorVersion', 'MinorVersion', 'NumericId', 'InstallName') + } + + SubTest1 = @{ + Name = 'SubTest1' + DisplayName = 'Sub Test Feature 1' + Description = 'Sub Test Feature with parent as test1' + Installed = $true + InstallState = 'Available' + FeatureType = 'Role Service' + Path = 'Test1\SubTest1' + Depth = 2 + DependsOn = @() + Parent = 'Test1' + ServerComponentDescriptor = $null + Subfeatures = @() + SystemService = @() + Notification = @() + BestPracticesModelId = $null + EventQuery = $null + PostConfigurationNeeded = $false + AdditionalInfo = @('MajorVersion', 'MinorVersion', 'NumericId', 'InstallName') + } + + SubTest2 = @{ + Name = 'SubTest2' + DisplayName = 'Sub Test Feature 2' + Description = 'Sub Test Feature with parent as test1' + Installed = $true + InstallState = 'Available' + FeatureType = 'Role Service' + Path = 'Test1\SubTest2' + Depth = 2 + DependsOn = @() + Parent = 'Test1' + ServerComponentDescriptor = $null + Subfeatures = @() + SystemService = @() + Notification = @() + BestPracticesModelId = $null + EventQuery = $null + PostConfigurationNeeded = $false + AdditionalInfo = @('MajorVersion', 'MinorVersion', 'NumericId', 'InstallName') + } + + SubTest3 = @{ + Name = 'SubTest3' + DisplayName = 'Sub Test Feature 3' + Description = 'Sub Test Feature with parent as test1' + Installed = $true + InstallState = 'Available' + FeatureType = 'Role Service' + Path = 'Test\SubTest3' + Depth = 2 + DependsOn = @() + Parent = 'Test1' + ServerComponentDescriptor = $null + Subfeatures = @() + SystemService = @() + Notification = @() + BestPracticesModelId = $null + EventQuery = $null + PostConfigurationNeeded = $false + AdditionalInfo = @('MajorVersion', 'MinorVersion', 'NumericId', 'InstallName') + } + + Test2 = @{ + Name = 'Test2' + DisplayName = 'Test Feature 2' + Description = 'Test Feature with 0 subfeatures' + Installed = $true + InstallState = 'Available' + FeatureType = 'Role Service' + Path = 'Test2' + Depth = 1 + DependsOn = @() + Parent = '' + ServerComponentDescriptor = 'ServerComponent_Test_Cert_Authority' + Subfeatures = @() + SystemService = @() + Notification = @() + BestPracticesModelId = $null + EventQuery = $null + PostConfigurationNeeded = $false + AdditionalInfo = @('MajorVersion', 'MinorVersion', 'NumericId', 'InstallName') + } + } + + + Describe 'WindowsFeature/Get-TargetResource' { + Mock -CommandName Import-ServerManager -MockWith {} + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testWindowsFeatureName2 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testWindowsFeatureName2] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testWindowsFeatureName1 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testWindowsFeatureName1] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName1 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName1] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName2 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName2] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName3 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName3] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + + Context 'Windows Feature exists with no sub features' { + + It 'Should return the correct hashtable when not on a 2008 Server' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + $getTargetResourceResult = Get-TargetResource -Name $testWindowsFeatureName2 + $getTargetResourceResult.Name | Should Be $testWindowsFeatureName2 + $getTargetResourceResult.DisplayName | Should Be $mockWindowsFeatures[$testWindowsFeatureName2].DisplayName + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.IncludeAllSubFeature | Should Be $false + } + + It 'Should return the correct hashtable when on a 2008 Server' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $true } + + $getTargetResourceResult = Get-TargetResource -Name $testWindowsFeatureName2 + $getTargetResourceResult.Name | Should Be $testWindowsFeatureName2 + $getTargetResourceResult.DisplayName | Should Be $mockWindowsFeatures[$testWindowsFeatureName2].DisplayName + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.IncludeAllSubFeature | Should Be $false + } + + It 'Should return the correct hashtable when on a 2008 Server and Credential is passed' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $true } + Mock -CommandName Invoke-Command -MockWith { + $windowsFeature = $mockWindowsFeatures[$testWindowsFeatureName2] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + $getTargetResourceResult = Get-TargetResource -Name $testWindowsFeatureName2 -Credential $testCredential + $getTargetResourceResult.Name | Should Be $testWindowsFeatureName2 + $getTargetResourceResult.DisplayName | Should Be $mockWindowsFeatures[$testWindowsFeatureName2].DisplayName + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.IncludeAllSubFeature | Should Be $false + + Assert-MockCalled -CommandName Invoke-Command -Times 1 -Exactly -Scope It + + } + } + Context 'Windows Feature exists with sub features' { + + It 'Should return the correct hashtable when all subfeatures are installed' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + $getTargetResourceResult = Get-TargetResource -Name $testWindowsFeatureName1 + $getTargetResourceResult.Name | Should Be $testWindowsFeatureName1 + $getTargetResourceResult.DisplayName | Should Be $mockWindowsFeatures[$testWindowsFeatureName1].DisplayName + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.IncludeAllSubFeature | Should Be $true + + Assert-MockCalled -CommandName Test-IsWinServer2008R2SP1 -Times 1 -Exactly -Scope It + } + + It 'Should return the correct hashtable when not all subfeatures are installed' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + $mockWindowsFeatures[$testSubFeatureName3].Installed = $false + + $getTargetResourceResult = Get-TargetResource -Name $testWindowsFeatureName1 + $getTargetResourceResult.Name | Should Be $testWindowsFeatureName1 + $getTargetResourceResult.DisplayName | Should Be $mockWindowsFeatures[$testWindowsFeatureName1].DisplayName + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.IncludeAllSubFeature | Should Be $false + + Assert-MockCalled -CommandName Test-IsWinServer2008R2SP1 -Times 1 -Exactly -Scope It + + $mockWindowsFeatures[$testSubFeatureName3].Installed = $true + } + } + + Context 'Windows Feature does not exist' { + + It 'Should throw invalid operation exception' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + $invalidName = 'InvalidFeature' + { Get-TargetResource -Name $invalidName } | Should Throw ($script:localizedData.FeatureNotFoundError -f $invalidName) + } + } + } + + Describe 'WindowsFeature/Set-TargetResource' { + Mock -CommandName Import-ServerManager -MockWith {} + + Context 'Install/Uninstall successful' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + Mock -CommandName Add-WindowsFeature -MockWith { + $windowsFeature = @{ + Success = $true + RestartNeeded = 'No' + FeatureResult = @($testWindowsFeatureName2) + ExitCode = 'Success' + } + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Remove-WindowsFeature -MockWith { + $windowsFeature = @{ + Success = $true + RestartNeeded = 'No' + FeatureResult = @($testWindowsFeatureName2) + ExitCode = 'Success' + } + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + It 'Should call Add-WindowsFeature when Ensure set to Present' { + { Set-TargetResource -Name $testWindowsFeatureName2 -Ensure 'Present' } | Should Not Throw + Assert-MockCalled -CommandName Add-WindowsFeature -Times 1 -Exactly -Scope It + } + + It 'Should call Remove-WindowsFeature when Ensure set to Absent' { + { Set-TargetResource -Name $testWindowsFeatureName2 -Ensure 'Absent' } | Should Not Throw + Assert-MockCalled -CommandName Remove-WindowsFeature -Times 1 -Exactly -Scope It + } + + } + + Context 'Install/Uninstall unsuccessful' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + Mock -CommandName Add-WindowsFeature -MockWith { + $windowsFeature = @{ + Success = $false + RestartNeeded = 'No' + FeatureResult = @($testWindowsFeatureName2) + ExitCode = 'Nothing succeeded' + } + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Remove-WindowsFeature -MockWith { + $windowsFeature = @{ + Success = $false + RestartNeeded = 'No' + FeatureResult = @($testWindowsFeatureName2) + ExitCode = 'Nothing succeeded' + } + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + It 'Should throw invalid operation exception when Ensure set to Present' { + { Set-TargetResource -Name $testWindowsFeatureName2 -Ensure 'Present' } | + Should Throw ($script:localizedData.FeatureInstallationFailureError -f $testWindowsFeatureName2) + Assert-MockCalled -CommandName Add-WindowsFeature -Times 1 -Exactly -Scope It + } + + It 'Should throw invalid operation exception when Ensure set to Absent' { + { Set-TargetResource -Name $testWindowsFeatureName2 -Ensure 'Absent' } | + Should Throw ($script:localizedData.FeatureUninstallationFailureError -f $testWindowsFeatureName2) + Assert-MockCalled -CommandName Remove-WindowsFeature -Times 1 -Exactly -Scope It + } + + } + + Context 'Uninstall/Install with R2/SP1' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $true } + + Mock -CommandName Invoke-Command -MockWith { + $windowsFeature = @{ + Success = $true + RestartNeeded = 'No' + FeatureResult = @($testWindowsFeatureName2) + ExitCode = 'Success' + } + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + It 'Should install the feature when Ensure set to Present and Credential passed in' { + + { + Set-TargetResource -Name $testWindowsFeatureName2 ` + -Ensure 'Present' ` + -Credential $testCredential + } | Should Not Throw + Assert-MockCalled -CommandName Invoke-Command -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Add-WindowsFeature -Times 0 -Scope It + } + + It 'Should uninstall the feature when Ensure set to Absent and Credential passed in' { + + { + Set-TargetResource -Name $testWindowsFeatureName2 ` + -Ensure 'Absent' ` + -Credential $testCredential + } | Should Not Throw + Assert-MockCalled -CommandName Invoke-Command -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Remove-WindowsFeature -Times 0 -Scope It + } + } + } + + Describe 'WindowsFeature/Test-TargetResource' { + Mock -CommandName Import-ServerManager -MockWith {} + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testWindowsFeatureName1 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testWindowsFeatureName1] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName1 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName1] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName2 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName2] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Mock -CommandName Get-WindowsFeature -ParameterFilter { $Name -eq $testSubFeatureName3 } -MockWith { + $windowsFeature = $mockWindowsFeatures[$testSubFeatureName3] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + # Used as Get-WindowsFeature when on R2/SP1 2008 + Mock -CommandName Invoke-Command -MockWith { + $windowsFeature = $mockWindowsFeatures[$testWindowsFeatureName1] + $windowsFeatureObject = New-Object -TypeName PSObject -Property $windowsFeature + $windowsFeatureObject.PSTypeNames[0] = 'Microsoft.Windows.ServerManager.Commands.Feature' + + return $windowsFeatureObject + } + + Context 'Feature is in the desired state' { + + It 'Should return true when Ensure set to Absent and Feature not installed' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Absent' ` + -IncludeAllSubFeature $false + $testTargetResourceResult | Should Be $true + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + } + + It 'Should return true when Ensure set to Present and Feature installed not checking subFeatures' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $true + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $false + $testTargetResourceResult | Should Be $true + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $false + } + + It 'Should return true when Ensure set to Present and Feature and subFeatures installed' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $true + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $true + $testTargetResourceResult | Should Be $true + Assert-MockCalled -CommandName Get-WindowsFeature -Times 4 -Exactly -Scope It + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $false + } + + It 'Should return true when Ensure set to Absent and Feature not installed and on R2/SP1 2008' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $true } + + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Absent' ` + -IncludeAllSubFeature $false ` + -Credential $testCredential + $testTargetResourceResult | Should Be $true + Assert-MockCalled -CommandName Invoke-Command -Times 1 -Exactly -Scope It + } + } + + Context 'Feature is not in the desired state' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $false } + + It 'Should return false when Ensure set to Present and Feature not installed' { + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $false + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + } + + It 'Should return false when Ensure set to Absent and Feature installed not checking subFeatures' { + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $true + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Absent' ` + -IncludeAllSubFeature $false + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $false + } + + It 'Should return false when Ensure set to Present, Feature not installed and subFeatures installed' { + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $true + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + } + + It 'Should return false when Ensure set to Absent and Feature not installed but subFeatures installed' { + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Absent' ` + -IncludeAllSubFeature $true + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 2 -Exactly -Scope It + } + + It 'Should return false when Ensure set to Absent and Feature installed and subFeatures installed' { + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $true + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Absent' ` + -IncludeAllSubFeature $true + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 1 -Exactly -Scope It + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $false + } + + It 'Should return false when Ensure set to Present and Feature installed but not all subFeatures installed' { + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $true + $mockWindowsFeatures[$testSubFeatureName2].Installed = $false + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $true + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Get-WindowsFeature -Times 3 -Exactly -Scope It + $mockWindowsFeatures[$testWindowsFeatureName1].Installed = $false + $mockWindowsFeatures[$testSubFeatureName2].Installed = $true + } + + It 'Should return false when Ensure set to Present and Feature not installed and on R2/SP1 2008' { + Mock -CommandName Test-IsWinServer2008R2SP1 -MockWith { return $true } + + $testTargetResourceResult = Test-TargetResource -Name $testWindowsFeatureName1 ` + -Ensure 'Present' ` + -IncludeAllSubFeature $false ` + -Credential $testCredential + $testTargetResourceResult | Should Be $false + Assert-MockCalled -CommandName Invoke-Command -Times 1 -Exactly -Scope It + } + } + } + + Describe 'WindowsFeature/Assert-SingleFeatureExists' { + $multipleFeature = @{ + Name = 'MultiFeatureName' + Count = 2 + } + + It 'Should throw invalid operation when feature equals null' { + $nonexistentName = 'NonexistentFeatureName' + { Assert-SingleFeatureExists -Feature $null -Name $nonexistentName } | + Should Throw ($script:localizedData.FeatureNotFoundError -f $nonexistentName) + } + + It 'Should throw invalid operation when there are multiple features with the given name' { + { Assert-SingleFeatureExists -Feature $multipleFeature -Name $multipleFeature.Name } | + Should Throw ($script:localizedData.MultipleFeatureInstancesError -f $multipleFeature.Name) + } + } + + Describe 'WindowsFeature/Import-ServerManager' { + $mockOperatingSystem = @{ + Name = 'mockOS' + Version = '6.1.' + OperatingSystemSKU = 10 + } + Mock -CommandName Get-WmiObject -MockWith { return $mockOperatingSystem } + + It 'Should Not Throw' { + Mock -CommandName Import-Module -MockWith {} + { Import-ServerManager } | Should Not Throw + } + + It 'Should throw invalid operation exception' { + Mock -CommandName Import-Module -MockWith { Throw } + + { Import-ServerManager } | Should Throw ($script:localizedData.SkuNotSupported) + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +}