diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml new file mode 100644 index 0000000..2ea7d8d --- /dev/null +++ b/.github/workflows/powershell.yml @@ -0,0 +1,134 @@ +name: PowerShell + +on: + push: + branches: [ "main" ] + paths-ignore: + - 'docs/**' + - 'Changelog.md' + - 'README.md' + - src/internal/Export-HelpToMd.ps1 + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # avoid shallow clone so nbgv can do its work. + + - name: Run PSScriptAnalyzer + uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f + with: + # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. + # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. + path: .\src + recurse: true + # Include your own basic security rules. Removing this option will run all the rules + includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' + output: results.sarif + + # Upload the SARIF file generated in the previous step + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: results.sarif + + - uses: dotnet/nbgv@1801854259a50d987aaa03b99b28cebf49faa779 + id: nbgv + + - name: Build + shell: pwsh + run: ./build.ps1 build ${{ steps.nbgv.outputs.VersionMajor }} ${{ steps.nbgv.outputs.VersionMinor }} ${{ steps.nbgv.outputs.BuildNumber }} ${{ steps.nbgv.outputs.VersionRevision }} ${{ steps.nbgv.outputs.PrereleaseVersionNoLeadingHyphen }} + + - name: Store build output + uses: actions/upload-artifact@v3 + with: + name: build + path: | + publish + retention-days: 1 + + test7: + permissions: + contents: read # for actions/checkout to fetch code + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Test PowerShell 7 + needs: Build + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/powershell:${{ matrix.pwshv }}-ubuntu-22.04 + strategy: + matrix: + pwshv: ['7.3','7.4'] + + steps: + - uses: actions/checkout@v4 + + - name: Download build output + uses: actions/download-artifact@v3 + with: + name: build + path: publish + + - name: Test + shell: pwsh + run: ./build.ps1 test + + test5: + permissions: + contents: read # for actions/checkout to fetch code + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Test PowerShell 5 + needs: Build + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download build output + uses: actions/download-artifact@v3 + with: + name: build + path: publish + + - name: Test + shell: powershell + run: ./build.ps1 test + + publish: + permissions: + contents: read # for actions/checkout to fetch code + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: Publish + needs: [test7, test5] + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/dotnet/sdk:8.0 + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Download build output + uses: actions/download-artifact@v3 + with: + name: build + path: publish + + - name: Publish + shell: pwsh + run: ./build.ps1 publish + env: + PSPublishApiKey: ${{ secrets.NUGETAPIKEY }} diff --git a/README.md b/README.md index 00f3bdc..1c341d1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,11 @@ This is not a 100% accurate port of [httpunit](https://github.com/StackExchange/ 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. -## TOML +## Docs + +[Full Docs](docs) + +### TOML This configuration file is targeting compatibility with the original [httpunit file format](https://github.com/StackExchange/httpunit/tree/master#toml) but is partially implemented. @@ -31,7 +35,7 @@ Each `[[plan]]` lists: - `insecureSkipVerify = true` Will allow testing of untrusted or self-signed certificates. - `[plan.headers]` For http/https, a list of keys and values to validate the response headers. -## A sample config file +#### A sample config file ```toml [[plan]] @@ -42,101 +46,3 @@ Each `[[plan]]` lists: [plan.headers] Server = "gws" ``` - -## Help - -### Invoke-HttpUnit - -Aliases: httpunit, ihu, Test-Http - -```text -SYNOPSIS - A PowerShell port of httpunit. - - -SYNTAX - Invoke-HttpUnit [-Url] [[-Code] ] [[-String] ] [[-Headers] ] [[-Timeout] ] [[-Certificate] ] - [] - - Invoke-HttpUnit [-Path] [[-Tag] ] [] - - -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. - - -PARAMETERS - -Path - Specifies a path to a TOML file with a list of tests. - - Required? true - Position? 1 - Default value - Accept pipeline input? true (ByValue, ByPropertyName) - Accept wildcard characters? false - - -Tag - If specified, only runs plans that are tagged with one of the - tags specified. - - Required? false - Position? 2 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -Url - The URL to retrieve. - - Required? true - Position? 1 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -Code - For http/https, the expected status code, default 200. - - Required? false - Position? 2 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -String - For http/https, a string we expect to find in the result. - - Required? false - Position? 3 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -Headers - For http/https, a hashtable to validate the response headers. - - Required? false - Position? 4 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -Timeout - A timeout for the test. Default is 3 seconds. - - Required? false - Position? 5 - Default value - Accept pipeline input? false - Accept wildcard characters? false - - -Certificate - For http/https, specifies the client certificate that is used for a secure web request. Enter a variable that contains a certificate. - - Required? false - Position? 6 - Default value - Accept pipeline input? false - Accept wildcard characters? false -``` diff --git a/build.ps1 b/build.ps1 index 6293735..2df358f 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,15 +1,75 @@ +#! /usr/bin/pwsh + [CmdletBinding()] param ( [Parameter(Position = 0)] - [ValidateSet('clean', 'build')] + [ValidateSet('clean', 'build', 'test', 'changelog', 'publish', 'docs')] + [string[]] + $Task, + + [Parameter(Position = 1)] + [int] + $Major, + + [Parameter(Position = 2)] + [int] + $Minor, + + [Parameter(Position = 3)] + [int] + $Build, + + [Parameter(Position = 4)] + [int] + $Revision, + + [Parameter(Position = 5)] [string] - $Task + $Prerelease ) -$publish = Join-Path $PSScriptRoot -ChildPath publish -AdditionalChildPath 'HttpUnitPS' -$src = Join-Path $PSScriptRoot -ChildPath src -$files = Join-Path $src -ChildPath * -$lib = Join-Path $src -ChildPath lib +if ( (Get-Command 'nbgv' -CommandType Application -ErrorAction SilentlyContinue) ) { + if (!$PSBoundParameters.ContainsKey('Major')) { $Major = $(nbgv get-version -v VersionMajor) } + if (!$PSBoundParameters.ContainsKey('Minor')) { $Minor = $(nbgv get-version -v VersionMinor) } + if (!$PSBoundParameters.ContainsKey('Build')) { $Build = $(nbgv get-version -v BuildNumber) } + if (!$PSBoundParameters.ContainsKey('Revision')) { $Revision = $(nbgv get-version -v VersionRevision) } +} + +$module = 'httpunitPS' +$parent = $PSScriptRoot +$parent = if ([string]::IsNullOrEmpty($parent)) { $pwd.Path } else { $parent } +$src = Join-Path $parent -ChildPath "src" +$docs = Join-Path $parent -ChildPath "docs" +$publish = [System.IO.Path]::Combine($parent, "publish", $module) + +Write-Host "src: $src" +Write-Host "docs: $docs" +Write-Host "publish: $publish" +Write-Host "dotnet: $([Environment]::Version)" +Write-Host "ps: $($PSVersionTable.PSVersion)" + +$manifest = @{ + Path = Join-Path -Path $publish -ChildPath "$module.psd1" + Author = 'Chris Hunt' + CompanyName = 'Chris Hunt' + Copyright = '(c) Chris Hunt. All rights reserved.' + CompatiblePSEditions = @("Desktop", "Core") + Description = 'A PowerShell port of httpunit.' + GUID = '0e2a60bb-00a6-4eae-8806-55bfbb2a8ac3' + LicenseUri = "https://github.com/cdhunt/$module/blob/main/LICENSE" + FunctionsToExport = @() + ModuleVersion = [version]::new($Major, $Minor, $Build, $Revision) + PowerShellVersion = '5.1' + ProjectUri = "https://github.com/cdhunt/$module" + RootModule = "$module.psm1" + Tags = @('test-automation') + IconUri = 'https://raw.githubusercontent.com/cdhunt/httpunitPS/main/httpunitps_small.png' + RequiredAssemblies = @('System.Net.Http') + RequiredModules = @( @{ModuleName = 'Import-ConfigData'; ModuleVersion = '0.1.15.27666' } ) + CmdletsToExport = '' + VariablesToExport = '' + AliasesToExport = @('httpunit', 'Test-Http', 'ihu') +} function Clean { param () @@ -19,24 +79,150 @@ function Clean { } } +function Dependencies { + param () + + Foreach ($mod in $manifest.RequiredModules) { + if ($null -eq (Get-Module -Name $mod.ModuleName -ListAvailable | Where-Object { [version]$_.Version -ge [version]$mod.ModuleVersion })) { + Install-Module $mod.ModuleName -RequiredVersion $mod.ModuleVersion -Scope CurrentUser -Confirm:$false -Force + } + } + +} + function Build { param () - New-Item -Path $publish -ItemType Directory | Out-Null - Copy-Item -Path $files -Destination $publish - Copy-Item -Path $lib -Destination $publish -Force -Recurse - Copy-Item -Path .\LICENSE -Destination $publish - Copy-Item -Path .\README.md -Destination $publish + New-Item -Path $publish -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + Copy-Item -Path "$src/$module.psm1" -Destination $publish + Copy-Item -Path @("$parent/LICENSE", "$parent/README.md") -Destination $publish -ErrorAction SilentlyContinue + + $publicFunctions = Get-ChildItem -Path "$src/public/*.ps1" + $privateFunctions = Get-ChildItem -Path "$src/private/*.ps1" -ErrorAction SilentlyContinue + + New-Item -Path "$publish/public" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + foreach ($function in $publicFunctions) { + Copy-Item -Path $function.FullName -Destination "$publish/public/$($function.Name)" + '. "$PSSCriptRoot/public/{0}"' -f $function.Name | Add-Content "$publish/$module.psm1" + $manifest.FunctionsToExport += $function.BaseName + } + + New-Item -Path "$publish/private" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + foreach ($function in $privateFunctions) { + Copy-Item -Path $function.FullName -Destination "$publish/private/$($function.Name)" + '. "$PSSCriptRoot/private/{0}"' -f $function.Name | Add-Content "$publish/$module.psm1" + } + + if ($PSBoundParameters.ContainsKey('Prerelease')) { + $manifest.Add('Prerelease', $PreRelease) + } + + New-ModuleManifest @manifest + +} + +function Test { + param () + + if ($null -eq (Get-Module Pester -ListAvailable)) { + Install-Module -Name Pester -Confirm:$false -Force + } + + Invoke-Pester -Path test -Output detailed +} + + +function ChangeLog { + param () + "# Changelog" + + # Start log at >0.1.11 + for ($m = $Minor; $m -ge 1; $m--) { + for ($b = $Build; $b -gt 11; $b--) { + "## v$Major.$m.$b" + nbgv get-commits "$Major.$m.$b" | ForEach-Object { + $hash, $ver, $message = $_.split(' ') + $shortHash = $hash.Substring(0, 7) + + "- [$shortHash](https://github.com/cdhunt/potel/commit/$hash) $($message -join ' ')" + } + } + } +} + +function Commit { + param () + + git rev-parse --short HEAD +} + +function Publish { + param () + + <# Disabled for now + $docChanges = git status docs -s + + if ($docChanges.count -gt 0) { + Write-Warning "There are pending Docs change. Run './build.ps1 docs', review and commit updated docs." + } + #> + + $repo = if ($env:PSPublishRepo) { $env:PSPublishRepo } else { 'PSGallery' } + + $notes = ChangeLog + Publish-Module -Path $publish -Repository $repo -NuGetApiKey $env:PSPublishApiKey -ReleaseNotes $notes +} + +function Docs { + param () + + Import-Module $publish -Force + + $commands = Get-Command -Module $module -CommandType Function + $HelpToMd = [System.IO.Path]::Combine($src, 'internal', 'Export-HelpToMd.ps1') + . $HelpToMd + + @('# Import-ConfigData', [System.Environment]::NewLine) | Set-Content -Path "$docs/README.md" + $($manifest.Description) | Add-Content -Path "$docs/README.md" + @('## Cmdlets', [System.Environment]::NewLine) | Add-Content -Path "$docs/README.md" + + foreach ($command in $Commands | Sort-Object -Property Verb) { + $name = $command.Name + $docPath = Join-Path -Path $docs -ChildPath "$name.md" + $help = Get-Help -Name $name + + Export-HelpToMd $help | Set-Content -Path $docPath + + "- [$name]($name.md) $($help.Synopsis)" | Add-Content -Path "$docs/README.md" + } + + ChangeLog | Set-Content -Path "$parent/Changelog.md" } switch ($Task) { - 'clean' { + { $_ -contains 'clean' } { Clean } - 'build' { + { $_ -contains 'build' } { Clean Build } + { $_ -contains 'test' } { + Dependencies + Test + } + { $_ -contains 'changelog' } { + ChangeLog + } + { $_ -contains 'publish' } { + Dependencies + Publish + } + { $_ -contains 'docs' } { + Dependencies + Docs + } Default { Clean Build diff --git a/docs/Invoke-HttpUnit.md b/docs/Invoke-HttpUnit.md new file mode 100644 index 0000000..67a1fc2 --- /dev/null +++ b/docs/Invoke-HttpUnit.md @@ -0,0 +1,94 @@ +# Invoke-HttpUnit + + +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. +## Parameters + + +### Parameter Set 1 + + +- `[String]` **Url** _The URL to retrieve._ Mandatory +- `[String]` **Code** _For http/https, the expected status code, default 200._ +- `[String]` **String** _For http/https, a string we expect to find in the result._ +- `[Hashtable]` **Headers** _For http/https, a hashtable to validate the response headers._ +- `[TimeSpan]` **Timeout** _A timeout for the test. Default is 3 seconds._ +- `[X509Certificate]` **Certificate** _For http/https, specifies the client certificate that is used for a secure web request. Enter a variable that contains a certificate._ +- `[String]` **Method** _For http/https, the HTTP method to send._ +- `[switch]` **Quiet** _Do not output ErrorRecords for failed tests._ + + +### Parameter Set 2 + + +- `[String]` **Path** _Specifies a path to a configuration file with a list of tests. Supported types are .toml, .yml, and .psd1._ Mandatory, ValueFromPipeline +- `[String[]]` **Tag** _If specified, only runs plans that are tagged with one of the tags specified._ +- `[switch]` **Quiet** _Do not output ErrorRecords for failed tests._ + + +## Examples + + +### Example 1 + + +Run an ad-hoc test against one Url. + + +```powershell +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 2 + + +Run all of the tests in a given config file. + + +```powershell +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 +``` + + +## Links + + +- [https://github.com/StackExchange/httpunit](https://github.com/StackExchange/httpunit) +- [https://github.com/cdhunt/Import-ConfigData](https://github.com/cdhunt/Import-ConfigData) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..115516f --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# Import-ConfigData + + +A PowerShell port of httpunit. +## Cmdlets + + +- [Invoke-HttpUnit](Invoke-HttpUnit.md) A PowerShell port of httpunit. diff --git a/src/HttpUnitPS.psd1 b/src/HttpUnitPS.psd1 deleted file mode 100644 index ffb0885..0000000 --- a/src/HttpUnitPS.psd1 +++ /dev/null @@ -1,136 +0,0 @@ -# -# Module manifest for module 'HttpUnitPS' -# -# Generated by: Chris Hunt -# -# Generated on: 10/17/2023 -# - -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'HttpUnitPS.psm1' - - # Version number of this module. - ModuleVersion = '0.3.2' - - # Supported PSEditions - CompatiblePSEditions = @('Desktop', 'Core') - - # ID used to uniquely identify this module - GUID = '0e2a60bb-00a6-4eae-8806-55bfbb2a8ac3' - - # Author of this module - Author = 'Chris Hunt' - - # Company or vendor of this module - CompanyName = 'FreedomPay' - - # Copyright statement for this module - Copyright = '(c) Chris Hunt. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'A PowerShell port of httpunit.' - - # Minimum version of the PowerShell engine required by this module - PowerShellVersion = '5.1' - - # Name of the PowerShell host required by this module - # PowerShellHostName = '' - - # Minimum version of the PowerShell host required by this module - # PowerShellHostVersion = '' - - # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # DotNetFrameworkVersion = '' - - # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. - # ClrVersion = '' - - # Processor architecture (None, X86, Amd64) required by this module - # ProcessorArchitecture = '' - - # Modules that must be imported into the global environment prior to importing this module - # RequiredModules = @() - - # Assemblies that must be loaded prior to importing this module - RequiredAssemblies = @('System.Net.Http') - - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - # ScriptsToProcess = @() - - # Type files (.ps1xml) to be loaded when importing this module - # TypesToProcess = @() - - # Format files (.ps1xml) to be loaded when importing this module - # FormatsToProcess = @() - - # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - # NestedModules = @() - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = 'Invoke-HttpUnit' - - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - #CmdletsToExport = '*' - - # Variables to export from this module - #VariablesToExport = '*' - - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - #AliasesToExport = '*' - - # DSC resources to export from this module - # DscResourcesToExport = @() - - # List of all modules packaged with this module - # ModuleList = @() - - # List of all files packaged with this module - # FileList = @() - - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('test-automation') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/cdhunt/httpunitPS/blob/main/LICENSE' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/cdhunt/httpunitPS' - - # A URL to an icon representing this module. - # IconUri = '' - - # ReleaseNotes of this module - ReleaseNotes = @' -4da14ca Add Help section to Readme -54ebc7d Adds build script -be42102 Add Timeout parameter to Invoke-HttpUnit -'@ - - # Prerelease string of this module - # Prerelease = '' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - # ExternalModuleDependencies = @() - - } # End of PSData hashtable - - } # End of PrivateData hashtable - - # HelpInfo URI of this module - # HelpInfoURI = '' - - # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. - # DefaultCommandPrefix = '' - -} - diff --git a/src/HttpUnitPS.psm1 b/src/httpunitPS.psm1 similarity index 79% rename from src/HttpUnitPS.psm1 rename to src/httpunitPS.psm1 index d0ba19d..fb48ded 100644 --- a/src/HttpUnitPS.psm1 +++ b/src/httpunitPS.psm1 @@ -5,6 +5,7 @@ class Plans { class TestPlan { [string] $Label [string] $URL + [string] $Method = "Get" [string[]] $IPs [string[]] $Tags @@ -18,9 +19,18 @@ class TestPlan { [System.Collections.Generic.List[TestCase]] Cases() { $cases = [System.Collections.Generic.List[TestCase]]::new() + $planUrl = [uri]$this.URL + <# WIP + if ($this.IPs.Count -gt 0) { + if ($this.IPs -contains '*') { + + $resolved = Resolve-DnsName -Name $planUrl.Host | Select-Object -ExpandProperty IPAddress + } + } + #> $case = [TestCase]@{ - URL = [uri]$this.URL + URL = $planUrl Plan = $this ExpectCode = [System.Net.HttpStatusCode]$this.Code } @@ -35,6 +45,7 @@ class TestPlan { $case.ExpectHeaders = $this.Headers } + $cases.Add($case) return $cases @@ -59,10 +70,18 @@ class TestCase { switch ($this.URL.Scheme) { http { return $this.TestHttp() } https { return $this.TestHttp() } + file { + $fileTest = [TestResult]::new() + $exception = [Exception]::new(("URL Scheme '{0}' is not supported. Did you mean to use the -Path parameter?" -f $this.URL.Scheme )) + $fileTest.Result = [System.Management.Automation.ErrorRecord]::new($exception, "100", "InvalidData", $this.URL) + return $fileTest + } } + $noTest = [TestResult]::new() - $noTest.Result = Write-Error -Message ("no test function implemented for URL Scheme '{0}'" -f $this.URL.Scheme ) + $exception = [Exception]::new(("no test function implemented for URL Scheme '{0}'" -f $this.URL.Scheme )) + $noTest.Result = [System.Management.Automation.ErrorRecord]::new($exception, "100", "InvalidData", $this.URL) return $noTest } @@ -91,6 +110,7 @@ class TestCase { $client.Timeout = $this.Plan.Timeout $content = [Net.Http.HttpRequestMessage]::new() $content.RequestUri = $this.URL + $content.Method = [Net.Http.HttpMethod]$this.Plan.Method if ($this.Plan.InsecureSkipVerify) { Write-Debug ('TestHttp: ValidateSSL={0}' -f $this.Plan.InsecureSkipVerify) @@ -99,8 +119,9 @@ class TestCase { try { + Write-Debug "Sending request" $response = $client.SendAsync($content).GetAwaiter().GetResult() - + Write-Debug "Got response" $result.Response = $response $result.Connected = $true @@ -113,8 +134,12 @@ class TestCase { } if (![string]::IsNullOrEmpty($this.ExpectText)) { + Write-Debug ('TestHttp: ExpectText={0}' -f $this.ExpectText) + $responseContent = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() - Write-Debug ('TestHttp: Response.Content.Length={0} ExpectText={0}' -f $responseContent.Length, $this.ExpectText) + + Write-Debug ('TestHttp: Response.Content.Length={0}' -f $responseContent.Length) + if (!$responseContent.Contains($this.ExpectText)) { $exception = [Exception]::new(("Response does not contain text {0}" -f $response.ExpectText)) $result.Result = [System.Management.Automation.ErrorRecord]::new($exception, "2", "InvalidResult", $response) @@ -159,12 +184,16 @@ class TestCase { } } - catch [Threading.Tasks.TaskCanceledException] { + catch [System.Threading.Tasks.TaskCanceledException] { $exception = [Exception]::new(("Request timed out after {0:N2}s" -f $this.Plan.Timeout.TotalSeconds)) $result.Result = [System.Management.Automation.ErrorRecord]::new($exception, "4", "OperationTimeout", $client) } catch { - $result.Result = $_ + if ($_.Exception.GetBaseException().Message -like 'The remote certificate is invalid*') { + $result.InvalidCert = $true + } + + $result.Result = [System.Management.Automation.ErrorRecord]::new($_.Exception.GetBaseException(), "5", "ConnectionError", $client) } finally { $result.TimeTotal = (Get-Date) - $time @@ -187,11 +216,10 @@ class TestResult { [bool] $InvalidCert [timespan] $TimeTotal + TestResult () {} + TestResult ([string]$label) { $this.Label = $label } } -Add-Type -Path "$PSScriptRoot/lib/netstandard2.0/Tomlyn.dll" - -. "$PSScriptRoot/Invoke-HttpUnit.ps1" \ No newline at end of file diff --git a/src/internal/Export-HelpToMd.ps1 b/src/internal/Export-HelpToMd.ps1 new file mode 100644 index 0000000..f943d60 --- /dev/null +++ b/src/internal/Export-HelpToMd.ps1 @@ -0,0 +1,148 @@ +function Export-HelpToMd { + [CmdletBinding()] + param ( + [Parameter(Mandatory, Position = 0, ValueFromPipeline)] + [PSCustomObject] + $HelpInfo + ) + + begin { + function GetText { + param ([string]$text, [string]$default) + + $text = $text.Trim() + if ([string]::IsNullOrEmpty($text)) { + if ([string]::IsNullOrEmpty($default)) { + $default = 'No description' + } + return $default + } + return $text + } + + function GetName ([PSCustomObject]$help) { + $lines = @() + $lines += '# {0}' -f $help.Name + $lines += [System.Environment]::NewLine + return $lines + } + + function GetDescription { + param ($description, [string]$noDesc) + + $description = $description.Description.Text | Out-String + $line = '{0}' -f (GetText $description $noDesc) + + return $line + } + + function GetParameterSet ([PSCustomObject]$help) { + $lines = @() + $setNum = 1 + + $lines += '## Parameters' + + foreach ($set in $help.syntax.syntaxItem) { + $lines += [System.Environment]::NewLine + $lines += '### Parameter Set {0}' -f $setNum + $lines += [System.Environment]::NewLine + + foreach ($param in $set.Parameter) { + $paramStringParts = @() + + $paramStringParts += '- `[{0}]`' -f (GetText $param.parameterValue 'switch') + + $paramStringParts += '**{0}**' -f $param.Name + + $paramStringParts += '_{0}_ ' -f (GetDescription -description $param -noDesc 'Parameter help description') + + $attributes = @() + if ($param.required -eq 'true') { $attributes += 'Mandatory' } + if ($param.pipelineInput -like '*ByValue*') { $attributes += 'ValueFromPipeline' } + + $paramStringParts += $attributes -join ', ' + + $lines += $paramStringParts -join ' ' + } + + $setNum++ + } + + return $lines + } + + function GetExample ([PSCustomObject]$help) { + $lines = @() + $exNum = 1 + + $lines += [System.Environment]::NewLine + $lines += '## Examples' + + foreach ($exampleList in $help.examples.example) { + foreach ($example in $exampleList) { + $lines += [System.Environment]::NewLine + $lines += '### Example {0}' -f $exNum + $lines += [System.Environment]::NewLine + + $lines += $example.remarks.Text.Where({ ![string]::IsNullOrEmpty($_) }) + $lines += [System.Environment]::NewLine + + $lines += '```powershell' + $lines += $example.code.Trim("`t") + $lines += '```' + + } + $exNum++ + } + + return $lines + } + + function GetLink ([PSCustomObject]$help, $Commands) { + if ($help.relatedLinks.count -gt 0) { + $lines = @() + + $lines += [System.Environment]::NewLine + $lines += '## Links' + $lines += [System.Environment]::NewLine + + foreach ($link in $help.relatedLinks) { + + foreach ($text in $link.navigationLink.linkText) { + + if ($text -match '\w{3,}-\w{3,}') { + $uri = $text + $lines += '- [{0}]({0}.md)' -f $uri + } + + if ($text -match 'images\/.+\.png') { + $uri = $text + $lines += '- [{0}]({0})' -f $uri + } + + } + foreach ($uri in $link.navigationLink.uri) { + if (![string]::IsNullOrEmpty($uri)) { + $lines += '- [{0}]({0})' -f $uri + } + } + } + + return $lines + } + } + } + + process { + + GetName $HelpInfo + GetDescription $HelpInfo $HelpInfo.Synopsis + GetParameterSet $HelpInfo + GetExample $HelpInfo + GetLink $HelpInfo + } + + end { + + } +} diff --git a/src/lib/netstandard2.0/Tomlyn.deps.json b/src/lib/netstandard2.0/Tomlyn.deps.json deleted file mode 100644 index 79066f6..0000000 --- a/src/lib/netstandard2.0/Tomlyn.deps.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "runtimeTarget": { - "name": ".NETStandard,Version=v2.0/", - "signature": "" - }, - "compilationOptions": {}, - "targets": { - ".NETStandard,Version=v2.0": {}, - ".NETStandard,Version=v2.0/": { - "Tomlyn/0.0.0-alpha.0": { - "dependencies": { - "Microsoft.SourceLink.GitHub": "1.1.1", - "MinVer": "3.1.0", - "NETStandard.Library": "2.0.3" - }, - "runtime": { - "Tomlyn.dll": {} - } - }, - "Microsoft.Build.Tasks.Git/1.1.1": {}, - "Microsoft.NETCore.Platforms/1.1.0": {}, - "Microsoft.SourceLink.Common/1.1.1": {}, - "Microsoft.SourceLink.GitHub/1.1.1": { - "dependencies": { - "Microsoft.Build.Tasks.Git": "1.1.1", - "Microsoft.SourceLink.Common": "1.1.1" - } - }, - "MinVer/3.1.0": {}, - "NETStandard.Library/2.0.3": { - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - } - } - }, - "libraries": { - "Tomlyn/0.0.0-alpha.0": { - "type": "project", - "serviceable": false, - "sha512": "" - }, - "Microsoft.Build.Tasks.Git/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==", - "path": "microsoft.build.tasks.git/1.1.1", - "hashPath": "microsoft.build.tasks.git.1.1.1.nupkg.sha512" - }, - "Microsoft.NETCore.Platforms/1.1.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", - "path": "microsoft.netcore.platforms/1.1.0", - "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" - }, - "Microsoft.SourceLink.Common/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==", - "path": "microsoft.sourcelink.common/1.1.1", - "hashPath": "microsoft.sourcelink.common.1.1.1.nupkg.sha512" - }, - "Microsoft.SourceLink.GitHub/1.1.1": { - "type": "package", - "serviceable": true, - "sha512": "sha512-IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", - "path": "microsoft.sourcelink.github/1.1.1", - "hashPath": "microsoft.sourcelink.github.1.1.1.nupkg.sha512" - }, - "MinVer/3.1.0": { - "type": "package", - "serviceable": true, - "sha512": "sha512-a7e2zj145UlybI6NAt9cyTjqKJpvbQzX/Th7bAdD8yTPhvzHfuAaFBe+ZBA7Cb0hVXCyyeeJikhKPB8pAbIAzA==", - "path": "minver/3.1.0", - "hashPath": "minver.3.1.0.nupkg.sha512" - }, - "NETStandard.Library/2.0.3": { - "type": "package", - "serviceable": true, - "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==", - "path": "netstandard.library/2.0.3", - "hashPath": "netstandard.library.2.0.3.nupkg.sha512" - } - } -} \ No newline at end of file diff --git a/src/lib/netstandard2.0/Tomlyn.dll b/src/lib/netstandard2.0/Tomlyn.dll deleted file mode 100644 index bc429a4..0000000 Binary files a/src/lib/netstandard2.0/Tomlyn.dll and /dev/null differ diff --git a/src/lib/netstandard2.0/Tomlyn.xml b/src/lib/netstandard2.0/Tomlyn.xml deleted file mode 100644 index 435f615..0000000 --- a/src/lib/netstandard2.0/Tomlyn.xml +++ /dev/null @@ -1,1500 +0,0 @@ - - - - Tomlyn - - - - - Iterator ala Stark. - - The type of an element of the iteration. - The type of the state of the iteration. - - - - Gets the start state for the iteration. - - - - - Tries to get the next element in the iteration. - - The state. - none if no element, or an element - - - - Naming helpers used by Tomlyn. - - - - - Converts a string from pascal case (e.g `ThisIsFine`) to snake case (e.g `this_is_fine`). - - A PascalCase string to convert to snake case. - The snake case version of the input string. - - - - Allow to attach metadata to properties - - - - - Gets or sets the attached metadata for properties - - - - - Kind of an TOML object. - - - - - Transform syntax to a model. - - - - - Create metadata for model. - - The syntax used to collect the metadata from. - The metadata to attach to the property; null if no metadata. - - - - Runtime representation of a TOML array - - - - - Base class for the runtime representation of a TOML object - - - - - The kind of the object - - - - - Gets the leading trivia attached to this node. Might be null if no leading trivias. - - - - - Gets the trailing trivia attached to this node. Might be null if no trailing trivias. - - - - - Gets the trailing trivia attached to this node. Might be null if no trailing trivias. - - - - - Runtime representation of a TOML table - - - This object keep the order of the inserted key=values - - - - - Creates an instance of a - - - - - Creates an instance of . - - - - - - - - - Runtime representation of a TOML table array - - - - - Gets a boolean indicating whether this lexer has errors. - - - - - Gets error messages. - - - - - Lexer enumerator that generates , to be used from a foreach. - - - - - Initialize a new instance of this . - - The text to analyze - - If text is null - - - - Gets a boolean indicating whether this lexer has errors. - - - - - Gets error messages. - - - - - The parser. - - - - - Initializes a new instance of the class. - - The lexer. - - - - - A lightweight token struct to avoid GC allocations. - - - - - Initializes a new instance of the struct. - - The type. - The start. - The end. - Optional parse value of the token - - - - - The type of token. - - - - - The start position of this token. - - - - - The end position of this token. - - - - - The parsed value - - - - - An item of an - - - - - Creates an instance of - - - - - Gets or sets the value of this item. - - - - - Gets or sets the comma of this item (mandatory to separate elements in an array) - - - - - An array TOML node. - - - - - Creates an instance of an - - - - - Creates an instance of an - - An array of integer values - - - - Creates an instance of an - - An array of string values - - - - Gets or sets the open bracket `[` token - - - - - Gets the of this array. - - - - - Gets or sets the close bracket `]` token - - - - - Base class for a or a - - - - - A TOML bare key syntax node. - - - - - Creates a new instance of a - - - - - Creates a new instance of a - - The name used for this key - - - - A textual representation of the key - - - - - A boolean TOML value syntax node. - - - - - Creates an instance of a - - - - - Creates an instance of a - - The boolean value - - - - The boolean token value (true or false) - - - - - The boolean parsed value. - - - - - A datetime TOML value syntax node. - - - - - Creates an instance of - - The kind of datetime - - - - Gets or sets the datetime token. - - - - - Gets or sets the parsed datetime value. - - - - - A diagnostic message with errors. - - - - - Creates a new instance of a - - The kind of message - The source span - The message - - - - Gets the kind of message. - - - - - Gets the source span. - - - - - Gets the message. - - - - - Kind of a - - - - - An error message. - - - - - A warning message. - - - - - A container for - - - - - Creates a new instance of a - - - - - Creates a new instance of a . - - An existing list of messages. - - - - Gets the number of messages. - - - - - Gets the message at the specified index. - - Index of the message. - A diagnostic message. - - - - Gets a boolean indicating if this bag contains any error messages. - - - - - Adds the specified message to this bag. - - The message to add - - - - Adds the specified list of messages to this bag. - - A list of messages. - - - - Clear this bag including the error state. - - - - - Adds a warning message - - The source span - The warning message - - - - Adds an error message - - The source span - The error message - - - - Gets the enumerator of - - - - - - Root TOML syntax tree - - - - - Creates an instance of a - - - - - Gets the diagnostics attached to this document. - - - - - Gets a boolean indicating if the has any errors. - - - - - Gets the list of - - - - - Gets the list of tables (either or ) - - - - - A part of a TOML dotted key used by - - - - - Creates an instance of - - - - - Creates an instance of - - The key used after the `.` - - - - The token `.` - - - - - The following key or string node. - - - - - A float TOML value syntax node. - - - - - Creates an instance of - - - - - Creates an instance of - - The double value - - - - The token storing the float value. - - - - - The parsed value of the - - - - - A key-value pair item of an inline table. - - - - - Creates an instance of - - - - - Creates an instance of - - The key=value - - - - Gets or sets the . - - - - - Gets or sets the comma, mandatory to separate entries in an inline table. - - - - - An inline table TOML syntax node. - - - - - Creates a new instance of an - - - - - Creates a new instance of an - - The key values of this inline table - - - - The token open brace `{` - - - - - The items of this table. - - - - - The token close brace `}` - - - - - An integer TOML value syntax node. - - - - - Creates an - - - - - Creates an - - The integer value - - - - The integer token with its textual representation - - - - - The parsed integer value - - - - - Represents an invalid - - - - - The kind of token which is invalid for the context. - - - - - A key TOML syntax node. - - - - - Creates a new instance of a - - - - - Creates a new instance of a - - A simple name of this key - - - - Creates a new instance of a - - the base key - the key after the dot - - - - The base of the key before the dot - - - - - List of the dotted keys. - - - - - A TOML key = value syntax node. - - - - - Creates an instance of - - - - - Creates an instance of - - The key - The value - - - - Creates an instance of - - The key - The value - - - - Gets or sets the key. - - - - - Gets or sets the `=` token - - - - - Gets or sets the value - - - - - Gets or sets the new-line token. - - - - - A textual source span. - - - - - Creates a source span. - - - - - - - - Gets or sets the filename. - - - - - Gets the starting offset of this span. - - - - - Gets the length of this span. - - - - - Gets or sets the starting text position. - - - - - Gets or sets the ending text position. - - - - - A string representation of this source span not including the position. - - - - - A string TOML syntax value node. - - - - - Creates a new instance of - - - - - Creates a new instance of - - String value used for this node - - - - The token of the string. - - - - - The associated parsed string value - - - - - A factory for - - - - - Creates a trivia whitespace. - - A trivia whitespace. - - - - Creates a newline trivia. - - A new line trivia - - - - Creates a comment trivia. - - A comment trivia - A comment trivia - - - - Creates a newline token. - - A new line token - - - - Creates a token from the specified token kind. - - The token kind - The token - - - - Defines the kind for a - - - - - Abstract list of - - - - - Abstract list of - - Type of the node - - - - Creates an instance of - - - - - Adds the specified node to this list. - - Node to add to this list - - - - Removes a node at the specified index. - - Index of the node to remove - - - - Removes the specified node instance. - - Node instance to remove - - - - Gets the default enumerator. - - The enumerator of this list - - - - Enumerator of a - - - - - Initialize an enumerator with a list of - - - - - - Base class used to define a TOML Syntax tree. - - - - - Gets the type of node. - - - - - Gets the leading trivia attached to this node. Might be null if no leading trivias. - - - - - Gets the trailing trivia attached to this node. Might be null if no trailing trivias. - - - - - Gets the number of children. - - - - - Gets a child at the specified index. - - Index of the child - A child at the specified index - - - - Gets a child at the specified index. - - Index of the child - A child at the specified index - The index is safe to use - - - - Writes this node to a textual TOML representation - - A writer to receive the TOML output - - - - Helper method to deparent/parent a node to this instance. - - Type of the node - The previous child node parented to this instance - The new child node to parent to this instance - - - - Helper method to deparent/parent a to this instance with an expected kind of token. - - Type of the node - The previous child node parented to this instance - The new child node to parent to this instance - The expected kind of token - - - - Helper method to deparent/parent a to this instance with an expected kind of token condition. - - Type of the node - The type of message - The previous child node parented to this instance - The new child node to parent to this instance - true if kind is matching, false otherwise - The message to display if the kind is not matching - - - - Helper method to deparent/parent a to this instance with an expected kind of token. - - Type of the node - The previous child node parented to this instance - The new child node to parent to this instance - The expected kind of token (option1) - The expected kind of token (option2) - - - - Base class for and - - - - - The text source span, read-write, manually updated from children. - - - - - Allow to visit this instance with the specified visitor. - - The visitor - - - - Gets the parent of this node. - - - - - Extensions for . - - - - - Get all and in Depth-First-Search order of the node. - - The node to collect all tokens from - true to include comments and whitespaces. - All descendants in Depth-First-Search order of the node. - - - - Get all descendants in Depth-First-Search order of the node. Note that this method returns the node itself (last). - - The node to collect all descendants - true to include tokens, comments and whitespaces. - All descendants in Depth-First-Search order of the node. - - - - A token node. - - - - - Creates a new instance of - - - - - Creates a new instance of - - The type of token - The associated textual representation - - - - Gets or sets the kind of token. - - - - - Gets or sets the associated text - - - - - Base class for a or a - - - - - Gets or sets the open bracket (simple `[` for , double `[[` for ) - - - - - Gets or sets the name of this table - - - - - Gets or sets the close bracket (simple `]` for , double `]]` for ) - - - - - Gets the new-line. - - - - - Gets the key-values associated with this table. - - - - - A position within a text (offset, line column) - - - - - Creates a new instance of a - - Offset in the source text - Line number - zero based - Column number - zero based - - - - Gets or sets the offset. - - - - - Gets or sets the column number (zero based) - - - - - Gets or sets the line number (zero based) - - - - - An enumeration to categorize tokens. - - - - - Helper functions for - - - - - Returns true if the specified token kind is considered as hidden (comments, whitespaces or newline) - - The token kind. - Makes the newline hidden by default. Default is true. - true if the specified token kind is considered as hidden; false otherwise - - - - Gets a textual representation of a token kind or null if not applicable (e.g TokenKind.Integer) - - A token kind - A textual representation of a token kind or null if not applicable (e.g TokenKind.Integer) - - - - Checks if the specified kind is a float. - - A token kind - true if the specified kind is a float. - - - - Checks if the specified kind is an integer - - A token kind - true if the specified kind is an integer. - - - - Checks if the specified kind is a datetime. - - A token kind - true if the specified kind is a datetime. - - - - Checks if the specified kind is a string. - - A token kind - true if the specified kind is a string - - - - Checks if the specified kind is a trivia. - - A token kind - true if the specified kind is a trivia - - - - Checks if the specified kind is a token for which will return not null - - A token kind - true if the specified kind is a simple token - - - - Base class for all TOML values. - - - - - A UTF-32 character ala Stark. - - - - - Initializes a new instance of the UTF-32 character. - - The UTF-32 code character. - - - - Gets the UTF-32 code. - - - - - Performs an implicit conversion from to . - - The c. - The result of the conversion. - - - - Performs an implicit conversion from to . - - The c. - The result of the conversion. - - - - (trait) CharacterIterator ala Stark - - - - - Escape a C# string to a TOML string - - - - - Converts a string that may have control characters to a printable string - - - - - Main entry class to parse, validate and transform to a model a TOML document. - - - - - Shows version - - - - - Parses a text to a TOML document. - - A string representing a TOML document - An optional path/file name to identify errors - Options for parsing. Default is parse and validate. - A parsed TOML document - - - - Parses a UTF8 byte array to a TOML document. - - A UTF8 string representing a TOML document - An optional path/file name to identify errors - Options for parsing. Default is parse and validate. - A parsed TOML document - - - - Validates the specified TOML document. - - The TOML document to validate - The same instance as the parameter. Check and for details. - - - - Gets the TOML string representation from the specified model. - - The type of the mode - Optional parameters for the serialization. - The TOML string representation from the specified model. - If there are errors while trying to serialize to a TOML string. - - - - Tries to get the TOML string representation from the specified model. - - The model instance to serialize to TOML. - The TOML string representation from the specified model if this method returns true. - The diagnostics error messages if this method returns false. - Optional parameters for the serialization. - The TOML string representation from the specified model. - true if the conversion was successful; false otherwise. - - - - Tries to get the TOML string representation from the specified model. - - The model instance to serialize to TOML. - The TOML string representation written to a if this method returns true. - The diagnostics error messages if this method returns false. - Optional parameters for the serialization. - The TOML string representation from the specified model. - true if the conversion was successful; false otherwise. - - - - Parses a TOML text to directly to a model. - - A string representing a TOML document - An optional path/file name to identify errors - The options for the mapping. - A parsed TOML document returned as . - - - - Parses a TOML text to directly to a model. - - A string representing a TOML document - An optional path/file name to identify errors - The options for the mapping. - A parsed TOML document - - - - Tries to parses a TOML text directly to a model. - - A string representing a TOML document - The output model. - The diagnostics if this method returns false. - An optional path/file name to identify errors - The options for the mapping. - A parsed TOML document - - - - Converts a to a - - A TOML document - A , a runtime representation of the TOML document - - - - Converts a to the specified runtime object. - - The runtime object to map the syntax to - The syntax to map from. - The options for the mapping. - The result of mapping to a runtime model of type T - If there were errors when mapping to properties. - - - - Tries to convert a to the specified runtime object. - - The runtime object to map the syntax to - The syntax to map from. - The output model. - The diagnostics if this method returns false. - The options for the mapping. - true if the mapping was successful; false otherwise. In that case the output will contain error messages. - - - - A datetime value that can represent a TOML - - An offset DateTime with Zero offset or Number offset. - - A local DateTime. - - A local Date. - - A local Time. - - The datetime offset. - The precision of milliseconds. - The kind of datetime offset. - - - - A datetime value that can represent a TOML - - An offset DateTime with Zero offset or Number offset. - - A local DateTime. - - A local Date. - - A local Time. - - The datetime offset. - The precision of milliseconds. - The kind of datetime offset. - - - The datetime offset. - - - The precision of milliseconds. - - - The kind of datetime offset. - - - - Gets the string formatter for the specified precision. - - A precision number from 1 to 7. Other numbers will return the default `fff` formatter. - The string formatter for the specified precision. - - - - Converts a datetime to TomlDateTime. - - - - - Offsets used for a - - - - - Default member to create an instance. - - - - - Default convert name using snake case via help . - - - - - Default convert name using snake case via help . - - - - - Gets or sets the delegate to retrieve a name from a property. If this function returns null, the property is ignored. - - - - - Gets or sets the delegate to retrieve a field from a property. If this function returns null, the field is ignored. - - - - - Gets or sets the delegate used to convert the name of the property to the name used in TOML. By default, it is using snake case via . - - - This delegate is used by the default delegate. - - - - - Gets or sets the delegate used to convert the name of the field to the name used in TOML. By default, it is using snake case via . - - - This delegate is used by the default delegate. - - - - - Gets or sets the function used when deserializing from TOML to create instance of objects. Default is set to . - The arguments of the function are: - - The type to create an instance for. - - The expected from a TOML perspective. - Returns an instance according to the type and the expected . - - - - - Gets or sets the convert function called when deserializing a value from TOML to a model (e.g string to Uri). - - Must return null if cannot convert. The arguments of the function are: - - The input object value to convert. - - The target type to convert to. - - Returns an instance of target type converted from the input value or null if conversion is not supported. - - - - - Gets or sets the convert function called when deserializing a value from TOML to a model (e.g string to Uri). - - Must return null if cannot convert. The arguments of the function are: - - The input object value to convert. - - The target type to convert to. - - Returns an instance of target type converted from the input value or null if conversion is not supported. - - - - - Gets or sets the convert function called when serializing a value from a model to a TOML representation. - This function allows to substitute a value to another type before converting (e.g Uri to string). - - - - - Gets the list of the attributes used to ignore a property. - - - By default, the list contains: - - System.Runtime.Serialization.IgnoreDataMemberAttribute - - System.Text.Json.Serialization.JsonPropertyNameAttribute - - - - - Gets the list of the attributes used to fetch the property `Name`. - - - By default, the list contains: - - System.Runtime.Serialization.DataMemberAttribute - - System.Text.Json.Serialization.JsonPropertyNameAttribute - - - - - Gets or sets the option to ignore properties in the TOML that are missing from a custom model - - - By default this is false - - - - - Gets or sets the option to include fields from a custom model in the TOML - - - By default this is false - - - - - Default implementation for getting the property name - - - - - Default implementation for getting the field name - - - - - Options for parsing a TOML string. - - - - - Parse and validate. - - - - - Parse only the document. - - - - diff --git a/src/Invoke-HttpUnit.ps1 b/src/public/Invoke-HttpUnit.ps1 similarity index 78% rename from src/Invoke-HttpUnit.ps1 rename to src/public/Invoke-HttpUnit.ps1 index 4a36b83..64b10e0 100644 --- a/src/Invoke-HttpUnit.ps1 +++ b/src/public/Invoke-HttpUnit.ps1 @@ -5,10 +5,9 @@ function Invoke-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. + Specifies a path to a configuration file with a list of tests. Supported types are .toml, .yml, and .psd1. .PARAMETER Tag - If specified, only runs plans that are tagged with one of the - tags specified. + If specified, only runs plans that are tagged with one of the tags specified. .PARAMETER Url The URL to retrieve. .PARAMETER Code @@ -21,9 +20,12 @@ function Invoke-HttpUnit { 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 Quiet + Do not output ErrorRecords for failed tests. .EXAMPLE PS > Invoke-HttpUnit -Url https://www.google.com -Code 200 - Label : https://www.google.com/ Result : Connected : True @@ -33,9 +35,11 @@ function Invoke-HttpUnit { GotHeaders : False InvalidCert : False TimeTotal : 00:00:00.4695217 + + Run an ad-hoc test against one Url. + .EXAMPLE PS > Invoke-HttpUnit -Path .\example.toml - Label : google Result : Connected : True @@ -45,7 +49,6 @@ function Invoke-HttpUnit { 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 @@ -55,7 +58,6 @@ function Invoke-HttpUnit { GotHeaders : False InvalidCert : False TimeTotal : 00:00:00.0280893 - Label : redirect Result : Unexpected status code: NotFound Connected : True @@ -65,6 +67,8 @@ function Invoke-HttpUnit { GotHeaders : False InvalidCert : False TimeTotal : 00:00:00.1021738 + + Run all of the tests in a given config file. .NOTES A $null Results property signifies no error and all specified test criteria passed. @@ -72,6 +76,8 @@ function Invoke-HttpUnit { 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 +.LINK + https://github.com/cdhunt/Import-ConfigData #> [CmdletBinding(DefaultParameterSetName = 'url')] @@ -96,44 +102,61 @@ function Invoke-HttpUnit { [Parameter(Mandatory, Position = 0, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [Alias('Address', 'ComputerName')] [string] $Url, [Parameter(Position = 1, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [Alias('StatusCode')] [string] $Code, [Parameter(Position = 2, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [Alias('Text')] [string] $String, - [Parameter(Position = 3, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [hashtable] $Headers, [Parameter(Position = 4, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [timespan] $Timeout, [Parameter(Position = 5, - ParameterSetName = 'url')] + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] [X509Certificate] - $Certificate + $Certificate, + + [Parameter(Position = 6, + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Connect', 'Delete', 'Get', 'Head', 'Options', 'Patch', 'Post', 'Put', 'Trace')] + [String] + $Method, + + [Parameter()] + [Switch] + $Quiet ) if ($PSBoundParameters.ContainsKey('Path')) { - $configContent = Get-Content -Path $Path -Raw + Write-Debug "Running checks defined in '$Path'" - $configObject = [Tomlyn.Toml]::ToModel($configContent) + + $configObject = Import-ConfigData -Path $Path foreach ($plan in $configObject['plan']) { $testPlan = [TestPlan]@{ @@ -143,15 +166,12 @@ function Invoke-HttpUnit { 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' { - $asHash = @{} - $plan[$_].ForEach({ $asHash.Add($_.Key, $_.Value) }) - $testPlan.Headers = $asHash - } + 'headers' { $testPlan.Headers = $plan[$_] } 'certficate' { $value = $plan[$_] if ($value -like 'cert:\*') { @@ -177,6 +197,7 @@ function Invoke-HttpUnit { Continue } } + foreach ($case in $testPlan.Cases()) { $case.Test() } @@ -192,13 +213,14 @@ function Invoke-HttpUnit { 'Headers' { $plan.Headers = $Headers } 'Timeout' { $plan.Timeout = $Timeout } 'Certificate' { $plan.ClientCertificate = $Certificate } + 'Method' { $plan.Method = $Method } } foreach ($case in $plan.Cases()) { $result = $case.Test() Write-Output $result - if ($null -ne $result.Result) { + if ($null -ne $result.Result -and !$Quiet) { Write-Error -ErrorRecord $result.Result } } diff --git a/test/httpunitps.Tests.ps1 b/test/httpunitps.Tests.ps1 new file mode 100644 index 0000000..2f78791 --- /dev/null +++ b/test/httpunitps.Tests.ps1 @@ -0,0 +1,105 @@ +BeforeAll { + + Import-Module "$PSScriptRoot/../publish/httpunitPS" -Force + +} + +Describe 'Invoke-HttpUnit' { + Context 'By Value' { + It 'Should return 200 for google' { + $result = Invoke-HttpUnit -Url https://www.google.com -Code 200 + + $result.Label | Should -Be "https://www.google.com/" + $result.Result | Should -BeNullOrEmpty + $result.Connected | Should -Be $True + $result.GotCode | Should -Be $True + $result.GotText | Should -Be $False + $result.GotRegex | Should -Be $False + $result.GotHeaders | Should -Be $False + $result.InvalidCert | Should -Be $False + $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) + } + It 'Should support string matching' { + $result = Invoke-HttpUnit -Url https://example.com/ -String 'Example Domain' + + $result.Label | Should -Be "https://example.com/" + $result.Result | Should -BeNullOrEmpty + $result.Connected | Should -Be $True + $result.GotCode | Should -Be $True + $result.GotText | Should -Be $true + $result.GotRegex | Should -Be $False + $result.GotHeaders | Should -Be $False + $result.InvalidCert | Should -Be $False + $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) + } + + It 'Should report a bad cert' { + $result = Invoke-HttpUnit -Url https://expired.badssl.com/ -Quiet + + $result.Result | Should -not -BeNullOrEmpty + $result.Connected | Should -Be $false + $result.InvalidCert | Should -Be $true + if ($PSVersionTable.PSVersion -ge [version]"7.3") { + $result.Result.Exception.Message | Should -Be 'The remote certificate is invalid because of errors in the certificate chain: NotTimeValid' + } + else { + $result.Result.Exception.Message | Should -Be 'The remote certificate is invalid according to the validation procedure.' + } + } + } + Context 'By Config' { + It 'Should return 200 for google and find header {Server = "gws"} []' -ForEach @( + @{ config = "$PSScriptRoot/testconfig1.psd1"; type = 'PSD1' } + @{ config = "$PSScriptRoot/testconfig1.toml"; type = 'TOML' } + ) { + $result = Invoke-HttpUnit -Path $config + + $result.Label | Should -BeExactly "google" + $result.Result | Should -BeNullOrEmpty + $result.Connected | Should -Be $True + $result.GotCode | Should -Be $True + $result.GotText | Should -Be $False + $result.GotRegex | Should -Be $False + $result.GotHeaders | Should -Be $true + $result.InvalidCert | Should -Be $False + $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) + } + + It 'Should filter by tag' { + $result = Invoke-HttpUnit -Path "$PSScriptRoot/testconfig2.yaml" -Tag Run + + $result.Label | Should -BeExactly "good" + $result.Result | Should -BeNullOrEmpty + $result.Connected | Should -Be $True + $result.GotCode | Should -Be $True + $result.GotText | Should -Be $False + $result.GotRegex | Should -Be $False + $result.GotHeaders | Should -Be $False + $result.InvalidCert | Should -Be $False + $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) + } + } + Context 'By Value by Pipeline' { + It 'Should return 200 for google' { + $inputObject = [PSCustomObject]@{ + Url = 'https://www.google.com/' + Code = 200 + } + $result = $inputObject | Invoke-HttpUnit + + $result.Label | Should -Be "https://www.google.com/" + $result.Result | Should -BeNullOrEmpty + $result.Connected | Should -Be $True + $result.GotCode | Should -Be $True + $result.GotText | Should -Be $False + $result.GotRegex | Should -Be $False + $result.GotHeaders | Should -Be $False + $result.InvalidCert | Should -Be $False + $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) + } + } +} + +AfterAll { + Remove-Module httpunitPS +} \ No newline at end of file diff --git a/test/testconfig1.psd1 b/test/testconfig1.psd1 new file mode 100644 index 0000000..75eee34 --- /dev/null +++ b/test/testconfig1.psd1 @@ -0,0 +1,11 @@ +@{ + Plan = @( + @{ + label = "google" + url = "https://www.google.com" + code = 200 + timeout = "0:0:10" + headers = @{Server = "gws" } + } + ) +} \ No newline at end of file diff --git a/test/testconfig1.toml b/test/testconfig1.toml new file mode 100644 index 0000000..05ad561 --- /dev/null +++ b/test/testconfig1.toml @@ -0,0 +1,7 @@ +[[plan]] + label = "google" + url = "https://www.google.com" + code = 200 + timeout = "0:0:10" + [plan.headers] + Server = "gws" \ No newline at end of file diff --git a/test/testconfig2.yaml b/test/testconfig2.yaml new file mode 100644 index 0000000..7a252bb --- /dev/null +++ b/test/testconfig2.yaml @@ -0,0 +1,11 @@ +Plan: +- code: 200 + label: good + timeout: 0:0:10 + url: https://www.google.com + tags: [run] +- code: 200 + label: bad + timeout: 0:0:10 + url: bad://www.google.com + tags: [do-not-run] \ No newline at end of file diff --git a/version.json b/version.json new file mode 100644 index 0000000..b3a00a5 --- /dev/null +++ b/version.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.4.0", + "cloudBuild": { + "buildNumber": { + "enabled": true + } + } +} \ No newline at end of file