From 3f9564e665a976b98ef08c793def98e32f1b8748 Mon Sep 17 00:00:00 2001 From: Mark Kraus Date: Tue, 16 Jan 2018 11:23:22 -0600 Subject: [PATCH] Replace HttpListener Link Header Tests with WebListener (#5806) - Add Link controller to WebListener - Replace HttpListener Link tests with WebListener - Update WebListener Documentation - Enable cross-platform multiple Link header tests --- .../WebCmdlets.Tests.ps1 | 84 ++++++++------- .../Modules/WebListener/WebListener.psm1 | 1 + test/tools/WebListener/Constants.cs | 6 ++ .../WebListener/Controllers/LinkController.cs | 102 ++++++++++++++++++ test/tools/WebListener/README.md | 61 +++++++++++ .../tools/WebListener/Views/Home/Index.cshtml | 3 +- 6 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 test/tools/WebListener/Controllers/LinkController.cs diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index c83f6a1fd8..41d4af5206 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -712,33 +712,31 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { } It "Validate Invoke-WebRequest returns valid RelationLink property with absolute uris if Link Header is present" { - $command = "Invoke-WebRequest -Uri 'http://localhost:8080/PowerShell?test=linkheader&maxlinks=5'" - $result = ExecuteWebCommand -command $command - $result.Output.RelationLink.Count | Should BeExactly 2 - $result.Output.RelationLink["next"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=5&linknumber=2" - $result.Output.RelationLink["last"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=5&linknumber=5" - } - - # Test pending support for multiple header capable server on Linux/macOS see issue #4639 - It "Validate Invoke-WebRequest returns valid RelationLink property with absolute uris if Multiple Link Headers are present" -Pending:$(!$IsWindows) { - $Query = @{ - body = "ok" - contenttype = 'text/plain' - headers = @{ - Link = @( - '; rel="self"' - '; rel="next"' - '; rel="last"' - ) - } | ConvertTo-Json -Compress - } - $Uri = Get-WebListenerUrl -Test 'Response' -Query $Query + $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = 5; linknumber = 2} $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command - $result.Output.RelationLink.Count | Should BeExactly 3 - $result.Output.RelationLink["self"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=5&linknumber=1" - $result.Output.RelationLink["next"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=5&linknumber=2" - $result.Output.RelationLink["last"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=5&linknumber=5" + + $result.Output.RelationLink.Count | Should BeExactly 5 + $baseUri = Get-WebListenerUrl -Test 'Link' + $result.Output.RelationLink["next"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=3&type=default" + $result.Output.RelationLink["last"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=5&type=default" + $result.Output.RelationLink["prev"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=default" + $result.Output.RelationLink["first"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=default" + $result.Output.RelationLink["self"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=2&type=default" + } + + It "Validate Invoke-WebRequest returns valid RelationLink property with absolute uris if Multiple Link Headers are present" { + $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = 5; linknumber = 2; type = 'multiple'} + $command = "Invoke-WebRequest -Uri '$uri'" + $result = ExecuteWebCommand -command $command + + $result.Output.RelationLink.Count | Should BeExactly 5 + $baseUri = Get-WebListenerUrl -Test 'Link' + $result.Output.RelationLink["next"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=3&type=multiple" + $result.Output.RelationLink["last"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=5&type=multiple" + $result.Output.RelationLink["prev"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=multiple" + $result.Output.RelationLink["first"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=1&type=multiple" + $result.Output.RelationLink["self"] | Should BeExactly "${baseUri}?maxlinks=5&linknumber=2&type=multiple" } It "Validate Invoke-WebRequest quietly ignores invalid Link Headers in RelationLink property: " -TestCases @( @@ -747,10 +745,15 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { @{ type = "noRel" } ) { param($type) - $command = "Invoke-WebRequest -Uri 'http://localhost:8080/PowerShell?test=linkheader&type=$type'" + $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} + $command = "Invoke-WebRequest -Uri '$uri'" $result = ExecuteWebCommand -command $command - $result.Output.RelationLink.Count | Should BeExactly 1 - $result.Output.RelationLink["last"] | Should BeExactly "http://localhost:8080/PowerShell?test=linkheader&maxlinks=3&linknumber=3" + + $result.Output.RelationLink.Count | Should BeExactly 3 + $baseUri = Get-WebListenerUrl -Test 'Link' + $result.Output.RelationLink["last"] | Should BeExactly "${baseUri}?maxlinks=3&linknumber=3&type=${type}" + $result.Output.RelationLink["first"] | Should BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" + $result.Output.RelationLink["self"] | Should BeExactly "${baseUri}?maxlinks=3&linknumber=1&type=${type}" } #region Redirect tests @@ -1030,9 +1033,7 @@ Describe "Invoke-WebRequest tests" -Tags "Feature" { $result.Output.RawContent | Should Match ([regex]::Escape('Content-Length: 2')) } - # Test pending due to limitation on Linux/macOS - # https://github.com/PowerShell/PowerShell/pull/4640 - It "Verifies Invoke-WebRequest Supports Multiple response headers with same name" -Pending:$(!$IsWindows) { + It "Verifies Invoke-WebRequest Supports Multiple response headers with same name" { $query = @{ contenttype = 'text/plain' body = 'OK' @@ -1723,23 +1724,23 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { It "Validate Invoke-RestMethod -FollowRelLink correctly follows all the available relation links" { $maxLinks = 5 - - $command = "Invoke-RestMethod -Uri 'http://localhost:8081/PowerShell?test=linkheader&maxlinks=$maxlinks' -FollowRelLink" + $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = $maxLinks} + $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command - $result.Output.output.Count | Should BeExactly $maxLinks - 1..$maxLinks | ForEach-Object { $result.Output.output[$_ - 1] | Should BeExactly $_ } + $result.Output.Count | Should BeExactly $maxLinks + 1..$maxLinks | ForEach-Object { $result.Output[$_ - 1].linknumber | Should BeExactly $_ } } It "Validate Invoke-RestMethod -FollowRelLink correctly limits to -MaximumRelLink" { $maxLinks = 10 $maxLinksToFollow = 6 - - $command = "Invoke-RestMethod -Uri 'http://localhost:8081/PowerShell?test=linkheader&maxlinks=$maxlinks' -FollowRelLink -MaximumFollowRelLink $maxLinksToFollow" + $uri = Get-WebListenerUrl -Test 'Link' -Query @{maxlinks = $maxLinks} + $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink -MaximumFollowRelLink $maxLinksToFollow" $result = ExecuteWebCommand -command $command - $result.Output.output.Count | Should BeExactly $maxLinksToFollow - 1..$maxLinksToFollow | ForEach-Object { $result.Output.output[$_ - 1] | Should BeExactly $_ } + $result.Output.Count | Should BeExactly $maxLinksToFollow + 1..$maxLinksToFollow | ForEach-Object { $result.Output[$_ - 1].linknumber | Should BeExactly $_ } } It "Validate Invoke-RestMethod quietly ignores invalid Link Headers if -FollowRelLink is specified: " -TestCases @( @@ -1748,9 +1749,10 @@ Describe "Invoke-RestMethod tests" -Tags "Feature" { @{ type = "noRel" } ) { param($type) - $command = "Invoke-RestMethod -Uri 'http://localhost:8081/PowerShell?test=linkheader&type=$type' -FollowRelLink" + $uri = Get-WebListenerUrl -Test 'Link' -Query @{type = $type} + $command = "Invoke-RestMethod -Uri '$uri' -FollowRelLink" $result = ExecuteWebCommand -command $command - $result.Output.output | Should BeExactly 1 + $result.Output.linknumber | Should BeExactly 1 } #region Redirect tests diff --git a/test/tools/Modules/WebListener/WebListener.psm1 b/test/tools/Modules/WebListener/WebListener.psm1 index b34ac9256e..7f9c69e307 100644 --- a/test/tools/Modules/WebListener/WebListener.psm1 +++ b/test/tools/Modules/WebListener/WebListener.psm1 @@ -135,6 +135,7 @@ function Get-WebListenerUrl { 'Encoding', 'Get', 'Home', + 'Link', 'Multipart', 'Patch', 'Post', diff --git a/test/tools/WebListener/Constants.cs b/test/tools/WebListener/Constants.cs index b36a4e8081..bb6c3d3c86 100644 --- a/test/tools/WebListener/Constants.cs +++ b/test/tools/WebListener/Constants.cs @@ -6,6 +6,12 @@ namespace mvc.Controllers { public const string HeaderSeparator = ", "; public const string ApplicationJson = "application/json"; + public const string LinkUriTemplate = "<{0}?maxlinks={1}&linknumber={2}&type={3}>; rel=\"{4}\""; + public const string MalformedUrlLinkHeader = "{url}; foo"; + public const string NoRelLinkHeader = "; foo=\"bar\""; + public const string NoUrlLinkHeader = "<>; rel=\"next\""; + + } internal static class StatusCodes diff --git a/test/tools/WebListener/Controllers/LinkController.cs b/test/tools/WebListener/Controllers/LinkController.cs new file mode 100644 index 0000000000..3242e35326 --- /dev/null +++ b/test/tools/WebListener/Controllers/LinkController.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Primitives; +using mvc.Models; + +namespace mvc.Controllers +{ + public class LinkController : Controller + { + public JsonResult Index() + { + if (!Request.Query.TryGetValue("maxlinks", out StringValues maxLinksSV) || !Int32.TryParse(maxLinksSV.FirstOrDefault(), out int maxLinks) || maxLinks < 1) + { + maxLinks = 3; + } + + if (!Request.Query.TryGetValue("linknumber", out StringValues linkNumberSV) || !Int32.TryParse(linkNumberSV.FirstOrDefault(), out int linkNumber) || linkNumber < 1) + { + linkNumber = 1; + } + + string baseUri = Regex.Replace(UriHelper.GetDisplayUrl(Request), "\\?.*", String.Empty); + + string type = Request.Query.TryGetValue("type", out StringValues typeSV) ? typeSV.FirstOrDefault() : "default"; + + var linkList = new List(); + if (maxLinks > 1 && linkNumber > 1) + { + linkList.Add(GetLink(baseUri: baseUri, maxLinks: maxLinks, linkNumber: linkNumber - 1, type: type, rel: "prev")); + } + linkList.Add(GetLink(baseUri: baseUri, maxLinks: maxLinks, linkNumber: maxLinks, type: type, rel: "last")); + linkList.Add(GetLink(baseUri: baseUri, maxLinks: maxLinks, linkNumber: 1, type: type, rel: "first")); + linkList.Add(GetLink(baseUri: baseUri, maxLinks: maxLinks, linkNumber: linkNumber, type: type, rel: "self")); + + bool sendMultipleHeaders = false; + bool skipNextLink = false; + switch (type.ToUpper()) + { + case "NOURL": + linkList.Add(Constants.NoUrlLinkHeader); + skipNextLink = true; + break; + case "MALFORMED": + linkList.Add(Constants.MalformedUrlLinkHeader); + skipNextLink = true; + break; + case "NOREL": + linkList.Add(Constants.NoRelLinkHeader); + skipNextLink = true; + break; + case "MULTIPLE": + sendMultipleHeaders = true; + break; + default: + break; + } + + if (!skipNextLink && maxLinks > 1 && linkNumber < maxLinks) + { + linkList.Add(GetLink(baseUri: baseUri, maxLinks: maxLinks, linkNumber: linkNumber + 1, type: type, rel: "next")); + } + + StringValues linkHeader; + if (sendMultipleHeaders) + { + linkHeader = linkList.ToArray(); + } + else + { + linkHeader = String.Join(",", linkList); + } + Response.Headers.Add("Link", linkHeader); + + // Generate /Get/ result and append linknumber, maxlinks, and type + var getController = new GetController(); + getController.ControllerContext = this.ControllerContext; + var result = getController.Index(); + var output = result.Value as Hashtable; + output.Add("linknumber", linkNumber); + output.Add("maxlinks", maxLinks); + output.Add("type", type.FirstOrDefault()); + + return result; + } + + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + private string GetLink(string baseUri, int maxLinks, int linkNumber, string type, string rel) + { + return String.Format(Constants.LinkUriTemplate, baseUri, maxLinks, linkNumber, type, rel); + } + } +} diff --git a/test/tools/WebListener/README.md b/test/tools/WebListener/README.md index 91eb1e9872..f589a54432 100644 --- a/test/tools/WebListener/README.md +++ b/test/tools/WebListener/README.md @@ -265,6 +265,67 @@ Invoke-RestMethod -Uri $uri -Body @{TestField = 'TestValue'} } ``` +### /Link/ + +Returns Link response headers to test paginated results. The endpoint accepts 3 query items: + +* `linknumber` - The current link number. This determines the current page. If not supplied or less than 1, this will be set to 1. +* `maxlinks` - The maximum number of links. This determines the last page. If not supplied or less than 1, this will be set to 3. +* `type` - The type of link to return. When not supplied or not in the list below, `default` will be used. + * `default` - Does not return any special test links and returns `next` link if one is available. + * `norel` - Returns a Link header that does not include the `rel=` portion. Suppresses `next` link. + * `nourl` - Returns a Link header that does not include the URI portion. Suppresses `next` link. + * `malformed` - Returns a malformed Link header. Suppresses `next` link. + * `multiple` - Returns multiple Link headers instead of a single Link header and returns `next` link if one is available. + +The body will contain the same results as `/Get/` with the addition of the `type`, `linknumber`, and `maxlinks` for the current page. + +```powershell +$Query = @{ + linknumber = 1 + maxlinks = 3 + type = 'default' +} +$Uri = Get-WebListenerUrl -Test 'Link' -Query $Query +Invoke-RestMethod -Uri $uri -FollowRelLink -MaximumFollowRelLink 1 +``` + +Headers: + +```none +HTTP/1.1 200 OK +Date: Sat, 06 Jan 2018 14:27:36 GMT +Content-Type: application/json; charset=utf-8 +Server: Kestrel +Transfer-Encoding: chunked +Link: ; rel="last",; rel="first",; rel="self",; rel="next" +``` + +Body: + +```json +{ + "type": "default", + "url": "http://localhost:8083/Link/?maxlinks=3&linknumber=1&type=default", + "maxlinks": 3, + "linknumber": 1, + "headers": { + "User-Agent": "insomnia/5.12.4", + "Accept": "*/*", + "Content-Length": "0", + "Host": "localhost:8083", + "Content-Type": "application/json" + }, + "args": { + "linknumber": "1", + "maxlinks": "3", + "type": "default" + }, + "origin": "127.0.0.1", + "method": "GET" +} +``` + ### /Multipart/ #### GET diff --git a/test/tools/WebListener/Views/Home/Index.cshtml b/test/tools/WebListener/Views/Home/Index.cshtml index a9c9a8818f..b8f26512e4 100644 --- a/test/tools/WebListener/Views/Home/Index.cshtml +++ b/test/tools/WebListener/Views/Home/Index.cshtml @@ -11,7 +11,8 @@
  • /Delete/ - returns data from a DELETE request
  • /Encoding/Utf8/ - Returns page containing UTF-8 data.
  • /Get/ - Emulates functionality of https://httpbin.org/get by returning GET headers, Arguments, and Request URL
  • -
  • /Multipart/ - Multipart/form-data submission testing
  • +
  • /Get/ - Emulates functionality of https://httpbin.org/get by returning GET headers, Arguments, and Request URL
  • +
  • /Link/ - Link Header (pagination) testing
  • /Patch/ - returns data from a PATCH request
  • /Post/ - returns data from a POST request
  • /Put/ - returns data from a PUT request