tests/HttpPipelineMocking.ps1


$ErrorActionPreference = "Stop" 

$script:description = new-object -type System.Collections.Stack
$script:operation = new-object -type System.Collections.Stack
$script:count = 0

function mock-http(
    $request,
    $callback,
    $next
) {
    $script:count = $script:count +1
    
    # DESCRIPTION+OPERATION+URI + COUNT:RESULT
    $rqkey = "$script:description+$script:operation+$($request.RequestUri)+$script:count"

    switch( $TestMode ) {
        'live' {
            return $next.SendAsync($request,$callback);
        }
        
        'record' {
            # make the call
            $responseTask = $next.SendAsync($request,$callback);
            $response = $responseTask.Result
            
            # duplicate the response
            $responseJson = serialize-response $response
            $responseCopy = deserialize-response $responseJson
    
            # save the call
            $script:recording[$rqkey]  = $responseJson
            set-content $script:TestRecordingFile -value (convertto-json $script:recording)
    
            # throw away the original.
            $response.Dispose();
    
            # return our synthetic result, should be as good as the original.
            return [System.Threading.Tasks.Task]::FromResult($responseCopy)            
        }
        
        'playback' {
            # pick the response up from
            $lookup = $script:recording[$rqkey]
            if( $lookup -eq $null ) {
                write-error "`n`nMissing recording for test`n $script:description => $script:operation`n [$script:count]$($request.RequestUri)"
            }
            $response = deserialize-response $lookup
            
            return [System.Threading.Tasks.Task]::FromResult($response)            
            
        }
    }
}

function serialize-response($response) {
    return @{
        RequestMessage = @{
            RequestUri = $response.RequestMessage.RequestUri;
            Method = $response.RequestMessage.Method;
            Headers = $response.RequestMessage.Headers
        };
        Content = $response.Content.ReadByteArrayAsnyc().Result
        Headers = $response.Headers
    }    
}

function ConvertJson-ToHashtable($InputObject)
{
    return ConvertPSObject-ToHashtable( convertfrom-json $InputObject )
}

function ConvertPSObject-ToHashtable($InputObject)
{
    if ($null -eq $InputObject) { return $null }

    if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
    {
        $collection = @(
            foreach ($object in $InputObject) { ConvertPSObject-ToHashtable $object }
        )
        Write-Output -NoEnumerate $collection
    }
    elseif ($InputObject -is [psobject])
    {
        $hash = @{}
        foreach ($property in $InputObject.PSObject.Properties)
        {
            $hash[$property.Name] = ConvertPSObject-ToHashtable $property.Value
        }
        $hash
    }
    else
    {
        $InputObject
    }
}

function deserialize-response($json) {
    $data = ConvertJson-ToHashtable $json
    
    $global:data = $data
    
    $response = new-object -type System.Net.Http.HttpResponseMessage
    
    $response.RequestMessage = new-object -type System.Net.Http.HttpRequestMessage 
    $response.RequestMessage.RequestUri = $data.RequestMessage.RequestUri
    $shh = $data.RequestMessage.Headers.Keys |% { $response.RequestMessage.Headers.TryAddWithoutValidation( $_ , $data.RequestMessage.Headers[ $_ ] ) } 
    
    $response.StatusCode = $data.StatusCode
    $response.Content =  new-object -type System.Net.Http.ByteArrayContent -ArgumentList @(,([System.Convert]::FromBase64String( $data.Content) ))
    $shh = $data.Headers.Keys |% { $response.Headers.TryAddWithoutValidation( $_ , $data.Headers[ $_ ] ) } 
    $shh = $data.ContentHeaders.Keys |% { $response.Content.Headers.TryAddWithoutValidation( $_ , $data.ContentHeaders[ $_ ] ) } 
    return $response
}

function serialize-headers($headers) {
    $result = @{} 
    if( $headers -ne $null ) {
      $headers|% { 
        if( $_.Key -eq "Authorization" ) {
            $result[$_.Key] = "AUTH_HEADER_REMOVED" 
        } else {
            $result[$_.Key] = $_.Value 
        }
      }
    }
    return $result;
}

function serialize-request($request) {
    return @{
        RequestUri = $request.RequestUri;
        Method = $request.Method;
        Headers = (serialize-headers $request.Headers)
    }
}

function serialize-response($response) {
    $obj =@{
        RequestMessage = (serialize-request $response.RequestMessage);
        Content = [System.Convert]::ToBase64String( $response.Content.ReadAsByteArrayAsync().Result )
        Headers = (serialize-headers $response.Headers )
        ContentHeaders = (serialize-headers $response.Content.Headers )
        StatusCode = $response.StatusCode
    }    
    return (convertto-json $obj)
}

$script:mock = ${function:mock-http}

function Describe(
    [Parameter(Mandatory = $true, Position = 0)]
    [string] $Name,

    [Alias('Tags')]
    [string[]] $Tag=@(),

    [Parameter(Position = 1)]
    [ValidateNotNull()]
    [ScriptBlock] $Fixture = $(Throw "No test script block is provided. (Have you put the open curly brace on the next line?)"))
{ 
    $script:description.Push( $Name ) 
    try {
        return pester\Describe -Name $Name -Tag $Tag -Fixture $fixture
    } 
    finally {
        $script:description.Pop()
    }
}

function It {
    [CmdletBinding(DefaultParameterSetName = 'Normal')]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$name,

        [Parameter(Position = 1)]
        [ScriptBlock] $test = {},

        [System.Collections.IDictionary[]] $TestCases,

        [Parameter(ParameterSetName = 'Pending')]
        [Switch] $Pending,

        [Parameter(ParameterSetName = 'Skip')]
        [Alias('Ignore')]
        [Switch] $Skip
    )
    $script:operation.Push( $Name )

    try {
        if( $skip ) {
            return pester\It -Name $name -test $test -TestCases $TestCases -Skip
        }
        if( $pending ) {
            return pester\It -Name $name -test $test -TestCases $TestCases -Pending
        }
        return pester\It -Name $name -test $test -TestCases $TestCases 
    }
    finally {
        $null = $script:operation.Pop()
    }
}

if( $global:TestMode -eq $null )   {
    $TestMode= 'live'
} else {
    $TestMode= $global:TestMode
    
    if( $global:TestRecordingFile -eq $null ) {
        $script:TestRecordingFile = "$PSScriptRoot/recording.json"
    } else {
        $script:TestRecordingFile = $global:TestRecordingFile
    }
    
    if( test-path $TestRecordingFile ) {
        write-debug "Loading responses from $TestRecordingFile"
        $script:recording  = ConvertPSObject-ToHashtable ( convertfrom-json ( get-content -raw $TestRecordingFile ) )
        
    } else {
        if( $TestMode -eq 'playback' ) {
            write-error "Recording file '$TestRecordingFile' is not present. Unable to continue."
        }
        $script:recording = @{}
    }
}