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 TOML file with a list of tests.
.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.
.EXAMPLE
    PS > Invoke-HttpUnit -Url https://www.google.com -Code 200
 
    Label : https://www.google.com/
    Result :
    Connected : True
    GotCode : True
    GotText : False
    GotRegex : False
    GotHeaders : False
    InvalidCert : False
    TimeTotal : 00:00:00.4695217
    .EXAMPLE
    PS > Invoke-HttpUnit -Path .\example.toml
 
    Label : google
    Result :
    Connected : True
    GotCode : True
    GotText : False
    GotRegex : False
    GotHeaders : False
    InvalidCert : False
    TimeTotal : 00:00:00.3210709
 
    Label : api
    Result : Exception calling "GetResult" with "0" argument(s): "No such host is known. (api.example.com:80)"
    Connected : False
    GotCode : False
    GotText : False
    GotRegex : False
    GotHeaders : False
    InvalidCert : False
    TimeTotal : 00:00:00.0280893
 
    Label : redirect
    Result : Unexpected status code: NotFound
    Connected : True
    GotCode : False
    GotText : False
    GotRegex : False
    GotHeaders : False
    InvalidCert : False
    TimeTotal : 00:00:00.1021738
.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 hidden Response property with the raw response from the server.
.LINK
    https://github.com/StackExchange/httpunit
#>


    [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')]
        [Alias('Address', 'ComputerName')]
        [string]
        $Url,

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

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


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

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

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

    if ($PSBoundParameters.ContainsKey('Path')) {
        $configContent = Get-Content -Path $Path -Raw

        $configObject = [Tomlyn.Toml]::ToModel($configContent)

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

            switch ($plan.Keys) {
                'label' { $testPlan.Label = $plan[$_] }
                'url' { $testPlan.Url = $plan[$_] }
                'code' { $testPlan.Code = $plan[$_] }
                'string' { $testPlan.Text = $plan[$_] }
                'timeout' { $testPlan.Timeout = [timespan]$plan[$_] }
                'tags' { $testPlan.Tags = $plan[$_] }
                'headers' {
                    $asHash = @{}
                    $plan[$_].ForEach({ $asHash.Add($_.Key, $_.Value) })
                    $testPlan.Headers = $asHash
                }
                'certficate' {
                    $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 }
        }

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

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