diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d17d8..33df562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ - Refactor Test-TargetResource to return $false in all DSC resource - Fixes [Issue #12](https://github.com/PlagueHO/FileContentDsc/issues/13). - Correct configuration names in Examples - fixes [Issue #15](https://github.com/PowerShell/FileContentDsc/issues/15). +- Refactor Test/Set-TargetResource in ReplaceText to be able to add a key if it + doesn't exist but should -Fixes + [Issue#20](https://github.com/PlagueHO/FileContentDsc/issues/20). ## 1.0.0.0 diff --git a/DSCResources/DSR_ReplaceText/DSR_ReplaceText.psm1 b/DSCResources/DSR_ReplaceText/DSR_ReplaceText.psm1 index e39958d..96d6e90 100644 --- a/DSCResources/DSR_ReplaceText/DSR_ReplaceText.psm1 +++ b/DSCResources/DSR_ReplaceText/DSR_ReplaceText.psm1 @@ -103,6 +103,9 @@ function Get-TargetResource .PARAMETER Secret The secret text to replace the text identified by the RegEx. Only used when Type is set to 'Secret'. + + .PARAMETER AllowAppend + Specifies to append text to the file being modified. Adds the ability to add a configuration entry. #> function Set-TargetResource { @@ -133,7 +136,11 @@ function Set-TargetResource [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] - $Secret + $Secret, + + [Parameter()] + [System.Boolean] + $AllowAppend = $false ) Assert-ParametersValid @PSBoundParameters @@ -155,10 +162,17 @@ function Set-TargetResource if ($null -eq $fileContent) { + # Configuration file does not exist $fileContent = $Text } + elseif ([regex]::Matches($fileContent, $Search).Count -eq 0 -and $AllowAppend -eq $true) + { + # Configuration file exists but Text does not exist so lets add it + $fileContent = Add-ConfigurationEntry -FileContent $fileContent -Text $Text + } else { + # Configuration file exists but Text not in a desired state so lets update it $fileContent = $fileContent -Replace $Search, $Text } @@ -189,6 +203,9 @@ function Set-TargetResource .PARAMETER Secret The secret text to replace the text identified by the RegEx. Only used when Type is set to 'Secret'. + + .PARAMETER AllowAppend + Specifies to append text to the file being modified. Adds the ability to add a configuration entry. #> function Test-TargetResource { @@ -218,7 +235,11 @@ function Test-TargetResource [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] - $Secret + $Secret, + + [Parameter()] + [System.Boolean] + $AllowAppend = $false ) Assert-ParametersValid @PSBoundParameters @@ -239,6 +260,15 @@ function Test-TargetResource if ($results.Count -eq 0) { + if ($AllowAppend -eq $true) + { + # No matches found - but we want to append + Write-Verbose -Message ($localizedData.StringNotFoundMessageAppend -f ` + $Path, $Search) + + return $false + } + # No matches found - already in state Write-Verbose -Message ($localizedData.StringNotFoundMessage -f ` $Path, $Search) @@ -325,7 +355,11 @@ function Assert-ParametersValid [Parameter()] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] - $Secret + $Secret, + + [Parameter()] + [System.Boolean] + $AllowAppend = $false ) # Does the file's parent path exist? @@ -338,4 +372,49 @@ function Assert-ParametersValid } # if } +<# + .SYNOPSIS + Uses the stringBuilder class to append a configuration entry to the existing file content. + + .PARAMETER FileContent + The existing file content of the configuration file. + + .PARAMETER Text + The text to append to the end of the FileContent. +#> +function Add-ConfigurationEntry +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $FileContent, + + [Parameter(Mandatory = $true)] + [String] + $Text + ) + + if ($FileContent -match '\n$' -and $FileContent -notmatch '\r\n$') + { + # default *nix line ending + $detectedNewLineFormat = "`n" + } + else + { + # default Windows line ending + $detectedNewLineFormat = "`r`n" + } + + $stringBuilder = New-Object -TypeName System.Text.StringBuilder + + $null = $stringBuilder.Append($FileContent) + $null = $stringBuilder.Append($Text) + $null = $stringBuilder.Append($detectedNewLineFormat) + + return $stringBuilder.ToString() +} + Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/DSR_ReplaceText/DSR_ReplaceText.schema.mof b/DSCResources/DSR_ReplaceText/DSR_ReplaceText.schema.mof index 7b37409..c30aaa4 100644 --- a/DSCResources/DSR_ReplaceText/DSR_ReplaceText.schema.mof +++ b/DSCResources/DSR_ReplaceText/DSR_ReplaceText.schema.mof @@ -5,5 +5,6 @@ class DSR_ReplaceText : OMI_BaseResource [Key, Description("The RegEx string to use to search the text file.")] String Search; [Write, Description("Specifies the value type to use as the replacement string. Defaults to 'Text'."),ValueMap{"Text", "Secret"},Values{"Text", "Secret"}] String Type; [Write, Description("The text to replace the text identified by the RegEx. Only used when Type is set to 'Text'.")] String Text; - [write, Description("The secret text to replace the text identified by the RegEx. Only used when Type is set to 'Secret'."),EmbeddedInstance("MSFT_Credential")] String Secret; + [Write, Description("The secret text to replace the text identified by the RegEx. Only used when Type is set to 'Secret'."),EmbeddedInstance("MSFT_Credential")] String Secret; + [Write, Description("Specifies to append text to the file being modified. Adds the ability to add a configuration entry.")] Boolean AllowAppend; }; diff --git a/DSCResources/DSR_ReplaceText/en-US/DSR_ReplaceText.strings.psd1 b/DSCResources/DSR_ReplaceText/en-US/DSR_ReplaceText.strings.psd1 index 85dca92..98ee192 100644 --- a/DSCResources/DSR_ReplaceText/en-US/DSR_ReplaceText.strings.psd1 +++ b/DSCResources/DSR_ReplaceText/en-US/DSR_ReplaceText.strings.psd1 @@ -2,6 +2,7 @@ ConvertFrom-StringData @' SearchForTextMessage = Searching using RegEx '{1}' in file '{0}'. + StringNotFoundMessageAppend = String not found using RegEx '{1}' in file '{0}', change required. StringNotFoundMessage = String not found using RegEx '{1}' in file '{0}', change not required. StringMatchFoundMessage = String(s) '{2}' found using RegEx '{1}' in file '{0}'. StringReplacementRequiredMessage = String found using RegEx '{1}' in file '{0}', replacement required. diff --git a/TestFile.txt b/TestFile.txt new file mode 100644 index 0000000..2d9e0de --- /dev/null +++ b/TestFile.txt @@ -0,0 +1,25 @@ +Setting1=Value1 +Setting.Two='Value2' +Setting.Two='Value3' +Setting.Two='TestText' +Setting3.Test=Value4 + +Setting.NotExist='TestText'Setting1=Value1 +Setting.Two='Value2' +Setting.Two='Value3' +Setting.Two='TestText' +Setting3.Test=Value4 + +Setting.NotExist='TestText'Setting1=Value1 +Setting.Two='Value2' +Setting.Two='Value3' +Setting.Two='TestText' +Setting3.Test=Value4 + +Setting.NotExist='TestText'Setting1=Value1 +Setting.Two='Value2' +Setting.Two='Value3' +Setting.Two='TestText' +Setting3.Test=Value4 + +Setting.NotExist='TestText' \ No newline at end of file diff --git a/Tests/Unit/DSR_ReplaceText.Tests.ps1 b/Tests/Unit/DSR_ReplaceText.Tests.ps1 index b993dee..cd8c723 100644 --- a/Tests/Unit/DSR_ReplaceText.Tests.ps1 +++ b/Tests/Unit/DSR_ReplaceText.Tests.ps1 @@ -36,6 +36,7 @@ try $script:testSecret = 'TestSecret' $script:testSearch = "Setting\.Two='(.)*'" $script:testSearchNoFind = "Setting.NotExist='(.)*'" + $script:testTextReplaceNoFind = "Setting.NotExist='$($script:testText)'" $script:testTextReplace = "Setting.Two='$($script:testText)'" $script:testSecretReplace = "Setting.Two='$($script:testSecret)'" $script:testSecureSecretReplace = ConvertTo-SecureString -String $script:testSecretReplace -AsPlainText -Force @@ -57,6 +58,16 @@ Setting.Two='$($script:testText)' Setting.Two='$($script:testText)' Setting3.Test=Value4 +"@ + + $script:testFileExpectedTextContentNewKey = @" +Setting1=Value1 +Setting.Two='Value2' +Setting.Two='Value3' +Setting.Two='$($script:testText)' +Setting3.Test=Value4 +Setting.NotExist='$($script:testText)' + "@ $script:testFileExpectedSecretContent = @" @@ -88,9 +99,9 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Get-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Verbose } | Should -Not -Throw } @@ -129,9 +140,9 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Get-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearchNoFind ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Verbose } | Should -Not -Throw } @@ -173,17 +184,67 @@ Setting3.Test=Value4 Mock ` -CommandName Set-Content ` -ParameterFilter { + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileExpectedTextContent) + } ` + -Verifiable + + It 'Should not throw an exception' { + { Set-TargetResource ` + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Text $script:testTextReplace ` + -Verbose + } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Assert-ParametersValid -Exactly 1 + + Assert-MockCalled ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -Exactly 1 + + Assert-MockCalled ` + -CommandName Set-Content ` + -ParameterFilter { ($path -eq $script:testTextFile) -and ` ($value -eq $script:testFileExpectedTextContent) } ` + -Exactly 1 + } + } + + Context 'File exists search text can not be found and AllowAppend is TRUE' { + # verifiable (should be called) mocks + Mock ` + -CommandName Assert-ParametersValid ` + -ModuleName 'DSR_ReplaceText' ` + -Verifiable + + Mock ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -MockWith { $script:testFileContent } ` + -Verifiable + + Mock ` + -CommandName Set-Content ` + -ParameterFilter { + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileExpectedTextContentNewKey) + } ` -Verifiable It 'Should not throw an exception' { - { Set-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Text $script:testTextReplace ` - -Verbose + { $script:result = Set-TargetResource ` + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Text $script:testTextReplaceNoFind ` + -AllowAppend $true ` + -Verbose } | Should -Not -Throw } @@ -199,15 +260,14 @@ Setting3.Test=Value4 Assert-MockCalled ` -CommandName Set-Content ` -ParameterFilter { - ($path -eq $script:testTextFile) -and ` - ($value -eq $script:testFileExpectedTextContent) - } ` + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileExpectedTextContentNewKey) + } ` -Exactly 1 } } - Context 'File exists and search secret can be found' { - # verifiable (should be called) mocks + Context 'File exists search text can not be found and AllowAppend is FALSE' { Mock ` -CommandName Assert-ParametersValid ` -ModuleName 'DSR_ReplaceText' ` @@ -222,18 +282,68 @@ Setting3.Test=Value4 Mock ` -CommandName Set-Content ` -ParameterFilter { + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileContent) + } ` + -Verifiable + + It 'Should not throw an exception' { + { $script:result = Set-TargetResource ` + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Text $script:testTextReplaceNoFind ` + -AllowAppend $false ` + -Verbose + } | Should -Not -Throw + } + + It 'Should call the expected mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Assert-ParametersValid -Exactly 1 + + Assert-MockCalled ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -Exactly 1 + + Assert-MockCalled ` + -CommandName Set-Content ` + -ParameterFilter { ($path -eq $script:testTextFile) -and ` - ($value -eq $script:testFileExpectedSecretContent) + ($value -eq $script:testFileContent) } ` + -Exactly 1 + } + } + + Context 'File exists and search secret can be found' { + # verifiable (should be called) mocks + Mock ` + -CommandName Assert-ParametersValid ` + -ModuleName 'DSR_ReplaceText' ` + -Verifiable + + Mock ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -MockWith { $script:testFileContent } ` + -Verifiable + + Mock ` + -CommandName Set-Content ` + -ParameterFilter { + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileExpectedSecretContent) + } ` -Verifiable It 'Should not throw an exception' { { Set-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Type 'Secret' ` - -Secret $script:testSecretCredential ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Type 'Secret' ` + -Secret $script:testSecretCredential ` + -Verbose } | Should -Not -Throw } @@ -249,9 +359,9 @@ Setting3.Test=Value4 Assert-MockCalled ` -CommandName Set-Content ` -ParameterFilter { - ($path -eq $script:testTextFile) -and ` - ($value -eq $script:testFileExpectedSecretContent) - } ` + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testFileExpectedSecretContent) + } ` -Exactly 1 } } @@ -272,17 +382,17 @@ Setting3.Test=Value4 Mock ` -CommandName Set-Content ` -ParameterFilter { - ($path -eq $script:testTextFile) -and ` - ($value -eq $script:testTextReplace) - } ` + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testTextReplace) + } ` -Verifiable It 'Should not throw an exception' { { Set-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Text $script:testTextReplace ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Text $script:testTextReplace ` + -Verbose } | Should -Not -Throw } @@ -298,9 +408,9 @@ Setting3.Test=Value4 Assert-MockCalled ` -CommandName Set-Content ` -ParameterFilter { - ($path -eq $script:testTextFile) -and ` - ($value -eq $script:testTextReplace) - } ` + ($path -eq $script:testTextFile) -and ` + ($value -eq $script:testTextReplace) + } ` -Exactly 1 } } @@ -310,7 +420,7 @@ Setting3.Test=Value4 #region Function Test-TargetResource Describe 'DSR_ReplaceString\Test-TargetResource' { - Context 'File exists and search text can not be found' { + Context 'File exists search text can not be found and AllowAppend is TRUE' { # verifiable (should be called) mocks Mock ` -CommandName Assert-ParametersValid ` @@ -333,14 +443,61 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearchNoFind ` - -Text $script:testTextReplace ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Text $script:testTextReplace ` + -AllowAppend $true ` + -Verbose } | Should -Not -Throw } - It 'Should return true' { + It 'Should return false' { + $script:result | Should -Be $false + } + + It 'Should call the expected mocks' { + Assert-VerifiableMock + Assert-MockCalled -CommandName Assert-ParametersValid -Exactly 1 + + Assert-MockCalled ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -Exactly 1 + } + } + + Context 'File exists search text can not be found and AllowAppend is FALSE' { + # verifiable (should be called) mocks + Mock ` + -CommandName Assert-ParametersValid ` + -ModuleName 'DSR_ReplaceText' ` + -Verifiable + + Mock ` + -CommandName Test-Path ` + -ModuleName 'DSR_ReplaceText' ` + -MockWith { $true } ` + -Verifiable + + Mock ` + -CommandName Get-Content ` + -ParameterFilter { $path -eq $script:testTextFile } ` + -MockWith { $script:testFileContent } ` + -Verifiable + + $script:result = $null + + It 'Should not throw an exception' { + { $script:result = Test-TargetResource ` + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Text $script:testTextReplace ` + -AllowAppend $false ` + -Verbose + } | Should -Not -Throw + } + + It 'Should return false' { $script:result | Should -Be $true } @@ -355,6 +512,7 @@ Setting3.Test=Value4 } } + Context 'File exists and search text can be found but does not match replace string' { # verifiable (should be called) mocks Mock ` @@ -378,10 +536,10 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Text $script:testTextReplace ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Text $script:testTextReplace ` + -Verbose } | Should -Not -Throw } @@ -423,10 +581,10 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Text $script:testTextReplace ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Text $script:testTextReplace ` + -Verbose } | Should -Not -Throw } @@ -468,11 +626,11 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Type 'Secret' ` - -Secret $script:testSecretCredential ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Type 'Secret' ` + -Secret $script:testSecretCredential ` + -Verbose } | Should -Not -Throw } @@ -514,11 +672,11 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Type 'Secret' ` - -Secret $script:testSecretCredential ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Type 'Secret' ` + -Secret $script:testSecretCredential ` + -Verbose } | Should -Not -Throw } @@ -559,10 +717,10 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { $script:result = Test-TargetResource ` - -Path $script:testTextFile ` - -Search $script:testSearchNoFind ` - -Text $script:testTextReplace ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearchNoFind ` + -Text $script:testTextReplace ` + -Verbose } | Should -Not -Throw } @@ -600,9 +758,9 @@ Setting3.Test=Value4 It 'Should not throw an exception' { { Assert-ParametersValid ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Verbose } | Should -Not -Throw } @@ -632,9 +790,9 @@ Setting3.Test=Value4 It 'Should throw expected exception' { { Assert-ParametersValid ` - -Path $script:testTextFile ` - -Search $script:testSearch ` - -Verbose + -Path $script:testTextFile ` + -Search $script:testSearch ` + -Verbose } | Should -Throw $errorRecord } @@ -645,6 +803,44 @@ Setting3.Test=Value4 } } #endregion + + Describe 'DSR_ReplaceText\Add-ConfigurationEntry' { + Context 'Append text' { + $result = Add-ConfigurationEntry ` + -Text "Setting.NotExist='$($script:testText)'" ` + -FileContent $script:testFileContent + + It 'Should append line to end of text' { + $result | Should -Be $script:testFileExpectedTextContentNewKey + } + } + + Context 'Apply a LF (default *nix)' { + $nixString = "Line1`nLine2`n" + + $result = Add-ConfigurationEntry ` + -Text 'Line3' ` + -FileContent $nixString + + It 'Should end with a LF' { + $result -match '\n$' | Should -BeTrue + $result -match '\b\r\n$' | Should -BeFalse + } + } + + Context 'Apply a CRLF (default Windows)' { + $windowsString = "Line1`r`nLine2`r`n" + + $result = Add-ConfigurationEntry ` + -Text 'Line3' ` + -FileContent $windowsString + + It 'Should match a CRLF line ending' { + $result -match '\r\n$' | Should -BeTrue + $result -match '\b\n$' | Should -BeFalse + } + } + } } } finally