diff --git a/DSCResources/Helper.psm1 b/DSCResources/Helper.psm1 index 2f87aaf7d..3dc05ab95 100644 --- a/DSCResources/Helper.psm1 +++ b/DSCResources/Helper.psm1 @@ -61,3 +61,152 @@ function Assert-Module -ErrorCategory ObjectNotFound } } + +<# + .SYNOPSIS + Locates one or more certificates using the passed certificate selector parameters. + + If more than one certificate is found matching the selector criteria, they will be + returned in order of descending expiration date. + + .PARAMETER Thumbprint + The thumbprint of the certificate to find. + + .PARAMETER FriendlyName + The friendly name of the certificate to find. + + .PARAMETER Subject + The subject of the certificate to find. + + .PARAMETER DNSName + The subject alternative name of the certificate to export must contain these values. + + .PARAMETER Issuer + The issuer of the certiicate to find. + + .PARAMETER KeyUsage + The key usage of the certificate to find must contain these values. + + .PARAMETER EnhancedKeyUsage + The enhanced key usage of the certificate to find must contain these values. + + .PARAMETER Store + The Windows Certificate Store Name to search for the certificate in. + Defaults to 'My'. + + .PARAMETER AllowExpired + Allows expired certificates to be returned. + +#> +function Find-Certificate +{ + [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2[]])] + param + ( + [Parameter()] + [String] + $Thumbprint, + + [Parameter()] + [String] + $FriendlyName, + + [Parameter()] + [String] + $Subject, + + [Parameter()] + [String[]] + $DNSName, + + [Parameter()] + [String] + $Issuer, + + [Parameter()] + [String[]] + $KeyUsage, + + [Parameter()] + [String[]] + $EnhancedKeyUsage, + + [Parameter()] + [String] + $Store = 'My', + + [Parameter()] + [Boolean] + $AllowExpired = $false + ) + + $certPath = Join-Path -Path 'Cert:\LocalMachine' -ChildPath $Store + + if (-not (Test-Path -Path $certPath)) + { + # The Certificte Path is not valid + New-InvalidArgumentError ` + -ErrorId 'CannotFindCertificatePath' ` + -ErrorMessage ($LocalizedData.CertificatePathError -f $certPath) + } # if + + # Assemble the filter to use to select the certificate + $certFilters = @() + if ($PSBoundParameters.ContainsKey('Thumbprint')) + { + $certFilters += @('($_.Thumbprint -eq $Thumbprint)') + } # if + + if ($PSBoundParameters.ContainsKey('FriendlyName')) + { + $certFilters += @('($_.FriendlyName -eq $FriendlyName)') + } # if + + if ($PSBoundParameters.ContainsKey('Subject')) + { + $certFilters += @('($_.Subject -eq $Subject)') + } # if + + if ($PSBoundParameters.ContainsKey('Issuer')) + { + $certFilters += @('($_.Issuer -eq $Issuer)') + } # if + + if (-not $AllowExpired) + { + $certFilters += @('(((Get-Date) -le $_.NotAfter) -and ((Get-Date) -ge $_.NotBefore))') + } # if + + if ($PSBoundParameters.ContainsKey('DNSName')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject $_.DNSNameList.Unicode -DifferenceObject $DNSName | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('KeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.Extensions.KeyUsages -split ", ") -DifferenceObject $KeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('EnhancedKeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.EnhancedKeyUsageList.FriendlyName) -DifferenceObject $EnhancedKeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + # Join all the filters together + $certFilterScript = '(' + ($certFilters -join ' -and ') + ')' + + Write-Verbose -Message ($LocalizedData.SearchingForCertificateUsingFilters ` + -f $store,$certFilterScript) + + $certs = Get-ChildItem -Path $certPath | + Where-Object -FilterScript ([ScriptBlock]::Create($certFilterScript)) + + # Sort the certificates + if ($certs.count -gt 1) + { + $certs = $certs | Sort-Object -Descending -Property 'NotAfter' + } # if + + return $certs +} # end function Find-Certificate diff --git a/DSCResources/MSFT_xWebAppPoolDefaults/MSFT_xWebAppPoolDefaults.psm1 b/DSCResources/MSFT_xWebAppPoolDefaults/MSFT_xWebAppPoolDefaults.psm1 index 3328e89d9..6def1b06a 100644 --- a/DSCResources/MSFT_xWebAppPoolDefaults/MSFT_xWebAppPoolDefaults.psm1 +++ b/DSCResources/MSFT_xWebAppPoolDefaults/MSFT_xWebAppPoolDefaults.psm1 @@ -24,22 +24,22 @@ function Get-TargetResource [OutputType([System.Collections.Hashtable])] param ( - [Parameter(Mandatory)] + [Parameter(Mandatory = $true)] [ValidateSet('Machine')] - [String] $ApplyTo + [System.String] + $ApplyTo ) - + Assert-Module Write-Verbose -Message $LocalizedData.VerboseGetTargetResource return @{ ManagedRuntimeVersion = (Get-Value -Path '' -Name 'managedRuntimeVersion') - IdentityType = ( Get-Value -Path 'processModel' -Name 'identityType') + IdentityType = (Get-Value -Path 'processModel' -Name 'identityType') } } - function Set-TargetResource { <# @@ -50,16 +50,21 @@ function Set-TargetResource [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] param - ( - [ValidateSet('Machine')] + ( [Parameter(Mandatory = $true)] - [String] $ApplyTo, + [ValidateSet('Machine')] + [System.String] + $ApplyTo, + [Parameter()] [ValidateSet('','v2.0','v4.0')] - [String] $ManagedRuntimeVersion, + [System.String] + $ManagedRuntimeVersion, + [Parameter()] [ValidateSet('ApplicationPoolIdentity','LocalService','LocalSystem','NetworkService')] - [String] $IdentityType + [System.String] + $IdentityType ) Assert-Module @@ -81,15 +86,20 @@ function Test-TargetResource [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSDSCUseVerboseMessageInDSCResource", "")] param ( - [ValidateSet('Machine')] [Parameter(Mandatory = $true)] - [String] $ApplyTo, - + [ValidateSet('Machine')] + [System.String] + $ApplyTo, + + [Parameter()] [ValidateSet('','v2.0','v4.0')] - [String] $ManagedRuntimeVersion, - + [System.String] + $ManagedRuntimeVersion, + + [Parameter()] [ValidateSet('ApplicationPoolIdentity','LocalService','LocalSystem','NetworkService')] - [String] $IdentityType + [System.String] + $IdentityType ) Assert-Module @@ -118,12 +128,19 @@ function Confirm-Value [CmdletBinding()] [OutputType([System.Boolean])] param - ( - [String] $Path, + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Path, - [String] $Name, + [Parameter(Mandatory = $true)] + [System.String] + $Name, - [String] $NewValue + [Parameter()] + [System.String] + $NewValue ) if (-not($NewValue)) @@ -149,12 +166,19 @@ function Set-Value { [CmdletBinding()] param - ( - [String] $Path, - - [String] $Name, - - [String] $NewValue + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.String] + $NewValue ) # if the variable doesn't exist, the user doesn't want to change this value @@ -179,35 +203,40 @@ function Set-Value $relPath = $Path + '/' + $Name Write-Verbose($LocalizedData.SettingValue -f $relPath,$NewValue); - } - } function Get-Value { - [CmdletBinding()] param - ( - [String] $Path, - - [String] $Name + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [System.String] + $Name ) + if ($Path -ne '') { - if ($Path -ne '') - { - $Path = '/' + $Path - } + $Path = '/' + $Path + } - return Get-WebConfigurationProperty ` + $result = Get-WebConfigurationProperty ` -PSPath 'MACHINE/WEBROOT/APPHOST' ` -Filter "system.applicationHost/applicationPools/applicationPoolDefaults$Path" ` -Name $Name - - } + if ($result -is [Microsoft.IIs.PowerShell.Framework.ConfigurationAttribute]) + { + return $result.Value + } else { + return $result + } } #endregion diff --git a/DSCResources/MSFT_xWebApplication/MSFT_xWebApplication.psm1 b/DSCResources/MSFT_xWebApplication/MSFT_xWebApplication.psm1 index 0b18377f7..06452e598 100644 --- a/DSCResources/MSFT_xWebApplication/MSFT_xWebApplication.psm1 +++ b/DSCResources/MSFT_xWebApplication/MSFT_xWebApplication.psm1 @@ -266,9 +266,9 @@ function Set-TargetResource { Write-Verbose -Message ($LocalizedData.VerboseSetTargetEnabledProtocols -f $Name) # Make input bindings which are an array, into a string - $stringafiedEnabledProtocols = $EnabledProtocols -join ' ' + $stringafiedEnabledProtocols = $EnabledProtocols -join ',' Set-ItemProperty -Path "IIS:\Sites\$Website\$Name" ` - -Name EnabledProtocols ` + -Name 'enabledProtocols' ` -Value $stringafiedEnabledProtocols ` -ErrorAction Stop } @@ -625,9 +625,9 @@ function Get-SslFlags ForEach-Object { $_.sslFlags } if ($null -eq $SslFlags) - { - [String]::Empty - } + { + return [String]::Empty + } return $SslFlags } diff --git a/DSCResources/MSFT_xWebsite/MSFT_xWebsite.psm1 b/DSCResources/MSFT_xWebsite/MSFT_xWebsite.psm1 index c4edfcd9b..7cb0138f5 100644 --- a/DSCResources/MSFT_xWebsite/MSFT_xWebsite.psm1 +++ b/DSCResources/MSFT_xWebsite/MSFT_xWebsite.psm1 @@ -23,6 +23,7 @@ data LocalizedData ErrorWebBindingMissingBindingInformation = The BindingInformation property is required for bindings of type "{0}". ErrorWebBindingMissingCertificateThumbprint = The CertificateThumbprint property is required for bindings of type "{0}". ErrorWebBindingMissingSniHostName = The HostName property is required for use with Server Name Indication. + ErrorWebBindingInvalidCertificateSubject = The Subject "{0}" provided is not found on this host in store "{1}" ErrorWebsitePreloadFailure = Failure to set Preload on Website "{0}". Error: "{1}". ErrorWebsiteAutoStartFailure = Failure to set AutoStart on Website "{0}". Error: "{1}". ErrorWebsiteAutoStartProviderFailure = Failure to set AutoStartProvider on Website "{0}". Error: "{1}". @@ -1464,11 +1465,24 @@ function ConvertTo-WebBinding { if ([String]::IsNullOrEmpty($binding.CertificateThumbprint)) { - $errorMessage = $LocalizedData.ErrorWebBindingMissingCertificateThumbprint ` - -f $binding.Protocol - New-TerminatingError -ErrorId 'WebBindingMissingCertificateThumbprint' ` - -ErrorMessage $errorMessage ` - -ErrorCategory 'InvalidArgument' + If ($Binding.CertificateSubject) + { + if ($binding.CertificateSubject.substring(0,3) -ne 'CN=') + { + $binding.CertificateSubject = "CN=$($Binding.CertificateSubject)" + } + $FindCertificateSplat = @{ + Subject = $Binding.CertificateSubject + } + } + else + { + $errorMessage = $LocalizedData.ErrorWebBindingMissingCertificateThumbprint ` + -f $binding.Protocol + New-TerminatingError -ErrorId 'WebBindingMissingCertificateThumbprint' ` + -ErrorMessage $errorMessage ` + -ErrorCategory 'InvalidArgument' + } } if ([String]::IsNullOrEmpty($binding.CertificateStoreName)) @@ -1483,8 +1497,33 @@ function ConvertTo-WebBinding $certificateStoreName = $binding.CertificateStoreName } + if ($FindCertificateSplat) + { + $FindCertificateSplat.Add('Store',$CertificateStoreName) + $Certificate = Find-Certificate @FindCertificateSplat + if ($Certificate) + { + $certificateHash = $Certificate.Thumbprint + } + else + { + $errorMessage = $LocalizedData.ErrorWebBindingInvalidCertificateSubject ` + -f $binding.CertificateSubject, $binding.CertificateStoreName + New-TerminatingError -ErrorId 'WebBindingInvalidCertificateSubject' ` + -ErrorMessage $errorMessage ` + -ErrorCategory 'InvalidArgument' + } + } + # Remove the Left-to-Right Mark character - $certificateHash = $binding.CertificateThumbprint -replace '^\u200E' + if ($certificateHash) + { + $certificateHash = $certificateHash -replace '^\u200E' + } + else + { + $certificateHash = $binding.CertificateThumbprint -replace '^\u200E' + } $outputObject.Add('certificateHash', [String]$certificateHash) $outputObject.Add('certificateStoreName', [String]$certificateStoreName) diff --git a/DSCResources/MSFT_xWebsite/MSFT_xWebsite.schema.mof b/DSCResources/MSFT_xWebsite/MSFT_xWebsite.schema.mof index fc452e3e4..05a148256 100644 --- a/DSCResources/MSFT_xWebsite/MSFT_xWebsite.schema.mof +++ b/DSCResources/MSFT_xWebsite/MSFT_xWebsite.schema.mof @@ -7,6 +7,7 @@ class MSFT_xWebBindingInformation [Write] UInt16 Port; [Write] String HostName; [Write] String CertificateThumbprint; + [Write] String CertificateSubject; [Write,ValueMap{"My", "WebHosting"},Values{"My", "WebHosting"}] String CertificateStoreName; [Write,ValueMap{"0","1","2","3"},Values{"0","1","2","3"}] String SslFlags; }; diff --git a/README.md b/README.md index 804d7f3bc..05d0480fe 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Please check out common DSC Resources [contributing guidelines](https://github.c * **Port**: The port of the binding. The value must be a positive integer between `1` and `65535`. This property is only applicable for `http` (the default value is `80`) and `https` (the default value is `443`) bindings. * **HostName**: The host name of the binding. This property is only applicable for `http` and `https` bindings. * **CertificateThumbprint**: The thumbprint of the certificate. This property is only applicable for `https` bindings. + * **CertificateSubject**: The subject of the certificate if the thumbprint isn't known. This property is only applicable for `https` bindings. * **CertificateStoreName**: The name of the certificate store where the certificate is located. This property is only applicable for `https` bindings. The acceptable values for this property are: `My`, `WebHosting`. The default value is `My`. * **SslFlags**: The type of binding used for Secure Sockets Layer (SSL) certificates. This property is supported in IIS 8.0 or later, and is only applicable for `https` bindings. The acceptable values for this property are: * **0**: The default value. The secure connection be made using an IP/Port combination. Only one certificate can be bound to a combination of IP address and the port. @@ -239,6 +240,11 @@ Please check out common DSC Resources [contributing guidelines](https://github.c ### Unreleased +### 1.19.0.0 + +* **xWebAppPoolDefaults** now returns values. Fixes #311. +* Added unit tests for **xWebAppPoolDefaults**. Fixes #183. + ### 1.18.0.0 * Added sample for **xWebVirtualDirectory** for creating a new virtual directory. Bugfix for #195. @@ -713,6 +719,91 @@ Configuration Sample_xWebsite_NewWebsite } ``` +When specifying a HTTPS web binding you can also specify a certifcate subject, for cases where the certificate +is being generated by the same configuration using something like xCertReq. + +```powershell +Configuration Sample_xWebsite_NewWebsite +{ + param + ( + # Target nodes to apply the configuration + [string[]]$NodeName = 'localhost', + # Name of the website to create + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String]$WebSiteName, + # Source Path for Website content + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String]$SourcePath, + # Destination path for Website content + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [String]$DestinationPath + ) + + # Import the module that defines custom resources + Import-DscResource -Module xWebAdministration + Node $NodeName + { + # Install the IIS role + WindowsFeature IIS + { + Ensure = "Present" + Name = "Web-Server" + } + + # Install the ASP .NET 4.5 role + WindowsFeature AspNet45 + { + Ensure = "Present" + Name = "Web-Asp-Net45" + } + + # Stop the default website + xWebsite DefaultSite + { + Ensure = "Present" + Name = "Default Web Site" + State = "Stopped" + PhysicalPath = "C:\inetpub\wwwroot" + DependsOn = "[WindowsFeature]IIS" + } + + # Copy the website content + File WebContent + { + Ensure = "Present" + SourcePath = $SourcePath + DestinationPath = $DestinationPath + Recurse = $true + Type = "Directory" + DependsOn = "[WindowsFeature]AspNet45" + } + + # Create the new Website with HTTPS + xWebsite NewWebsite + { + Ensure = "Present" + Name = $WebSiteName + State = "Started" + PhysicalPath = $DestinationPath + BindingInfo = @( + MSFT_xWebBindingInformation + { + Protocol = "HTTPS" + Port = 8444 + CertificateSubject = "CN=CertificateSubject" + CertificateStoreName = "MY" + } + ) + DependsOn = "[File]WebContent" + } + } +} +``` + ### Creating the default website using configuration data In this example, we’ve moved the parameters used to generate the website into a configuration data file. diff --git a/Tests/Integration/MSFT_xWebAppPoolDefaults.Integration.Tests.ps1 b/Tests/Integration/MSFT_xWebAppPoolDefaults.Integration.Tests.ps1 index 432c091a7..3b6e4d551 100644 --- a/Tests/Integration/MSFT_xWebAppPoolDefaults.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xWebAppPoolDefaults.Integration.Tests.ps1 @@ -50,7 +50,7 @@ try } | Should not throw } - It 'should be able to call Get-DscConfiguration without throwing' { + It 'Should be able to call Get-DscConfiguration without throwing' { { Get-DscConfiguration -Verbose -ErrorAction Stop } | Should Not throw } #endregion diff --git a/Tests/Integration/MSFT_xWebApplication.Integration.Tests.ps1 b/Tests/Integration/MSFT_xWebApplication.Integration.Tests.ps1 index 2c96a201e..1f8d2a836 100644 --- a/Tests/Integration/MSFT_xWebApplication.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xWebApplication.Integration.Tests.ps1 @@ -83,7 +83,7 @@ try It 'Should create a WebApplication with correct settings' -test { - Invoke-Expression -Command "$($script:DSCResourceName)_Present -ConfigurationData `$DSCConfg -OutputPath `$TestDrive" + Invoke-Expression -Command "$($script:DSCResourceName)_Present -ConfigurationData `$DSCConfg -OutputPath `$TestDrive" # Build results to test $Result = Get-WebApplication -Site $DSCConfig.AllNodes.Website -Name $DSCConfig.AllNodes.WebApplication @@ -113,7 +113,7 @@ try Get-SslFlags -Website $DSCConfig.AllNodes.Website -WebApplication $DSCConfig.AllNodes.WebApplication | Should Be $DSCConfig.AllNodes.WebApplicationSslFlags # Test EnabledProtocols - $Result.EnabledProtocols | Should Be $DSCConfig.AllNodes.EnabledProtocols + $Result.EnabledProtocols | Should Be ($DSCConfig.AllNodes.EnabledProtocols -join ',') } diff --git a/Tests/Integration/MSFT_xWebsite.Integration.Tests.ps1 b/Tests/Integration/MSFT_xWebsite.Integration.Tests.ps1 index 5ff934609..85ad2beff 100644 --- a/Tests/Integration/MSFT_xWebsite.Integration.Tests.ps1 +++ b/Tests/Integration/MSFT_xWebsite.Integration.Tests.ps1 @@ -110,12 +110,16 @@ try $result.bindings.Collection.BindingInformation[0] | Should Match $dscConfig.AllNodes.HTTP1Hostname $result.bindings.Collection.BindingInformation[1] | Should Match $dscConfig.AllNodes.HTTP2Hostname $result.bindings.Collection.BindingInformation[2] | Should Match $dscConfig.AllNodes.HTTPSHostname + $result.bindings.Collection.BindingInformation[3] | Should Match $dscConfig.AllNodes.HTTPSHostname $result.bindings.Collection.BindingInformation[0] | Should Match $dscConfig.AllNodes.HTTPPort $result.bindings.Collection.BindingInformation[1] | Should Match $dscConfig.AllNodes.HTTPPort $result.bindings.Collection.BindingInformation[2] | Should Match $dscConfig.AllNodes.HTTPSPort $result.bindings.Collection.certificateHash[2] | Should Be $selfSignedCert.Thumbprint $result.bindings.Collection.certificateStoreName[2] | Should Be $dscConfig.AllNodes.CertificateStoreName - + $result.bindings.Collection.BindingInformation[3] | Should Match $dscConfig.AllNodes.HTTPSPort2 + $result.bindings.Collection.certificateHash[3] | Should Be $selfSignedCert.Thumbprint + $result.bindings.Collection.certificateStoreName[3] | Should Be $dscConfig.AllNodes.CertificateStoreName + #Test DefaultPage is correct $defultPages[0] | Should Match $dscConfig.AllNodes.DefaultPage @@ -178,11 +182,15 @@ try $result.bindings.Collection.BindingInformation[0] | Should Match $dscConfig.AllNodes.HTTP1Hostname $result.bindings.Collection.BindingInformation[1] | Should Match $dscConfig.AllNodes.HTTP2Hostname $result.bindings.Collection.BindingInformation[2] | Should Match $dscConfig.AllNodes.HTTPSHostname + $result.bindings.Collection.BindingInformation[3] | Should Match $dscConfig.AllNodes.HTTPSHostname $result.bindings.Collection.BindingInformation[0] | Should Match $dscConfig.AllNodes.HTTPPort $result.bindings.Collection.BindingInformation[1] | Should Match $dscConfig.AllNodes.HTTPPort $result.bindings.Collection.BindingInformation[2] | Should Match $dscConfig.AllNodes.HTTPSPort $result.bindings.Collection.certificateHash[2] | Should Be $selfSignedCert.Thumbprint $result.bindings.Collection.certificateStoreName[2] | Should Be $dscConfig.AllNodes.CertificateStoreName + $result.bindings.Collection.BindingInformation[3] | Should Match $dscConfig.AllNodes.HTTPSPort2 + $result.bindings.Collection.certificateHash[3] | Should Be $selfSignedCert.Thumbprint + $result.bindings.Collection.certificateStoreName[3] | Should Be $dscConfig.AllNodes.CertificateStoreName #Test DefaultPage is correct $defultPages[0] | Should Match $dscConfig.AllNodes.DefaultPage diff --git a/Tests/Integration/MSFT_xWebsite.config.ps1 b/Tests/Integration/MSFT_xWebsite.config.ps1 index 588a4bb87..360b00a33 100644 --- a/Tests/Integration/MSFT_xWebsite.config.ps1 +++ b/Tests/Integration/MSFT_xWebsite.config.ps1 @@ -50,6 +50,16 @@ configuration MSFT_xWebsite_Present_Started CertificateThumbprint = $CertificateThumbprint CertificateStoreName = $Node.CertificateStoreName SslFlags = $Node.SslFlags + } + MSFT_xWebBindingInformation + { + Protocol = $Node.HTTPSProtocol + Port = $Node.HTTPSPort2 + IPAddress = '*' + Hostname = $Node.HTTPSHostname + CertificateSubject = $Node.HTTPSHostname + CertificateStoreName = $Node.CertificateStoreName + SslFlags = $Node.SslFlags }) DefaultPage = $Node.DefaultPage EnabledProtocols = $Node.EnabledProtocols @@ -113,7 +123,17 @@ configuration MSFT_xWebsite_Present_Stopped CertificateThumbprint = $CertificateThumbprint CertificateStoreName = $Node.CertificateStoreName SslFlags = $Node.SslFlags - }) + } + MSFT_xWebBindingInformation + { + Protocol = $Node.HTTPSProtocol + Port = $Node.HTTPSPort2 + IPAddress = '*' + Hostname = $Node.HTTPSHostname + CertificateSubject = $Node.HTTPSHostname + CertificateStoreName = $Node.CertificateStoreName + SslFlags = $Node.SslFlags + }) DefaultPage = $Node.DefaultPage EnabledProtocols = $Node.EnabledProtocols PhysicalPath = $Node.PhysicalPath diff --git a/Tests/Integration/MSFT_xWebsite.config.psd1 b/Tests/Integration/MSFT_xWebsite.config.psd1 index 716eed53b..b94e07c83 100644 --- a/Tests/Integration/MSFT_xWebsite.config.psd1 +++ b/Tests/Integration/MSFT_xWebsite.config.psd1 @@ -24,6 +24,7 @@ HTTP2Hostname = 'http2.website' HTTPSProtocol = 'https' HTTPSPort = '443' + HTTPSPort2 = '8444' HTTPSHostname = 'https.website' CertificateStoreName = 'MY' SslFlags = '1' diff --git a/Tests/TestHelper/CommonTestHelper.psm1 b/Tests/TestHelper/CommonTestHelper.psm1 new file mode 100644 index 000000000..8dd376e4d --- /dev/null +++ b/Tests/TestHelper/CommonTestHelper.psm1 @@ -0,0 +1,135 @@ +<# + .SYNOPSIS + Returns an invalid argument exception object + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ArgumentName + The name of the invalid argument that is causing this error to be thrown +#> +function Get-InvalidArgumentRecord +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ArgumentName + ) + + $argumentException = New-Object -TypeName 'ArgumentException' -ArgumentList @( $Message, + $ArgumentName ) + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $argumentException, $ArgumentName, 'InvalidArgument', $null ) + } + return New-Object @newObjectParams +} + +<# + .SYNOPSIS + Returns an invalid operation exception object + + .PARAMETER Message + The message explaining why this error is being thrown + + .PARAMETER ErrorRecord + The error record containing the exception that is causing this terminating error +#> +function Get-InvalidOperationRecord +{ + [CmdletBinding()] + param + ( + [ValidateNotNullOrEmpty()] + [String] + $Message, + + [ValidateNotNull()] + [System.Management.Automation.ErrorRecord] + $ErrorRecord + ) + + if ($null -eq $Message) + { + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' + } + elseif ($null -eq $ErrorRecord) + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message ) + } + else + { + $invalidOperationException = + New-Object -TypeName 'InvalidOperationException' -ArgumentList @( $Message, + $ErrorRecord.Exception ) + } + + $newObjectParams = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( $invalidOperationException.ToString(), 'MachineStateIncorrect', + 'InvalidOperation', $null ) + } + return New-Object @newObjectParams +} + +<# + .SYNOPSIS + Some tests require a self-signed certificate to be created. However, the + New-SelfSignedCertificate cmdlet built into Windows Server 2012 R2 is too + limited to work for this process. + + Therefore an alternate method of creating self-signed certificates to meet the + reqirements. A script on Microsoft Script Center can be used for this but must + be downloaded: + https://gallery.technet.microsoft.com/scriptcenter/Self-signed-certificate-5920a7c6 + + This cmdlet will install the script if it is not available and dot source it. + + .PARAMETER OutputPath + The path to download the script to. If not provided will default to the current + users temp folder. + + .OUTPUTS + The path to the script that was downloaded. +#> +function Install-NewSelfSignedCertificateExScript +{ + [CmdletBinding()] + [OutputType([String])] + param + ( + [Parameter()] + [String] + $OutputPath = $env:Temp + ) + + $newSelfSignedCertURL = 'https://gallery.technet.microsoft.com/scriptcenter/Self-signed-certificate-5920a7c6/file/101251/2/New-SelfSignedCertificateEx.zip' + $newSelfSignedCertZip = Split-Path -Path $newSelfSignedCertURL -Leaf + $newSelfSignedCertZipPath = Join-Path -Path $OutputPath -ChildPath $newSelfSignedCertZip + $newSelfSignedCertScriptPath = Join-Path -Path $OutputPath -ChildPath 'New-SelfSignedCertificateEx.ps1' + if (-not (Test-Path -Path $newSelfSignedCertScriptPath)) + { + if (Test-Path -Path $newSelfSignedCertZip) + { + Remove-Item -Path $newSelfSignedCertZipPath -Force + } + Invoke-WebRequest -Uri $newSelfSignedCertURL -OutFile $newSelfSignedCertZipPath + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($newSelfSignedCertZipPath, $OutputPath) + } # if + return $newSelfSignedCertScriptPath +} # end function Install-NewSelfSignedCertificateExScript + +Export-ModuleMember -Function ` + Install-NewSelfSignedCertificateExScript, ` + Get-InvalidArgumentRecord, ` + Get-InvalidOperationRecord diff --git a/Tests/Unit/Helper.tests.ps1 b/Tests/Unit/Helper.tests.ps1 new file mode 100644 index 000000000..31e66846a --- /dev/null +++ b/Tests/Unit/Helper.tests.ps1 @@ -0,0 +1,480 @@ +$script:ModuleName = 'Helper' +$script:DSCModuleName = 'xWebAdministration' + +#region HEADER +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) + if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force + +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'Tests\TestHelper\CommonTestHelper.psm1') +Import-Module (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResources\Helper.psm1') +#endregion + +# Begin Testing +try +{ + InModuleScope $script:ModuleName { + + Describe "$DSCResourceName\Find-Certificate" { + + # Download and dot source the New-SelfSignedCertificateEx script + . (Install-NewSelfSignedCertificateExScript) + + # Generate the Valid certificate for testing but remove it from the store straight away + $certDNSNames = @('www.fabrikam.com', 'www.contoso.com') + $certDNSNamesReverse = @('www.contoso.com', 'www.fabrikam.com') + $certDNSNamesNoMatch = $certDNSNames + @('www.nothere.com') + $certKeyUsage = @('DigitalSignature','DataEncipherment') + $certKeyUsageReverse = @('DataEncipherment','DigitalSignature') + $certKeyUsageNoMatch = $certKeyUsage + @('KeyEncipherment') + $certEKU = @('Server Authentication','Client authentication') + $certEKUReverse = @('Client authentication','Server Authentication') + $certEKUNoMatch = $certEKU + @('Encrypting File System') + $certSubject = 'CN=contoso, DC=com' + $certFriendlyName = 'Contoso Test Cert' + $validCert = New-SelfSignedCertificateEx ` + -Subject $certSubject ` + -KeyUsage $certKeyUsage ` + -KeySpec 'Exchange' ` + -EKU $certEKU ` + -SubjectAlternativeName $certDNSNames ` + -FriendlyName $certFriendlyName ` + -StoreLocation 'CurrentUser' ` + -Exportable + # Pull the generated certificate from the store so we have the friendlyname + $validThumbprint = $validCert.Thumbprint + $validCert = Get-Item -Path "cert:\CurrentUser\My\$validThumbprint" + Remove-Item -Path $validCert.PSPath -Force + + # Generate the Expired certificate for testing but remove it from the store straight away + $expiredCert = New-SelfSignedCertificateEx ` + -Subject $certSubject ` + -KeyUsage $certKeyUsage ` + -KeySpec 'Exchange' ` + -EKU $certEKU ` + -SubjectAlternativeName $certDNSNames ` + -FriendlyName $certFriendlyName ` + -NotBefore ((Get-Date) - (New-TimeSpan -Days 2)) ` + -NotAfter ((Get-Date) - (New-TimeSpan -Days 1)) ` + -StoreLocation 'CurrentUser' ` + -Exportable + # Pull the generated certificate from the store so we have the friendlyname + $expiredThumbprint = $expiredCert.Thumbprint + $expiredCert = Get-Item -Path "cert:\CurrentUser\My\$expiredThumbprint" + Remove-Item -Path $expiredCert.PSPath -Force + + $nocertThumbprint = '1111111111111111111111111111111111111111' + + # Dynamic mock content for Get-ChildItem + $mockGetChildItem = { + switch ( $Path ) + { + 'cert:\LocalMachine\My' + { + return @( $validCert ) + } + + 'cert:\LocalMachine\NoCert' + { + return @() + } + + 'cert:\LocalMachine\TwoCerts' + { + return @( $expiredCert, $validCert ) + } + + 'cert:\LocalMachine\Expired' + { + return @( $expiredCert ) + } + + default + { + throw 'mock called with unexpected value {0}' -f $Path + } + } + } + + BeforeEach { + Mock ` + -CommandName Test-Path ` + -MockWith { $true } + + Mock ` + -CommandName Get-ChildItem ` + -MockWith $mockGetChildItem + } + + Context 'Thumbprint only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $nocertThumbprint } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName 'Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Subject only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Subject $certSubject } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Subject only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Subject 'CN=Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Issuer only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Issuer $certSubject } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Issuer only is passed and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Issuer 'CN=Does Not Exist' } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNames } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNamesReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNames[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'DNSName only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -DnsName $certDNSNamesNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsage } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsageReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsage[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'KeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certKeyUsageNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKU } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed in reversed order and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKUReverse } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKU[0] } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'EnhancedKeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certEKUNoMatch } | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist in the store' { + It 'should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint -Store 'NoCert'} | Should Not Throw + } + + It 'should return null' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and both valid and expired certificates exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'TwoCerts' } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $validThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and only expired certificates exist' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'Expired' } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result | Should BeNullOrEmpty + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + + Context 'FriendlyName only is passed and only expired certificates exist but allowexpired passed' { + It 'should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certFriendlyName -Store 'Expired' -AllowExpired:$true } | Should Not Throw + } + + It 'should return expected certificate' { + $script:result.Thumbprint | Should Be $expiredThumbprint + } + + It 'should call expected mocks' { + Assert-MockCalled -CommandName Test-Path -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ChildItem -Exactly -Times 1 + } + } + } + } +} +finally +{ + #region FOOTER + #endregion +} diff --git a/Tests/Unit/MSFT_xSSLSettings.Tests.ps1 b/Tests/Unit/MSFT_xSSLSettings.Tests.ps1 index e5dfda402..93671f582 100644 --- a/Tests/Unit/MSFT_xSSLSettings.Tests.ps1 +++ b/Tests/Unit/MSFT_xSSLSettings.Tests.ps1 @@ -27,6 +27,7 @@ try #region Pester Tests InModuleScope $DSCResourceName { + $script:DSCResourceName = 'MSFT_xSSLSettings' Describe "$script:DSCResourceName\Test-TargetResource" { Context 'Ensure is Present and SSLSettings is Present' { @@ -38,7 +39,7 @@ try $result = Test-TargetResource -Name 'Test' -Ensure 'Present' -Bindings 'Ssl' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return true' { $result | should be $true @@ -54,7 +55,7 @@ try $result = Test-TargetResource -Name 'Test' -Ensure 'Absent' -Bindings 'Ssl' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return true' { $result | should be $true @@ -70,7 +71,7 @@ try $result = Test-TargetResource -Name 'Test' -Ensure 'Present' -Bindings 'Ssl' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return true' { $result | should be $false @@ -90,7 +91,7 @@ try Ensure = 'Present' } - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return the correct bindings' { $result.Bindings | should be $expected.Bindings @@ -112,7 +113,7 @@ try Ensure = 'Absent' } - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return the correct bindings' { $result.Bindings | should be $expected.Bindings @@ -134,7 +135,7 @@ try # Check that the LocalizedData message from the Set-TargetResource is correct $resultMessage = $LocalizedData.SettingSSLConfig -f 'Name', '' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return the correct string' { $result | Should Be $resultMessage @@ -150,7 +151,7 @@ try # Check that the LocalizedData message from the Set-TargetResource is correct $resultMessage = $LocalizedData.SettingSSLConfig -f 'Name', 'Ssl' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return the correct string' { $result | Should Be $resultMessage @@ -166,7 +167,7 @@ try # Check that the LocalizedData message from the Set-TargetResource is correct $resultMessage = $LocalizedData.SettingSSLConfig -f 'Name', 'Ssl,SslNegotiateCert,SslRequireCert' - Assert-VerifiableMocks + Assert-VerifiableMock It 'should return the correct string' { $result | Should Be $resultMessage diff --git a/Tests/Unit/MSFT_xWebAppPoolDefaults.tests.ps1 b/Tests/Unit/MSFT_xWebAppPoolDefaults.tests.ps1 new file mode 100644 index 000000000..c6161a95f --- /dev/null +++ b/Tests/Unit/MSFT_xWebAppPoolDefaults.tests.ps1 @@ -0,0 +1,194 @@ +#region HEADER + +$script:DSCModuleName = 'xWebAdministration' +$script:DSCResourceName = 'MSFT_xWebAppPoolDefaults' + +# Unit Test Template Version: 1.2.1 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git', ` + (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path ` + -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force + + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup { +} + +function Invoke-TestCleanup { + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + + Describe "$($script:DSCResourceName)\Get-TargetResource" { + + Context 'Get application pool defaults' { + $mockAppPoolDefaults = @{ + managedRuntimeVersion = 'v4.0' + processModel = @{ + identityType = 'SpecificUser' + } + } + + Mock Get-WebConfigurationProperty -MockWith { + $path = $Filter.Replace('system.applicationHost/applicationPools/applicationPoolDefaults', '') + + if ([System.String]::IsNullOrEmpty($path)) { + return $mockAppPoolDefaults[$Name] + } else { + $path = $path.Replace('/', '') + return $mockAppPoolDefaults[$path][$Name] + } + } + + $result = Get-TargetResource -ApplyTo 'Machine' + + It 'Should return managedRuntimeVersion' { + $result.managedRuntimeVersion | ` + Should Be $mockAppPoolDefaults.managedRuntimeVersion + } + + It 'Should return processModel\identityType' { + $result.identityType | ` + Should Be $mockAppPoolDefaults.processModel.identityType + } + } + } + + Describe "$($script:DSCResourceName)\Test-TargetResource" { + + $mockAppPoolDefaults = @{ + managedRuntimeVersion = 'v4.0' + processModel = @{ + identityType = 'NetworkService' + } + } + + Mock Get-WebConfigurationProperty -MockWith { + $path = $Filter.Replace('system.applicationHost/applicationPools/applicationPoolDefaults', '') + + if ([System.String]::IsNullOrEmpty($path)) { + return $mockAppPoolDefaults[$Name] + } else { + $path = $path.Replace('/', '') + return $mockAppPoolDefaults[$path][$Name] + } + } + + Context 'Application pool defaults correct' { + $result = Test-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v4.0' ` + -IdentityType 'NetworkService' + + It 'Should return True' { + $result | Should Be $true + } + } + + Context 'Application pool different managedRuntimeVersion' { + $result = Test-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v2.0' ` + -IdentityType 'NetworkService' + + It 'Should return False' { + $result | Should Be $false + } + } + + Context 'Application pool different processModel/@identityType' { + $result = Test-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v4.0' ` + -IdentityType 'LocalSystem' + + It 'Should return False' { + $result | Should Be $false + } + } + + Context 'Application pool no value for managedRuntimeVersion' { + $result = Test-TargetResource -ApplyTo 'Machine' ` + -IdentityType 'NetworkService' + + It 'Should return True' { + $result | Should Be $true + } + } + } + + Describe "$($script:DSCResourceName)\Set-TargetResource" { + + $mockAppPoolDefaults = @{ + managedRuntimeVersion = 'v4.0' + processModel = @{ + identityType = 'NetworkService' + } + } + + Mock Get-WebConfigurationProperty -MockWith { + $path = $Filter.Replace('system.applicationHost/applicationPools/applicationPoolDefaults', '') + + if ([System.String]::IsNullOrEmpty($path)) { + return $mockAppPoolDefaults[$Name] + } else { + $path = $path.Replace('/', '') + return $mockAppPoolDefaults[$path][$Name] + } + } + + Mock Set-WebConfigurationProperty -MockWith { } + + Context 'Application pool defaults correct' { + Set-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v4.0' ` + -IdentityType 'NetworkService' + + It 'Should not call Set-WebConfigurationProperty' { + Assert-MockCalled Set-WebConfigurationProperty -Exactly 0 + } + } + + Context 'Application pool different managedRuntimeVersion' { + Set-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v2.0' ` + -IdentityType 'NetworkService' + + It 'Should call Set-WebConfigurationProperty once' { + Assert-MockCalled Set-WebConfigurationProperty -Exactly 1 ` + -ParameterFilter { $Name -eq 'managedRuntimeVersion' } + } + } + + Context 'Application pool different processModel/@identityType' { + Set-TargetResource -ApplyTo 'Machine' ` + -ManagedRuntimeVersion 'v4.0' ` + -IdentityType 'LocalSystem' + + It 'Should call Set-WebConfigurationProperty once' { + Assert-MockCalled Set-WebConfigurationProperty -Exactly 1 ` + -ParameterFilter { $Name -eq 'identityType' } + } + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/Tests/Unit/MSFT_xWebsite.Tests.ps1 b/Tests/Unit/MSFT_xWebsite.Tests.ps1 index 49d2b8b59..c9f9106a9 100644 --- a/Tests/Unit/MSFT_xWebsite.Tests.ps1 +++ b/Tests/Unit/MSFT_xWebsite.Tests.ps1 @@ -25,7 +25,8 @@ try { #region Pester Tests InModuleScope -ModuleName $script:DSCResourceName -ScriptBlock { - + $script:DSCResourceName = 'MSFT_xWebsite' + Describe "$script:DSCResourceName\Assert-Module" { Context 'WebAdminstration module is not installed' { Mock -ModuleName Helper -CommandName Get-Module -MockWith { return $null } @@ -1922,7 +1923,7 @@ try } } - Describe "$script:DSCResourceName\ConvertTo-WebBinding" { + Describe "$script:DSCResourceName\ConvertTo-WebBinding" -Tag 'ConvertTo' { Context 'Expected behaviour' { $MockBindingInfo = @( New-CimInstance ` @@ -2101,7 +2102,90 @@ try $ErrorRecord = New-Object ` -TypeName System.Management.Automation.ErrorRecord ` -ArgumentList $Exception, $ErrorId, $ErrorCategory, $null + { ConvertTo-WebBinding -InputObject $MockBindingInfo } | Should Throw $ErrorRecord + } + } + + Context 'Protocol is HTTPS and CertificateSubject is specified' { + $MockBindingInfo = @( + New-CimInstance -ClassName MSFT_xWebBindingInformation ` + -Namespace root/microsoft/Windows/DesiredStateConfiguration ` + -Property @{ + Protocol = 'https' + CertificateSubject = 'TestCertificate' + } -ClientOnly + ) + + Mock Find-Certificate -MockWith { + return [PSCustomObject]@{ + Thumbprint = 'C65CE51E20C523DEDCE979B9922A0294602D9D5C' + } + } + + It 'should not throw an error' { + { ConvertTo-WebBinding -InputObject $MockBindingInfo } | Should Not Throw + } + It 'should return the correct thumbprint' { + $Result = ConvertTo-WebBinding -InputObject $MockBindingInfo + $Result.certificateHash | Should Be 'C65CE51E20C523DEDCE979B9922A0294602D9D5C' + } + It 'Should call Find-Certificate mock' { + Assert-MockCalled -CommandName Find-Certificate -Times 1 + } + } + + Context 'Protocol is HTTPS and full CN of CertificateSubject is specified' { + $MockBindingInfo = @( + New-CimInstance -ClassName MSFT_xWebBindingInformation ` + -Namespace root/microsoft/Windows/DesiredStateConfiguration ` + -Property @{ + Protocol = 'https' + CertificateSubject = 'CN=TestCertificate' + } -ClientOnly + ) + Mock Find-Certificate -MockWith { + return [PSCustomObject]@{ + Thumbprint = 'C65CE51E20C523DEDCE979B9922A0294602D9D5C' + } + } + + It 'should not throw an error' { + { ConvertTo-WebBinding -InputObject $MockBindingInfo } | Should Not Throw + } + It 'should return the correct thumbprint' { + $Result = ConvertTo-WebBinding -InputObject $MockBindingInfo + $Result.certificateHash | Should Be 'C65CE51E20C523DEDCE979B9922A0294602D9D5C' + } + It 'Should call Find-Certificate mock' { + Assert-MockCalled -CommandName Find-Certificate -Times 1 + } + } + + Context 'Protocol is HTTPS and invalid CertificateSubject is specified' { + $MockBindingInfo = @( + New-CimInstance -ClassName MSFT_xWebBindingInformation ` + -Namespace root/microsoft/Windows/DesiredStateConfiguration ` + -Property @{ + Protocol = 'https' + CertificateSubject = 'TestCertificate' + CertificateStoreName = 'MY' + } -ClientOnly + ) + + Mock Find-Certificate + + It 'should throw the correct error' { + $CertificateSubject = "CN=$($MockBindingInfo.CertificateSubject)" + $ErrorId = 'WebBindingInvalidCertificateSubject' + $ErrorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument + $ErrorMessage = $LocalizedData.ErrorWebBindingInvalidCertificateSubject -f $CertificateSubject, $MockBindingInfo.CertificateStoreName + $Exception = New-Object ` + -TypeName System.InvalidOperationException ` + -ArgumentList $ErrorMessage + $ErrorRecord = New-Object ` + -TypeName System.Management.Automation.ErrorRecord ` + -ArgumentList $Exception, $ErrorId, $ErrorCategory, $null { ConvertTo-WebBinding -InputObject $MockBindingInfo } | Should Throw $ErrorRecord } } @@ -3238,8 +3322,8 @@ try Update-WebsiteBinding -Name $MockWebsite.Name -BindingInfo $MockBindingInfo - It 'should call all the mocks' { - Assert-Verifiablemocks + It 'Should call all the mocks' { + Assert-VerifiableMock Assert-MockCalled -CommandName Add-WebConfiguration -Exactly $MockBindingInfo.Count Assert-MockCalled -CommandName Set-WebConfigurationProperty } diff --git a/appveyor.yml b/appveyor.yml index b1c94a908..191deddc7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ install: - ps: | Import-Module -Name .\DscResource.Tests\TestHelper.psm1 -Force Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force - Install-Module -Name Pester -Repository PSGallery -MaximumVersion 3.4.3 -Force + Install-Module -Name Pester -Repository PSGallery -MaximumVersion 4.0.7 -Force Install-WindowsFeature -IncludeAllSubFeature -IncludeManagementTools -Name 'Web-Server' #---------------------------------# diff --git a/xWebAdministration.psd1 b/xWebAdministration.psd1 index c03ad7c8b..419031c92 100644 --- a/xWebAdministration.psd1 +++ b/xWebAdministration.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. -ModuleVersion = '1.18.0.0' +ModuleVersion = '1.19.0.0' # ID used to uniquely identify this module GUID = 'b3239f27-d7d3-4ae6-a5d2-d9a1c97d6ae4' @@ -41,10 +41,8 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '* Added sample for **xWebVirtualDirectory** for creating a new virtual directory. Bugfix for 195. -* Added integration tests for **xWebVirtualDirectory**. Fixes 188. -* xWebsite: - * Fixed bugs when setting log properties, fixes 299. + ReleaseNotes = '* **xWebAppPoolDefaults** now returns values. Fixes 311. +* Added unit tests for **xWebAppPoolDefaults**. Fixes 183. ' @@ -67,3 +65,4 @@ CmdletsToExport = '*' +