From ca45108e98e5952fb683d5c28f8058ecef456ba2 Mon Sep 17 00:00:00 2001 From: Mariah Breakey Date: Thu, 15 Dec 2016 19:08:38 -0800 Subject: [PATCH] adding script resource --- .../MSFT_ScriptResource.psm1 | 283 +++++++++++ .../MSFT_ScriptResource.schema.mof | 10 + .../en-US/MSFT_ScriptResource.schema.mfl | Bin 0 -> 1940 bytes .../en-US/MSFT_ScriptResource.strings.psd1 | 16 + Examples/Sample_Script.ps1 | 62 +++ PSDscResources.psd1 | 2 +- README.md | 27 ++ .../MSFT_ScriptResource.Integration.Tests.ps1 | 135 ++++++ ...SFT_ScriptResource_NoCredential.config.ps1 | 60 +++ ...T_ScriptResource_WithCredential.config.ps1 | 67 +++ Tests/Unit/MSFT_ScriptResource.Tests.ps1 | 455 ++++++++++++++++++ 11 files changed, 1116 insertions(+), 1 deletion(-) create mode 100644 DscResources/MSFT_ScriptResource/MSFT_ScriptResource.psm1 create mode 100644 DscResources/MSFT_ScriptResource/MSFT_ScriptResource.schema.mof create mode 100644 DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.schema.mfl create mode 100644 DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.strings.psd1 create mode 100644 Examples/Sample_Script.ps1 create mode 100644 Tests/Integration/MSFT_ScriptResource.Integration.Tests.ps1 create mode 100644 Tests/Integration/MSFT_ScriptResource_NoCredential.config.ps1 create mode 100644 Tests/Integration/MSFT_ScriptResource_WithCredential.config.ps1 create mode 100644 Tests/Unit/MSFT_ScriptResource.Tests.ps1 diff --git a/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.psm1 b/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.psm1 new file mode 100644 index 0000000..f18a809 --- /dev/null +++ b/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.psm1 @@ -0,0 +1,283 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +# Import CommonResourceHelper for Get-LocalizedData +$script:dscResourcesFolderFilePath = Split-Path $PSScriptRoot -Parent +$script:commonResourceHelperFilePath = Join-Path -Path $script:dscResourcesFolderFilePath -ChildPath 'CommonResourceHelper.psm1' +Import-Module -Name $script:commonResourceHelperFilePath + +# Localized messages for verbose and error statements in this resource +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_ScriptResource' + +<# + .SYNOPSIS + Runs the given get script. + Should return a hashtable. + + .PARAMETER GetScript + The script to retrieve the current state of the resource. + + .PARAMETER SetScript + Not used in Get-TargetResource. + + .PARAMETER TestScript + Not used in Get-TargetResource. + + .PARAMETER Credential + The Credential to run the get script under if needed. +#> +function Get-TargetResource +{ + [OutputType([Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.GetTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [ScriptBlock]::Create($GetScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.GetScriptThrewError -ErrorRecord $invokeScriptResult + } + + $invokeScriptResultAsHashTable = $invokeScriptResult -as [Hashtable] + + if ($null -eq $invokeScriptResultAsHashTable) + { + New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.GetScriptDidNotReturnHashtable + } + + Write-Verbose -Message $script:localizedData.GetTargetResourceEndVerboseMessage + + return $invokeScriptResultAsHashTable +} + +<# + .SYNOPSIS + Runs the given set script. + Should not return. + + .PARAMETER GetScript + Not used in Set-TargetResource. + + .PARAMETER SetScript + The script to set the resource to the desired state. + + .PARAMETER TestScript + Not used in Set-TargetResource. + + .PARAMETER Credential + The Credential to run the set script under if needed. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.SetTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [ScriptBlock]::Create($SetScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.SetScriptThrewError -ErrorRecord $invokeScriptResult + } + + Write-Verbose -Message $script:localizedData.SetTargetResourceEndVerboseMessage +} + +<# + .SYNOPSIS + Runs the given test script. + Should return true if the resource is in the desired state and false otherwise. + + .PARAMETER GetScript + Not used in Test-TargetResource. + + .PARAMETER SetScript + Not used in Test-TargetResource. + + .PARAMETER TestScript + The script to validate whether or not the resource is in the desired state. + + .PARAMETER Credential + The Credential to run the test script under if needed. +#> +function Test-TargetResource +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $GetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SetScript, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $TestScript, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Write-Verbose -Message $script:localizedData.TestTargetResourceStartVerboseMessage + + $invokeScriptParameters = @{ + ScriptBlock = [ScriptBlock]::Create($TestScript) + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeScriptParameters['Credential'] = $Credential + } + + $invokeScriptResult = Invoke-Script @invokeScriptParameters + + # If the script is returing multiple objects, then we consider the last object to be the result of the script execution. + if ($invokeScriptResult -is [Object[]] -and $invokeScriptResult.Count -gt 0) + { + $invokeScriptResult = $invokeScriptResult[$invokeScriptResult.Count - 1] + } + + if ($invokeScriptResult -is [System.Management.Automation.ErrorRecord]) + { + New-InvalidOperationException -Message $script:localizedData.TestScriptThrewError -ErrorRecord $invokeScriptResult + } + + if ($null -eq $invokeScriptResult -or -not ($invokeScriptResult -is [Boolean])) + { + New-InvalidArgumentException -ArgumentName 'TestScript' -Message $script:localizedData.TestScriptDidNotReturnBoolean + } + + Write-Verbose -Message $script:localizedData.TestTargetResourceEndVerboseMessage + + return $invokeScriptResult +} + +<# + .SYNOPSIS + Invokes the given script block. + + The output of the script will be returned unless the script throws an error. + If the script throws an error, the ErrorRecord will be returned rather than thrown. + + .PARAMETER ScriptBlock + The script block to invoke. + + .PARAMETER Credential + The credential to run the script under if needed. +#> +function Invoke-Script +{ + [OutputType([Object])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ScriptBlock] + $ScriptBlock, + + [Parameter()] + [ValidateNotNull()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + $scriptResult = $null + + try + { + Write-Verbose -Message ($script:localizedData.ExecutingScriptMessage -f $ScriptBlock) + + if ($null -ne $Credential) + { + $scriptResult = Invoke-Command -ScriptBlock $ScriptBlock -Credential $Credential -ComputerName . + } + else + { + $scriptResult = & $ScriptBlock + } + } + catch + { + # Surfacing the error thrown by the execution of the script + $scriptResult = $_ + } + + return $scriptResult +} diff --git a/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.schema.mof b/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.schema.mof new file mode 100644 index 0000000..f4508a3 --- /dev/null +++ b/DscResources/MSFT_ScriptResource/MSFT_ScriptResource.schema.mof @@ -0,0 +1,10 @@ + +[ClassVersion("1.0.0"),FriendlyName("Script")] +class MSFT_ScriptResource : OMI_BaseResource +{ + [Key, Description("A string that can be used to create a PowerShell script block that retrieves the current state of the resource.")] String GetScript; + [Key, Description("A string that can be used to create a PowerShell script block that sets the resource to the desired state.")] String SetScript; + [Key, Description("A string that can be used to create a PowerShell script block that validates whether or not the resource is in the desired state.")] String TestScript; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The credential of the user account to run the script under if needed.")] String Credential; + [Read, Description("The result from the GetScript script block.")] String Result; +}; diff --git a/DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.schema.mfl b/DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.schema.mfl new file mode 100644 index 0000000000000000000000000000000000000000..05c843da6885dd66cb26e294a14b56c32ddc9b2d GIT binary patch literal 1940 zcmds&OK;Oq5QS%r#DBQz3Z$xH!3J1`77(IPRA*OEWo(y3Bo6YURfPEKz<2J%I8JJ$ zN^C$@;`=y0Gjrz7+@C)#?8FMIEwjqz_SG8xS612sJFuzEnAH~86J}*Cql|IbthcNd zmNReI4SZjKxMEz{lKCsH!=H(rvM0Tk@Lt+udt^@{*GHJ{+dXV8@VMp)e4p4$qQB(b z`*4n(p}z9s4D@S|&*RsH?V`l}Em{bm^<3B+nAXv|@w`qzN_%CzL?>4*WhLN~G8F6+ zJOLGl`IW~7h$il)AbNe15q-tAAPbp&0k2%TB{8`VgIV_otFDrd*4)j^{Bt2c-J}qr1^O5Ph&i)bNMWf zRAKWNxuV;yEw6sXn}W~SRj2m2b&{$$@8hc@tKTB(9o|3n{XFuPSaX{>J$J0z?$_z@ zExztn>N(75?A5v_)D!ydI=s=;*F0Y>*;X`H%vbTg>hihY8HRVpc2B!YBqu0Qb4%(z QSaYAG@t*NmzMH>)0YLXog8%>k literal 0 HcmV?d00001 diff --git a/DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.strings.psd1 b/DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.strings.psd1 new file mode 100644 index 0000000..8a25d8d --- /dev/null +++ b/DscResources/MSFT_ScriptResource/en-US/MSFT_ScriptResource.strings.psd1 @@ -0,0 +1,16 @@ +# Localized MSFT_ScriptResource.strings.psd1 + +ConvertFrom-StringData @' + GetTargetResourceStartVerboseMessage = Begin executing get script. + GetScriptThrewError = The get script threw an error. + GetScriptDidNotReturnHashtable = The get script did not return a hashtable. + GetTargetResourceEndVerboseMessage = End executing get script. + SetTargetResourceStartVerboseMessage = Begin executing set script. + SetScriptThrewError = The set script threw an error. + SetTargetResourceEndVerboseMessage = End executing set script. + TestTargetResourceStartVerboseMessage = Begin executing test script. + TestScriptThrewError = The test script threw an error. + TestScriptDidNotReturnBoolean = The test script did not return a boolean. + TestTargetResourceEndVerboseMessage = End executing test script. + ExecutingScriptMessage = Executing script: {0} +'@ diff --git a/Examples/Sample_Script.ps1 b/Examples/Sample_Script.ps1 new file mode 100644 index 0000000..caea309 --- /dev/null +++ b/Examples/Sample_Script.ps1 @@ -0,0 +1,62 @@ +<# + .SYNOPSIS + Creates a file at the given file path with the specified content through the Script resource. + + .PARAMETER FilePath + The path at which to create the file. + + .PARAMETER FileContent + The content to set for the new file. +#> +Configuration ScriptExample { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FilePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FileContent + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Script ScriptExample + { + SetScript = { + $streamWriter = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $using:FilePath ) + $streamWriter.WriteLine($using:FileContent) + $streamWriter.Close() + } + TestScript = { + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + return $fileContent -eq $using:FileContent + } + else + { + return $false + } + } + GetScript = { + $fileContent = $null + + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + } + + return @{ + Result = Get-Content -Path $fileContent + } + } + } + } +} diff --git a/PSDscResources.psd1 b/PSDscResources.psd1 index 93c55d4..86c48fb 100644 --- a/PSDscResources.psd1 +++ b/PSDscResources.psd1 @@ -73,7 +73,7 @@ VariablesToExport = '*' AliasesToExport = @() # DSC resources to export from this module -DscResourcesToExport = 'Group', 'Service', 'User', 'WindowsFeature', 'WindowsOptionalFeature', 'WindowsPackageCab', 'WindowsProcess' +DscResourcesToExport = 'Group', 'Script', 'Service', 'User', 'WindowsFeature', 'WindowsOptionalFeature', 'WindowsPackageCab', 'WindowsProcess' # List of all modules packaged with this module # ModuleList = @() diff --git a/README.md b/README.md index 58d35a3..b5f9985 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Please check out the common DSC Resources [contributing guidelines](https://gith ## Resources * [Group](#group): Provides a mechanism to manage local groups on a target node. +* [Script](#script): Provides a mechanism to run PowerShell script blocks on a target node. * [Service](#service): Provides a mechanism to configure and manage Windows services on a target node. * [User](#user): Provides a mechanism to manage local users on a target node. * [WindowsFeature](#windowsfeature): Provides a mechanism to install or uninstall Windows roles or features on a target node. @@ -45,6 +46,7 @@ Please check out the common DSC Resources [contributing guidelines](https://gith ### Resources that Work on Nano Server * [Group](#group) +* [Script](#script) * [Service](#service) * [User](#user) * [WindowsOptionalFeature](#windowsoptionalfeature) @@ -78,6 +80,30 @@ None * [Set members of a group](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Group_SetMembers.ps1) * [Remove members of a group](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Group_RemoveMembers.ps1) +### Script + +Provides a mechanism to run PowerShell script blocks on a target node. +This resource works on Nano Server. + +#### Requirements + +None + +#### Parameters + +* **[String] GetScript** _(Key)_: A string that can be used to create a PowerShell script block that retrieves the current state of the resource. This script block runs when the Get-DscConfiguration cmdlet is called. This script block should return a hash table containing one key named Result with a string value. +* **[String] SetScript** _(Key)_: A string that can be used to create a PowerShell script block that sets the resource to the desired state. This script block runs conditionally when the Start-DscConfiguration cmdlet is called. The TestScript script block will run first. If the TestScript block returns False, this script block will run. If the TestScript block returns True, this script block will not run. This script block should not return. +* **[String] TestScript** _(Key)_: A string that can be used to create a PowerShell script block that validates whether or not the resource is in the desired state. This script block runs when the Start-DscConfiguration cmdlet is called or when the Test-DscConfiguration cmdlet is called. This script block should return a boolean with True meaning that the resource is in the desired state and False meaning that the resource is not in the desired state. +* **[PSCredential] Credential** _(Write)_: The credential of the user account to run the script under if needed. + +#### Read-Only Properties from Get-TargetResource + +* **[String] Result** _(Read)_: The result from the GetScript script block. + +#### Examples + +* [Create a file with content through xScript](https://github.com/PowerShell/PSDscResources/blob/master/Examples/Sample_Script.ps1) + ### Service Provides a mechanism to configure and manage Windows services on a target node. @@ -273,6 +299,7 @@ None * Updated resource module, unit tests, integration tests, and examples to reflect the changes made in xPSDesiredStateConfiguration. * Group: * Updated resource module, examples, and integration tests to reflect the changes made in xPSDesiredStateConfiguration. +* Added Script ### 2.1.0.0 diff --git a/Tests/Integration/MSFT_ScriptResource.Integration.Tests.ps1 b/Tests/Integration/MSFT_ScriptResource.Integration.Tests.ps1 new file mode 100644 index 0000000..c9586a5 --- /dev/null +++ b/Tests/Integration/MSFT_ScriptResource.Integration.Tests.ps1 @@ -0,0 +1,135 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +if ($PSVersionTable.PSVersion.Major -lt 5 -or $PSVersionTable.PSVersion.Minor -lt 1) +{ + Write-Warning -Message 'Cannot run PSDscResources integration tests on PowerShell versions lower than 5.1' + return +} + +# Import CommonTestHelper for Enter-DscResourceTestEnvironment, Exit-DscResourceTestEnvironment +$script:moduleRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent +$script:testPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:testPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'PSDscResources' ` + -DscResourceName 'MSFT_ScriptResource' ` + -TestType 'Integration' + +try +{ + Describe 'Script Integration Tests' { + BeforeAll { + # Import Script module for Get-TargetResource, Test-TargetResource + $dscResourcesFolderFilePath = Join-Path -Path $script:moduleRootPath -ChildPath 'DscResources' + $scriptResourceFolderFilePath = Join-Path -Path $dscResourcesFolderFilePath -ChildPath 'MSFT_ScriptResource' + $scriptResourceModuleFilePath = Join-Path -Path $scriptResourceFolderFilePath -ChildPath 'MSFT_ScriptResource.psm1' + Import-Module -Name $scriptResourceModuleFilePath + + $script:configurationNoCredentialFilePath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_ScriptResource_NoCredential.config.ps1' + $script:configurationWithCredentialFilePath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_ScriptResource_WithCredential.config.ps1' + + # Cannot use $TestDrive here because script is run outside of Pester + $script:testFilePath = Join-Path -Path $env:SystemDrive -ChildPath 'TestFile.txt' + + if (Test-Path -Path $script:testFilePath) + { + Remove-Item -Path $script:testFilePath -Force + } + } + + AfterAll { + if (Test-Path -Path $script:testFilePath) + { + Remove-Item -Path $script:testFilePath -Force + } + } + + Context 'Get, set, and test scripts specified and Credential not specified' { + if (Test-Path -Path $script:testFilePath) + { + Remove-Item -Path $script:testFilePath -Force + } + + $configurationName = 'TestScriptNoCredential' + + # Cannot use $TestDrive here because script is run outside of Pester + $resourceParameters = @{ + FilePath = $script:testFilePath + FileContent = 'Test file content' + } + + It 'Should have removed test file before config runs' { + Test-Path -Path $resourceParameters.FilePath | Should Be $false + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationNoCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should have created the test file' { + Test-Path -Path $resourceParameters.FilePath | Should Be $true + } + + It 'Should have set file content correctly' { + Get-Content -Path $resourceParameters.FilePath -Raw | Should Be "$($resourceParameters.FileContent)`r`n" + } + } + + Context 'Get, set, and test scripts specified and Credential specified' { + if (Test-Path -Path $script:testFilePath) + { + Remove-Item -Path $script:testFilePath -Force + } + + $configurationName = 'TestScriptWithCredential' + + # Cannot use $TestDrive here because script is run outside of Pester + $resourceParameters = @{ + FilePath = $script:testFilePath + FileContent = 'Test file content' + Credential = Get-AppVeyorAdministratorCredential + } + + It 'Should have removed test file before config runs' { + Test-Path -Path $resourceParameters.FilePath | Should Be $false + } + + $configData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + PSDscAllowPlainTextPassword = $true + PSDscAllowDomainUser = $true + } + ) + } + + It 'Should compile and apply the MOF without throwing' { + { + . $script:configurationWithCredentialFilePath -ConfigurationName $configurationName + & $configurationName -OutputPath $TestDrive -ConfigurationData $configData @resourceParameters + Start-DscConfiguration -Path $TestDrive -ErrorAction 'Stop' -Wait -Force + } | Should Not Throw + } + + It 'Should have created the test file' { + Test-Path -Path $resourceParameters.FilePath | Should Be $true + } + + It 'Should have set file content correctly' { + Get-Content -Path $resourceParameters.FilePath -Raw | Should Be "$($resourceParameters.FileContent)`r`n" + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} diff --git a/Tests/Integration/MSFT_ScriptResource_NoCredential.config.ps1 b/Tests/Integration/MSFT_ScriptResource_NoCredential.config.ps1 new file mode 100644 index 0000000..7c33181 --- /dev/null +++ b/Tests/Integration/MSFT_ScriptResource_NoCredential.config.ps1 @@ -0,0 +1,60 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FilePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FileContent + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Script ScriptExample + { + SetScript = { + $streamWriter = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $using:FilePath ) + $streamWriter.WriteLine($using:FileContent) + $streamWriter.Close() + } + TestScript = { + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + return $fileContent -eq $using:FileContent + } + else + { + return $false + } + } + GetScript = { + $fileContent = $null + + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + } + + return @{ + Result = Get-Content -Path $fileContent + } + } + } + } +} diff --git a/Tests/Integration/MSFT_ScriptResource_WithCredential.config.ps1 b/Tests/Integration/MSFT_ScriptResource_WithCredential.config.ps1 new file mode 100644 index 0000000..0f9f066 --- /dev/null +++ b/Tests/Integration/MSFT_ScriptResource_WithCredential.config.ps1 @@ -0,0 +1,67 @@ +param +( + [Parameter(Mandatory = $true)] + [String] + $ConfigurationName +) + +Configuration $ConfigurationName +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FilePath, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FileContent, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential + ) + + Import-DscResource -ModuleName 'PSDscResources' + + Node localhost + { + Script Script1 + { + SetScript = { + $streamWriter = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $using:FilePath ) + $streamWriter.WriteLine($using:FileContent) + $streamWriter.Close() + } + TestScript = { + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + return $fileContent -eq $using:FileContent + } + else + { + return $false + } + } + GetScript = { + $fileContent = $null + + if (Test-Path -Path $using:FilePath) + { + $fileContent = Get-Content -Path $using:filePath -Raw + } + + return @{ + Result = Get-Content -Path $fileContent + } + } + Credential = $Credential + } + } +} diff --git a/Tests/Unit/MSFT_ScriptResource.Tests.ps1 b/Tests/Unit/MSFT_ScriptResource.Tests.ps1 new file mode 100644 index 0000000..741b112 --- /dev/null +++ b/Tests/Unit/MSFT_ScriptResource.Tests.ps1 @@ -0,0 +1,455 @@ +$errorActionPreference = 'Stop' +Set-StrictMode -Version 'Latest' + +# Import CommonTestHelper for Enter-DscResourceTestEnvironment, Exit-DscResourceTestEnvironment +$script:moduleRootPath = Split-Path -Path $PSScriptRoot -Parent +$script:testHelpersPath = Join-Path -Path $script:moduleRootPath -ChildPath 'TestHelpers' +Import-Module -Name (Join-Path -Path $script:testHelpersPath -ChildPath 'CommonTestHelper.psm1') + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DSCResourceModuleName 'PSDscResources' ` + -DSCResourceName 'MSFT_ScriptResource' ` + -TestType 'Unit' + +try +{ + InModuleScope 'MSFT_ScriptResource' { + $testUsername = 'TestUsername' + $testPassword = 'TestPassword' + $secureTestPassword = ConvertTo-SecureString -String $testPassword -AsPlainText -Force + + $script:testCredenital = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( $testUsername, $secureTestPassword ) + + Describe 'Script\Get-TargetResource' { + Mock -CommandName 'Invoke-Script' -MockWith { } + + Context 'Specified get script returns null' { + $getTargetResourceParameters = @{ + GetScript = 'return $null' + TestScript = 'NotUsed' + SetScript = 'NotUsed' + } + + It 'Should throw an error for malformed get script' { + $errorMessage = $script:localizedData.GetScriptDidNotReturnHashtable + { $null = Get-TargetResource @getTargetResourceParameters } | Should Throw $errorMessage + } + } + + Mock -CommandName 'Invoke-Script' -MockWith { return "String" } + + Context 'Specified get script returns a string' { + $getTargetResourceParameters = @{ + GetScript = 'return "String"' + TestScript = 'NotUsed' + SetScript = 'NotUsed' + } + + It 'Should throw an error for malformed get script' { + $errorMessage = $script:localizedData.GetScriptDidNotReturnHashtable + { $null = Get-TargetResource @getTargetResourceParameters } | Should Throw $errorMessage + } + } + + $testException = New-Object -TypeName 'System.Exception' -ArgumentList @() + $newErrorRecoredArguments = @( $testException, 'Test', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) + $testErrorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $newErrorRecoredArguments + + Mock -CommandName 'Invoke-Script' -MockWith { return $testErrorRecord } + + Context 'Specified get script throws an error' { + $getTargetResourceParameters = @{ + GetScript = 'throw "Error"' + TestScript = 'NotUsed' + SetScript = 'NotUsed' + } + + It 'Should throw error from get script' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Throw $testErrorRecord + } + } + + $testScriptResult = @{ TestResult = 'Value1' } + Mock -CommandName 'Invoke-Script' -MockWith { return $testScriptResult } + + Context 'Specified get script returns a hashtable and Credential not specified' { + $getTargetResourceParameters = @{ + GetScript = 'return "something"' + TestScript = 'NotUsed' + SetScript = 'NotUsed' + } + + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script' { + $expectedScriptBlock = [ScriptBlock]::Create($getTargetResourceParameters.GetScript) + + $null = Get-TargetResource @getTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + return $scriptBlockParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return a hashtable' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the output from the specified get script' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + Compare-Object -ReferenceObject $testScriptResult -DifferenceObject $getTargetResourceResult | Should Be $null + } + } + + Context 'Specified get script returns a hashtable and Credential specified' { + $getTargetResourceParameters = @{ + GetScript = 'return "something"' + TestScript = 'NotUsed' + SetScript = 'NotUsed' + Credential = $script:testCredenital + } + + It 'Should not throw' { + { $null = Get-TargetResource @getTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script with the specified Credential' { + $expectedScriptBlock = [ScriptBlock]::Create($getTargetResourceParameters.GetScript) + + $null = Get-TargetResource @getTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + $credentialParameterCorrect = $null -eq (Compare-Object -ReferenceObject $getTargetResourceParameters.Credential -DifferenceObject $Credential) + + return $scriptBlockParameterCorrect -and $credentialParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return a hashtable' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + $getTargetResourceResult -is [Hashtable] | Should Be $true + } + + It 'Should return the output from the specified get script' { + $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters + Compare-Object -ReferenceObject $testScriptResult -DifferenceObject $getTargetResourceResult | Should Be $null + } + } + } + + Describe 'Script\Set-TargetResource' { + Mock -CommandName 'Invoke-Script' -MockWith { } + + Context 'Specified set script returns correctly and Credential not specified' { + $setTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'NotUsed' + SetScript = '$assignedVariable = "Value1"' + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script' { + $expectedScriptBlock = [ScriptBlock]::Create($setTargetResourceParameters.SetScript) + + Set-TargetResource @setTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + return $scriptBlockParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + } + + Context 'Specified set script returns correctly and Credential specified' { + $setTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'NotUsed' + SetScript = '$assignedVariable = "Value1"' + Credential = $script:testCredenital + } + + It 'Should not throw' { + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script with specified Credential' { + $expectedScriptBlock = [ScriptBlock]::Create($setTargetResourceParameters.SetScript) + + Set-TargetResource @setTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + $credentialParameterCorrect = $null -eq (Compare-Object -ReferenceObject $setTargetResourceParameters.Credential -DifferenceObject $Credential) + + return $scriptBlockParameterCorrect -and $credentialParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + } + + $testException = New-Object -TypeName 'System.Exception' -ArgumentList @() + $newErrorRecoredArguments = @( $testException, 'Test', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) + $testErrorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $newErrorRecoredArguments + + Mock -CommandName 'Invoke-Script' -MockWith { return $testErrorRecord } + + Context 'Specified set script returns an error' { + $setTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'NotUsed' + SetScript = 'throw "Error"' + } + + It 'Should throw error from set script' { + { Set-TargetResource @setTargetResourceParameters } | Should Throw $testErrorRecord + } + } + } + + Describe 'Script\Test-TargetResource' { + Mock -CommandName 'Invoke-Script' -MockWith { } + + Context 'Specified test script returns null' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'return $null' + SetScript = 'NotUsed' + } + + It 'Should throw an error for malformed test script' { + $errorMessage = $script:localizedData.TestScriptDidNotReturnBoolean + { $null = Test-TargetResource @testTargetResourceParameters } | Should Throw $errorMessage + } + } + + $testException = New-Object -TypeName 'System.Exception' -ArgumentList @() + $newErrorRecoredArguments = @( $testException, 'Test', [System.Management.Automation.ErrorCategory]::InvalidOperation, $null ) + $testErrorRecord = New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList $newErrorRecoredArguments + + Mock -CommandName 'Invoke-Script' -MockWith { return $testErrorRecord } + + Context 'Specified test script returns an error' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'throw "Error"' + SetScript = 'NotUsed' + } + + It 'Should throw error from test script' { + { $null = Test-TargetResource @testTargetResourceParameters } | Should Throw $testErrorRecord + } + } + + $expectedBoolean = $true + Mock -CommandName 'Invoke-Script' -MockWith { return $expectedBoolean } + + Context 'Specified test script returns one boolean and Credential not specified' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'return $true' + SetScript = 'NotUsed' + } + + It 'Should not throw' { + { $null = Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script' { + $expectedScriptBlock = [ScriptBlock]::Create($testTargetResourceParameters.TestScript) + + $null = Test-TargetResource @testTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + return $scriptBlockParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return the expected boolean' { + $testTargetResourceResult = Test-TargetResource @testTargetResourceParameters + $testTargetResourceResult | Should Be $expectedBoolean + } + } + + Context 'Specified test script returns one boolean and Credential specified' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'return $true' + SetScript = 'NotUsed' + Credential = $script:testCredenital + } + + It 'Should not throw' { + { $null = Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script with specified Credential' { + $expectedScriptBlock = [ScriptBlock]::Create($testTargetResourceParameters.TestScript) + + $null = Test-TargetResource @testTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + $credentialParameterCorrect = $null -eq (Compare-Object -ReferenceObject $testTargetResourceParameters.Credential -DifferenceObject $Credential) + + return $scriptBlockParameterCorrect -and $credentialParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return the expected boolean' { + $testTargetResourceResult = Test-TargetResource @testTargetResourceParameters + $testTargetResourceResult | Should Be $expectedBoolean + } + } + + $expectedBoolean = $false + Mock -CommandName 'Invoke-Script' -MockWith { return @( (-not $expectedBoolean), $expectedBoolean ) } + + Context 'Specified test script returns multiple booleans' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'return $true, $false' + SetScript = 'NotUsed' + } + + It 'Should not throw' { + { $null = Test-TargetResource @testTargetResourceParameters } | Should Not Throw + } + + It 'Should use script execution helper to run script' { + $expectedScriptBlock = [ScriptBlock]::Create($testTargetResourceParameters.TestScript) + + $null = Test-TargetResource @testTargetResourceParameters + + $invokeScriptParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $expectedScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + return $scriptBlockParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Script' -ParameterFilter $invokeScriptParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return the expected boolean' { + $testTargetResourceResult = Test-TargetResource @testTargetResourceParameters + $testTargetResourceResult | Should Be $expectedBoolean + } + } + + Mock -CommandName 'Invoke-Script' -MockWith { return 'Value1' } + + Context 'Specified test script returns a string' { + $testTargetResourceParameters = @{ + GetScript = 'NotUsed' + TestScript = 'return "MyString"' + SetScript = 'NotUsed' + } + + It 'Should throw an error for malformed test script' { + $errorMessage = $script:localizedData.TestScriptDidNotReturnBoolean + { $null = Test-TargetResource @testTargetResourceParameters } | Should Throw $errorMessage + } + } + } + + Describe 'Script\Invoke-Script' { + Mock -CommandName 'Invoke-Command' -MockWith { } + + Context 'Specified script throws an error' { + $testErrorMessage = 'Script execution helper test error message' + + $scriptExecutionHelperParameters = @{ + ScriptBlock = { throw $testErrorMessage } + } + + It 'Should not throw' { + { $null = Invoke-Script @scriptExecutionHelperParameters } | Should Not Throw + } + + It 'Should return an error record' { + $scriptExecutionHelperResult = Invoke-Script @scriptExecutionHelperParameters + $scriptExecutionHelperResult -is [System.Management.Automation.ErrorRecord] | Should Be $true + } + + It 'Should return an error record' { + $scriptExecutionHelperResult = Invoke-Script @scriptExecutionHelperParameters + $scriptExecutionHelperResult -is [System.Management.Automation.ErrorRecord] | Should Be $true + } + + It 'Should return error with expected message from script' { + $scriptExecutionHelperResult = Invoke-Script @scriptExecutionHelperParameters + $scriptExecutionHelperResult.Exception.Message | Should Be $testErrorMessage + } + } + + Context 'Specified script returns nothing and Credential specified' { + $scriptExecutionHelperParameters = @{ + ScriptBlock = { return $null } + Credential = $script:testCredenital + } + + It 'Should not throw' { + { $null = Invoke-Script @scriptExecutionHelperParameters } | Should Not Throw + } + + It 'Should run script through Invoke-Command using the specified Credential' { + $null = Invoke-Script @scriptExecutionHelperParameters + + $invokeCommandParameterFilter = { + $scriptBlockParameterCorrect = $null -eq (Compare-Object -ReferenceObject $scriptExecutionHelperParameters.ScriptBlock.Ast -DifferenceObject $ScriptBlock.Ast) + $credentialParameterCorrect = $null -eq (Compare-Object -ReferenceObject $scriptExecutionHelperParameters.Credential -DifferenceObject $Credential) + + return $scriptBlockParameterCorrect -and $credentialParameterCorrect + } + + Assert-MockCalled -CommandName 'Invoke-Command' -ParameterFilter $invokeCommandParameterFilter -Times 1 -Scope 'It' + } + + It 'Should return nothing' { + $scriptExecutionHelperResult = Invoke-Script @scriptExecutionHelperParameters + $scriptExecutionHelperResult | Should Be $null + } + } + + Context 'Specified script returns a result and Credential not specified' { + $testScriptResult = 'Script result' + + $scriptExecutionHelperParameters = @{ + ScriptBlock = { return $testScriptResult } + } + + It 'Should not run script through Invoke-Command' { + $null = Invoke-Script @scriptExecutionHelperParameters + Assert-MockCalled -CommandName 'Invoke-Command' -Times 0 -Scope 'It' + } + + It 'Should return result of script' { + $scriptExecutionHelperResult = Invoke-Script @scriptExecutionHelperParameters + $scriptExecutionHelperResult | Should Be $testScriptResult + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +}