public/Invoke-HttpUnit.ps1

function Invoke-HttpUnit {
<#
.SYNOPSIS
    A PowerShell port of httpunit.
.DESCRIPTION
    This is not a 100% accurate port of httpunit. The goal of this module is to utilize Net.Http.HttpClient to more closely simulate a .Net client application. It also provides easy access to the Windows Certificate store for client certificate authentication.
.PARAMETER Path
    Specifies a path to a configuration file with a list of tests. Supported types are .toml, .yml, json, and .psd1.
.PARAMETER Tag
    If specified, only runs plans that are tagged with one of the tags specified.
.PARAMETER Url
    The URL to retrieve.
.PARAMETER Code
    For http/https, the expected status code, default 200.
.PARAMETER String
    For http/https, a string we expect to find in the result.
.PARAMETER Headers
    For http/https, a hashtable to validate the response headers.
.PARAMETER Timeout
    A timeout for the test. Default is 3 seconds.
.PARAMETER Certificate
    For http/https, specifies the client certificate that is used for a secure web request. Enter a variable that contains a certificate.
.PARAMETER Method
    For http/https, the HTTP method to send.
.PARAMETER IPAddress
    Provide one or more IPAddresses to target. Pass `'*'` to test all resolved addresses. Default is first resolved address.
.PARAMETER SkipVerify
    Allow testing of untrusted or self-signed certificates
.PARAMETER Quiet
    Do not output ErrorRecords for failed tests.
.EXAMPLE
    PS > Invoke-HttpUnit -Url https://www.google.com -Code 200
    Label Result Connected GotCode GotText GotHeaders InvalidCert TimeTotal
    ----- ------ --------- ------- ------- ---------- ----------- ---------
    https://www.google.com/ (142.250.190.132) True True False False False 00:00:00.2840173

    Run an ad-hoc test against one Url.

    .EXAMPLE
    PS > Invoke-HttpUnit -Path .\example.toml
    Label Result Connected GotCode GotText GotHeaders InvalidCert TimeTotal
    ----- ------ --------- ------- ------- ---------- ----------- ---------
    google (142.250.190.132) True True False False False 00:00:00.2064638
    redirect (93.184.216.34) InvalidResult True False False False False 00:00:00.0953043
    redirect (10.11.22.33) OperationTimeout False False False False False 00:00:03.0100917
    redirect (10.99.88.77) OperationTimeout False False False False False 00:00:03.0067049

    Run all of the tests in a given config file.
.NOTES
    A `$null` Results property signifies no error and all specified test criteria passed.

    You can use the common variable _OutVariable_ to save the test results.
    Each TestResult object has a Response property with the raw response from the server.
    For HTTPS tests, the TestResult object will have the ServerCertificate populated with the certificate presented by the server.
.LINK
    https://github.com/StackExchange/httpunit
.LINK
    https://github.com/cdhunt/Import-ConfigData
#>


    [CmdletBinding(DefaultParameterSetName = 'url')]
    [Alias('httpunit', 'Test-Http', 'ihu')]
    param (
        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'config-file',
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true,
            HelpMessage = "Path to one or more locations.")]
        [Alias('PSPath')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Path,

        [Parameter(Position = 1,
            ParameterSetName = 'config-file')]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Tag,

        [Parameter(Mandatory,
            Position = 0,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Address', 'ComputerName')]
        [string]
        $Url,

        [Parameter(Position = 1,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [Alias('StatusCode')]
        [string]
        $Code,

        [Parameter(Position = 2,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [Alias('Text')]
        [string]
        $String,

        [Parameter(Position = 3,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [hashtable]
        $Headers,

        [Parameter(Position = 4,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [timespan]
        $Timeout,

        [Parameter(Position = 5,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [X509Certificate]
        $Certificate,

        [Parameter(Position = 6,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Options', 'Patch', 'Post', 'Put', 'Trace')]
        [String]
        $Method,

        [Parameter(Position = 7,
            ParameterSetName = 'url',
            ValueFromPipelineByPropertyName = $true)]
        [String[]]
        $IPAddress,

        [Parameter()]
        [Alias('InsecureSkipVerify')]
        [switch]
        $SkipVerify,

        [Parameter()]
        [Switch]
        $Quiet
    )

    process {

        if ($PSBoundParameters.ContainsKey('Path')) {
            foreach ($p in $Path) {
                Write-Debug "Running checks defined in '$p'"


                $configObject = Import-ConfigData -Path $p

                foreach ($plan in $configObject['plan']) {
                    $testPlan = [TestPlan]@{
                        Label = $plan['label']
                    }

                    switch ($plan.Keys) {
                        'label' { $testPlan.Label = $plan[$_] }
                        'url' { $testPlan.Url = $plan[$_] }
                        'method' { $testPlan.Method = $plan[$_] }
                        'code' { $testPlan.Code = $plan[$_] }
                        'string' { $testPlan.Text = $plan[$_] }
                        'timeout' { $testPlan.Timeout = [timespan]$plan[$_] }
                        'tags' { $testPlan.Tags = $plan[$_] }
                        'headers' { $testPlan.Headers = $plan[$_] }
                        'ips' { $testPlan.IPs = $plan[$_] }
                        'certificate' {
                            $value = $plan[$_]
                            if ($value -like 'cert:\*') {
                                $testPlan.ClientCertificate = Get-Item $value
                            } else {
                                $testPlan.ClientCertificate = (Get-Item "Cert:\LocalMachine\My\$value")
                            }
                        }
                        'insecureSkipVerify' { $testPlan.InsecureSkipVerify = $plan[$_] }
                    }

                    # Filter tests
                    if ($PSBoundParameters.ContainsKey('Tag')) {
                        $found = $false
                        foreach ($t in $Tag) {
                            if ($testPlan.Tags -contains $t) { $found = $true }
                        }
                        if (!$found) {
                            $testTags = $testPlan.Tags -join ', '
                            $filterTags = $Tag -join ', '
                            Write-Debug "Specified tags ($filterTags) do not match defined tags ($testTags)"
                            Continue
                        }
                    }

                    foreach ($case in $testPlan.Cases()) {
                        $case.Test()
                    }
                }
            }
        } else {
            $plan = [TestPlan]::new()
            $plan.URL = $Url

            switch ($PSBoundParameters.Keys) {
                'Code' { $plan.Code = $Code }
                'String' { $plan.Text = $String }
                'Headers' { $plan.Headers = $Headers }
                'Timeout' { $plan.Timeout = $Timeout }
                'Certificate' { $plan.ClientCertificate = $Certificate }
                'Method' { $plan.Method = $Method }
                'IPAddress' { $plan.IPs = $IPAddress }
            }

            if ($SkipVerify) { $plan.insecureSkipVerify = $true }

            foreach ($case in $plan.Cases()) {
                $result = $case.Test()

                Write-Output $result
                if ($null -ne $result.Result -and !$Quiet) {
                    Write-Error -ErrorRecord $result.Result
                }
            }
        }
    }
}