Viscalyx.Assert.psm1
#Region '.\prefix.ps1' -1 $script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common' Import-Module -Name $script:dscResourceCommonModulePath $script:viscalyxCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/Viscalyx.Common' Import-Module -Name $script:viscalyxCommonModulePath $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' #EndRegion '.\prefix.ps1' 8 #Region '.\Public\Assert-BlockString.ps1' -1 <# .SYNOPSIS Asserts that a string, here-string, or array of strings matches the expected value. .DESCRIPTION The `Assert-BlockString` command compares a string, here-string, or array of strings against an expected string, here-string, or array of strings. If they are not identical, it throws an error that includes the hex output. The comparison is case-sensitive. It is commonly used in unit testing scenarios to verify the correctness of string outputs at the byte level. .PARAMETER Actual The actual string, here-string, or array of strings to be compared with the expected value. This parameter accepts pipeline input. .PARAMETER Expected The expected string, here-string, or array of strings that the actual value should match. .PARAMETER Because An optional reason or explanation for the assertion. .PARAMETER Highlight An optional ANSI color code to highlight the difference between the expected and actual strings. The default value is '31m' (red text). .PARAMETER NoHexOutput Specifies whether to omit the hex columns and output only the character groups. .EXAMPLE PS> Assert-BlockString -Actual 'hello', 'world' -Expected 'Hello', 'World' This example asserts that the array of strings 'hello' and 'world' matches the expected array of strings 'Hello' and 'World'. If the assertion fails, an error is thrown. .EXAMPLE PS> 'hello', 'world' | Assert-BlockString -Expected 'Hello', 'World' This example demonstrates the usage of pipeline input. A string array containing 'hello' and 'world' are piped to `Assert-BlockString` and compared with the expected string array containing 'Hello' and 'World'. If the assertion fails, an error is thrown. .NOTES TODO: Is it possible to rename the command to `Should-BeBlockString`? Pester handles commands with the `Should` verb; however, it is unclear if this issue can be resolved here. See: https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-b7592dd925696de2521c9b12b966d65519d502045462f002c343caa7c0986936 and https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-460f64eafc16facefbed201eb00fb151c75eadf7cc58a504a01527015fb1c7cdR17 #> function Assert-BlockString { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')] [CmdletBinding()] [Alias('Should-BeBlockString')] [OutputType()] param ( [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true)] [System.Object] $Actual, [Parameter(Position = 0, Mandatory = $true)] [System.Object] $Expected, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Because, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Highlight = '31m', [Parameter()] [System.Management.Automation.SwitchParameter] $NoHexOutput ) $hasPipelineInput = $MyInvocation.ExpectingInput if ($hasPipelineInput) { $Actual = @($local:Input) } # Verify if $Actual is a string or string array $isActualStringType = $Actual -is [System.String] -or ($Actual -is [System.Array] -and ($Actual.Count -eq 0 -or ($Actual.Count -gt 0 -and $Actual[0] -is [System.String]))) $isExpectedStringType = $Expected -is [System.String] -or ($Expected -is [System.Array] -and ($Expected.Count -eq 0 -or ($Expected.Count -gt 0 -and $Expected[0] -is [System.String]))) $stringsAreEqual = $isActualStringType -and $isExpectedStringType -and (-join $Actual) -ceq (-join $Expected) if (-not $stringsAreEqual) { if (-not $isActualStringType) { $message = $script:localizedData.Assert_BlockString_ActualInvalid } elseif (-not $isExpectedStringType) { $message = $script:localizedData.Assert_BlockString_ExpectedInvalid } else { $message = $script:localizedData.Assert_BlockString_StringsNotEqual if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_BlockString_StringsNotEqual_Because } $message += "{0}`r`n " -f $script:localizedData.Assert_BlockString_Difference $message += Out-Difference -Reference $Expected -Difference $Actual -ReferenceLabel 'Expected:' -DifferenceLabel 'But was:' -HighlightStart:$Highlight -NoHexOutput:$NoHexOutput.IsPresent | ForEach-Object -Process { "`e[0m$_`r`n" } } throw [Pester.Factory]::CreateShouldErrorRecord($Message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } } #EndRegion '.\Public\Assert-BlockString.ps1' 129 #Region '.\Public\Assert-ObjectMethod.ps1' -1 <# .SYNOPSIS Asserts that an object contains a specified method. .DESCRIPTION The `Assert-ObjectMethod` command verifies that an object contains a specified method. This is commonly used in unit testing scenarios to verify object structure and ensure that required methods are available on objects before attempting to invoke them. .PARAMETER Method The name of the method to assert exists on the object. .PARAMETER Actual The object to be inspected for the method. This parameter accepts pipeline input. .PARAMETER Because An optional reason or explanation for the assertion. .INPUTS System.Object Accepts any object via the pipeline for method inspection. .OUTPUTS None This command does not return any output on success. .EXAMPLE PS> Assert-ObjectMethod -Actual $myObject -Method 'ToString' This example asserts that the object in `$myObject` has a method named 'ToString'. If the method does not exist, an error is thrown. .EXAMPLE PS> $myObject | Assert-ObjectMethod -Method 'GetHashCode' This example demonstrates pipeline usage. The object `$myObject` is piped to `Assert-ObjectMethod` and checked for a method named 'GetHashCode'. .EXAMPLE PS> Assert-ObjectMethod -Method 'Connect' -Actual $connection -Because 'the connection object should support Connect method' This example asserts that `$connection` has a method named 'Connect', providing a reason for the assertion. .EXAMPLE PS> $collection | Assert-ObjectMethod -Method 'Add' This example verifies that a collection object has an 'Add' method, which is useful when you need to ensure you can add items to a collection. #> function Assert-ObjectMethod { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')] [CmdletBinding()] [Alias('Should-HaveMethod')] [OutputType()] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] $Method, [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true)] [System.Object] $Actual, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Because ) $hasPipelineInput = $MyInvocation.ExpectingInput if ($hasPipelineInput) { $Actual = @($local:Input) } # If multiple objects were passed via pipeline, iterate through each one if ($hasPipelineInput -and $Actual -is [System.Array] -and $Actual.Count -gt 0) { foreach ($currentObject in $Actual) { # Check if the current object is null if ($null -eq $currentObject) { $message = $script:localizedData.Assert_ObjectMethod_ActualIsNull if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectMethod_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # Check if the method exists on the current object $hasMethod = $false try { # Use Get-Member to check for method existence $member = $currentObject | Get-Member -Name $Method -MemberType Method, ScriptMethod -ErrorAction SilentlyContinue if ($null -ne $member) { $hasMethod = $true } else { # For dynamic objects and PSCustomObject, also check PSObject.Methods $methodMember = $currentObject.PSObject.Methods[$Method] if ($null -ne $methodMember) { $hasMethod = $true } else { # Final check: try to get the method directly for edge cases try { $methodInfo = $currentObject.GetType().GetMethod($Method) if ($null -ne $methodInfo) { $hasMethod = $true } } catch { # If reflection fails, the method doesn't exist $hasMethod = $false } } } } catch { $hasMethod = $false } if (-not $hasMethod) { $message = $script:localizedData.Assert_ObjectMethod_MethodNotFound -f $Method if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectMethod_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } } } else { # Single object case (direct parameter or not from pipeline) # Check if the actual value is null if ($null -eq $Actual) { $message = $script:localizedData.Assert_ObjectMethod_ActualIsNull if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectMethod_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # Check if the method exists on the object $hasMethod = $false try { # Use Get-Member to check for method existence $member = $Actual | Get-Member -Name $Method -MemberType Method, ScriptMethod -ErrorAction SilentlyContinue if ($null -ne $member) { $hasMethod = $true } else { # For dynamic objects and PSCustomObject, also check PSObject.Methods $methodMember = $Actual.PSObject.Methods[$Method] if ($null -ne $methodMember) { $hasMethod = $true } else { # Final check: try to get the method directly for edge cases try { $methodInfo = $Actual.GetType().GetMethod($Method) if ($null -ne $methodInfo) { $hasMethod = $true } } catch { # If reflection fails, the method doesn't exist $hasMethod = $false } } } } catch { $hasMethod = $false } if (-not $hasMethod) { $message = $script:localizedData.Assert_ObjectMethod_MethodNotFound -f $Method if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectMethod_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } } } #EndRegion '.\Public\Assert-ObjectMethod.ps1' 228 #Region '.\Public\Assert-ObjectProperty.ps1' -1 <# .SYNOPSIS Asserts that an object contains a specified property and optionally that the property has a specified value. .DESCRIPTION The `Assert-ObjectProperty` command verifies that an object contains a specified property. It can optionally also verify that the property has a specific value. This is commonly used in unit testing scenarios to verify object structure and property values. .PARAMETER Property The name of the property to assert exists on the object. .PARAMETER Actual The object to be inspected for the property. This parameter accepts pipeline input. .PARAMETER Value The expected value of the property. If specified, the assertion will check both property existence and value equality. .PARAMETER Because An optional reason or explanation for the assertion. .INPUTS System.Object Accepts any object via the pipeline for property inspection. .OUTPUTS None This command does not return any output on success. .EXAMPLE PS> Assert-ObjectProperty -Actual $myObject -Property 'Enabled' This example asserts that the object in `$myObject` has a property named 'Enabled'. If the property does not exist, an error is thrown. .EXAMPLE PS> Assert-ObjectProperty -Actual $myObject -Property 'Enabled' -Value $true This example asserts that the object in `$myObject` has a property named 'Enabled' with the value `$true`. If the property does not exist or the value does not match, an error is thrown. .EXAMPLE PS> $myObject | Assert-ObjectProperty -Property 'Status' -Value 'Running' This example demonstrates pipeline usage. The object `$myObject` is piped to `Assert-ObjectProperty` and checked for a property named 'Status' with the value 'Running'. .EXAMPLE PS> Assert-ObjectProperty -Property 'Count' -Actual $collection -Value 5 -Because 'the collection should contain exactly 5 items' This example asserts that `$collection` has a property named 'Count' with the value 5, providing a reason for the assertion. #> function Assert-ObjectProperty { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')] [CmdletBinding(DefaultParameterSetName = 'AssertProperty')] [Alias('Should-HaveProperty')] [OutputType()] param ( [Parameter(ParameterSetName = 'AssertProperty', Position = 0, Mandatory = $true)] [Parameter(ParameterSetName = 'AssertValue', Position = 0, Mandatory = $true)] [System.String] $Property, [Parameter(ParameterSetName = 'AssertProperty', Position = 1, Mandatory = $true, ValueFromPipeline = $true)] [Parameter(ParameterSetName = 'AssertValue', Position = 2, Mandatory = $true, ValueFromPipeline = $true)] [System.Object] $Actual, [Parameter(ParameterSetName = 'AssertValue', Position = 1, Mandatory = $true)] [AllowNull()] [System.Object] $Value, [Parameter()] [ValidateNotNullOrEmpty()] [System.String] $Because ) $hasPipelineInput = $MyInvocation.ExpectingInput if ($hasPipelineInput) { $Actual = @($local:Input) } # If multiple objects were passed via pipeline, iterate through each one if ($hasPipelineInput -and $Actual -is [System.Array] -and $Actual.Count -gt 0) { foreach ($currentObject in $Actual) { # Check if the current object is null if ($null -eq $currentObject) { $message = $script:localizedData.Assert_ObjectProperty_ActualIsNull if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # Check if the property exists on the current object $hasProperty = $false try { # For hashtables, check if the key exists if ($currentObject -is [System.Collections.IDictionary]) { $hasProperty = $currentObject.ContainsKey($Property) } # For PSCustomObject and other objects, try PSObject.Properties first elseif ($null -ne $currentObject.PSObject.Properties[$Property]) { $hasProperty = $true } # Use Get-Member as fallback for .NET objects else { $member = $currentObject | Get-Member -Name $Property -MemberType Property, NoteProperty, ScriptProperty -ErrorAction SilentlyContinue if ($null -ne $member) { $hasProperty = $true } else { # Final check with direct property access for edge cases try { $null = $currentObject.$Property # If we got here without exception, the property exists # But we need to make sure it's not just returning $null for non-existent properties $actualMember = $currentObject.PSObject.Members | Where-Object { $_.Name -eq $Property } $hasProperty = $null -ne $actualMember } catch { $hasProperty = $false } } } } catch { $hasProperty = $false } if (-not $hasProperty) { $message = $script:localizedData.Assert_ObjectProperty_PropertyNotFound -f $Property if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # If we're in the AssertValue parameter set, also check the value if ($PSCmdlet.ParameterSetName -eq 'AssertValue') { $actualValue = $currentObject.$Property # Use more sophisticated comparison that handles arrays and null values correctly $valuesAreEqual = $false if ($null -eq $actualValue -and $null -eq $Value) { $valuesAreEqual = $true } elseif ($actualValue -is [System.Array] -and $Value -is [System.Array]) { # Use StructuralEqualityComparer for element-wise array comparison (supports value-type arrays) $valuesAreEqual = [System.Collections.StructuralComparisons]::StructuralEqualityComparer.Equals($actualValue, $Value) } else { # Use PowerShell's built-in comparison for other types $valuesAreEqual = $actualValue -eq $Value } if (-not $valuesAreEqual) { $message = $script:localizedData.Assert_ObjectProperty_ValueMismatch -f $Property, $Value, $actualValue if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } } } } else { # Single object case (not an array or empty array) # Check if the actual value is null if ($null -eq $Actual) { $message = $script:localizedData.Assert_ObjectProperty_ActualIsNull if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # Check if the property exists on the object $hasProperty = $false try { # For hashtables, check if the key exists if ($Actual -is [System.Collections.IDictionary]) { $hasProperty = $Actual.ContainsKey($Property) } # For PSCustomObject and other objects, try PSObject.Properties first elseif ($null -ne $Actual.PSObject.Properties[$Property]) { $hasProperty = $true } # Use Get-Member as fallback for .NET objects else { $member = $Actual | Get-Member -Name $Property -MemberType Property, NoteProperty, ScriptProperty -ErrorAction SilentlyContinue if ($null -ne $member) { $hasProperty = $true } else { # Final check with direct property access for edge cases try { $null = $Actual.$Property # If we got here without exception, the property exists # But we need to make sure it's not just returning $null for non-existent properties $actualMember = $Actual.PSObject.Members | Where-Object { $_.Name -eq $Property } $hasProperty = $null -ne $actualMember } catch { $hasProperty = $false } } } } catch { $hasProperty = $false } if (-not $hasProperty) { $message = $script:localizedData.Assert_ObjectProperty_PropertyNotFound -f $Property if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } # If we're in the AssertValue parameter set, also check the value if ($PSCmdlet.ParameterSetName -eq 'AssertValue') { $actualValue = $Actual.$Property # Use more sophisticated comparison that handles arrays and null values correctly $valuesAreEqual = $false if ($null -eq $actualValue -and $null -eq $Value) { $valuesAreEqual = $true } elseif ($actualValue -is [System.Array] -and $Value -is [System.Array]) { # Use SequenceEqual for efficient structural array comparison $valuesAreEqual = [System.Linq.Enumerable]::SequenceEqual([System.Collections.Generic.IEnumerable[object]]$actualValue, [System.Collections.Generic.IEnumerable[object]]$Value) } else { # Use PowerShell's built-in comparison for other types $valuesAreEqual = $actualValue -eq $Value } if (-not $valuesAreEqual) { $message = $script:localizedData.Assert_ObjectProperty_ValueMismatch -f $Property, $Value, $actualValue if ($Because) { $message += " {0} $Because" -f $script:localizedData.Assert_ObjectProperty_Because } throw [Pester.Factory]::CreateShouldErrorRecord($message, $MyInvocation.ScriptName, $MyInvocation.ScriptLineNumber, $MyInvocation.Line.TrimEnd([System.Environment]::NewLine), $true) } } } } #EndRegion '.\Public\Assert-ObjectProperty.ps1' 318 |