From bb161f62228b62ddea9a0937bc0930f9be0f91b3 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 09:54:12 -0700 Subject: [PATCH 01/41] Tunes up test --- tests/https.Tests/IntegrationTests.cs | 35 +++++++++++++++++++++++++++ tests/https.Tests/Startup.cs | 21 ++++++++++++++++ tests/https.Tests/WebHostFixture.cs | 33 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/https.Tests/IntegrationTests.cs create mode 100644 tests/https.Tests/Startup.cs create mode 100644 tests/https.Tests/WebHostFixture.cs diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/IntegrationTests.cs new file mode 100644 index 0000000..5263182 --- /dev/null +++ b/tests/https.Tests/IntegrationTests.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Https.Tests +{ + public class IntegrationTests : IClassFixture + { + readonly WebHostFixture _fixture; + public IntegrationTests(WebHostFixture fixture) => + _fixture = fixture; + + [Fact] + public async Task MirrorTests() + { + var args = new[] + { + "post", _fixture.Url, "--json", "foo=bar", "lorem=ipsum" + }; + + using (var stdin = new MemoryStream()) + using (var stdout = new MemoryStream()) + using (var stderr = new MemoryStream()) + { + await new Program(() => stderr, () => stdin, () => stdout, false) + .RunAsync(args); + + stdout.Position = 0; + var json = new StreamReader(stdout).ReadToEnd(); + Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); + stderr.Position = 0; + } + } + } +} diff --git a/tests/https.Tests/Startup.cs b/tests/https.Tests/Startup.cs new file mode 100644 index 0000000..552627f --- /dev/null +++ b/tests/https.Tests/Startup.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; + +namespace Https.Tests +{ + public class Startup + { + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.Run(async (context) => + { + if (!string.IsNullOrEmpty(context.Request.ContentType)) + { + context.Response.ContentType = context.Request.ContentType; + } + + await context.Request.Body.CopyToAsync(context.Response.Body); + }); + } + } +} diff --git a/tests/https.Tests/WebHostFixture.cs b/tests/https.Tests/WebHostFixture.cs new file mode 100644 index 0000000..7982689 --- /dev/null +++ b/tests/https.Tests/WebHostFixture.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Https.Tests +{ + public class WebHostFixture : IDisposable + { + readonly IWebHost _webHost; + readonly Task _task; + readonly CancellationTokenSource _cts; + public string Url { get; } + + public WebHostFixture() + { + _webHost = WebHost.CreateDefaultBuilder() + .UseUrls(Url = "http://localhost:5000") + .UseStartup() + .Build(); + _cts = new CancellationTokenSource(); + _task = _webHost.RunAsync(_cts.Token); + } + + public async void Dispose() + { + await _webHost.StopAsync(); + _cts.Cancel(); + _cts.Dispose(); + } + } +} From 25d40cea0872c779de1e6c8db47272c950d20d28 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:03:53 -0700 Subject: [PATCH 02/41] Better testing types --- tests/https.Tests/Https.cs | 26 ++++++++++++++ tests/https.Tests/HttpsResult.cs | 50 +++++++++++++++++++++++++++ tests/https.Tests/IntegrationTests.cs | 14 ++------ 3 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 tests/https.Tests/Https.cs create mode 100644 tests/https.Tests/HttpsResult.cs diff --git a/tests/https.Tests/Https.cs b/tests/https.Tests/Https.cs new file mode 100644 index 0000000..2e7832c --- /dev/null +++ b/tests/https.Tests/Https.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Threading.Tasks; + +namespace Https.Tests +{ + public static class Https + { + public static async Task ExecuteAsync(params string[] args) + { + using var stdin = new MemoryStream(); + + return await ExecuteAsync(stdin, args); + } + + public static async Task ExecuteAsync(Stream stdin, params string[] args) + { + var stdout = new MemoryStream(); + var stderr = new MemoryStream(); + + var exitCode = await new Program(() => stderr, () => stdin, () => stdout, false) + .RunAsync(args); + + return new HttpsResult(exitCode, stdout, stderr); + } + } +} diff --git a/tests/https.Tests/HttpsResult.cs b/tests/https.Tests/HttpsResult.cs new file mode 100644 index 0000000..75a749d --- /dev/null +++ b/tests/https.Tests/HttpsResult.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Https.Tests +{ + public class HttpsResult : IDisposable + { + public int ExitCode { get; } + public MemoryStream StdOut { get; } + public string Status { get; } + public IReadOnlyDictionary Headers { get; } + + public HttpsResult(int exitCode, MemoryStream stdout, MemoryStream stderr) + { + ExitCode = exitCode; + + StdOut = stdout; + StdOut.Position = 0; + + stderr.Position = 0; + var lines = new StreamReader(stderr) + .ReadToEnd() + .Split(Environment.NewLine); + + Status = lines[0]; + + var headers = new Dictionary(); + foreach (var line in lines.Skip(1)) + { + var pos = line.IndexOf(':'); + if (pos > -1) + { + var key = line.Substring(0, pos); + var value = line.Substring(pos + 1); + headers[key] = value; + } + } + Headers = headers; + + stderr.Dispose(); + } + + public void Dispose() + { + StdOut.Dispose(); + } + } +} diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/IntegrationTests.cs index 5263182..26b53b4 100644 --- a/tests/https.Tests/IntegrationTests.cs +++ b/tests/https.Tests/IntegrationTests.cs @@ -18,18 +18,10 @@ public async Task MirrorTests() "post", _fixture.Url, "--json", "foo=bar", "lorem=ipsum" }; - using (var stdin = new MemoryStream()) - using (var stdout = new MemoryStream()) - using (var stderr = new MemoryStream()) - { - await new Program(() => stderr, () => stdin, () => stdout, false) - .RunAsync(args); + var result = await Https.ExecuteAsync(args); - stdout.Position = 0; - var json = new StreamReader(stdout).ReadToEnd(); - Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); - stderr.Position = 0; - } + var json = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); } } } From bf3b3fec33796d8f3084a329d84259506c23d4c8 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:14:06 -0700 Subject: [PATCH 03/41] Implements the ability to stop automatic redirects. --- src/https/Program.cs | 56 ++++++++++++++++++------- tests/https.Tests/HttpsResult.cs | 2 +- tests/https.Tests/IntegrationTests.cs | 16 ++++++- tests/https.Tests/MirrorMiddleware.cs | 28 +++++++++++++ tests/https.Tests/RedirectMiddleware.cs | 24 +++++++++++ tests/https.Tests/Startup.cs | 11 +++-- 6 files changed, 114 insertions(+), 23 deletions(-) create mode 100644 tests/https.Tests/MirrorMiddleware.cs create mode 100644 tests/https.Tests/RedirectMiddleware.cs diff --git a/src/https/Program.cs b/src/https/Program.cs index a876340..a0f9644 100644 --- a/src/https/Program.cs +++ b/src/https/Program.cs @@ -261,20 +261,8 @@ public async Task RunAsync(string[] args) var stdoutWriter = new StreamWriter(stdout) { AutoFlush = true }; { var renderer = new Renderer(stdoutWriter, stderrWriter); - - var http = options.IgnoreCertificate - ? new HttpClient( - new HttpClientHandler - { - ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - } - ) - : new HttpClient(); - if (options.Timeout.HasValue) - { - http.Timeout = options.Timeout.Value; - } + var http = CreateHttpClient(options); var request = new HttpRequestMessage( command.Method ?? HttpMethod.Get, @@ -341,6 +329,35 @@ public async Task RunAsync(string[] args) return 0; } + + static HttpClient CreateHttpClient(Options options) + { + var http = default(HttpClient); + if (options.RequiresHandler) + { + var handler = new HttpClientHandler(); + if (options.IgnoreCertificate) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + if (options.StopAutoRedirects) + { + handler.AllowAutoRedirect = false; + } + http = new HttpClient(handler); + } + else + { + http = new HttpClient(); + } + + if (options.Timeout.HasValue) + { + http.Timeout = options.Timeout.Value; + } + + return http; + } } enum ContentType @@ -358,9 +375,11 @@ class Options public TimeSpan? Timeout { get; } public bool Version { get; } public bool Help { get; } + public bool StopAutoRedirects { get; } + public bool RequiresHandler => IgnoreCertificate || StopAutoRedirects; - public Options(ContentType requestContentType, string xmlRootName, bool ignoreCertificate, TimeSpan? timeout, bool version, bool help) + public Options(ContentType requestContentType, string xmlRootName, bool ignoreCertificate, TimeSpan? timeout, bool version, bool help, bool stopAutoRedirects) { RequestContentType = requestContentType; XmlRootName = xmlRootName; @@ -368,6 +387,7 @@ public Options(ContentType requestContentType, string xmlRootName, bool ignoreCe Timeout = timeout; Version = version; Help = help; + StopAutoRedirects = stopAutoRedirects; } public static IEnumerable GetOptionHelp() @@ -379,6 +399,7 @@ public static IEnumerable GetOptionHelp() yield return "--timeout= Sets the timeout of the request using System.TimeSpan.TryParse (https://docs.microsoft.com/en-us/dotnet/api/system.timespan.parse)"; yield return "--version Displays the application verison."; yield return "--xml= Renders the content arguments as application/xml using the optional xml root name."; + yield return "--stop-auto-redirects Prevents redirects from automatically being processed."; } static int GetArgValueIndex(string arg) @@ -400,6 +421,7 @@ public static Options Parse(IEnumerable args) var timeout = default(TimeSpan?); var help = false; var version = false; + var stopAutoRedirects = false; foreach (var arg in args) { if (arg.StartsWith("--json")) @@ -451,8 +473,12 @@ public static Options Parse(IEnumerable args) { help = true; } + else if (arg.StartsWith("--stop-auto-redirects")) + { + stopAutoRedirects = true; + } } - return new Options(requestContentType, xmlRootName, ignoreCertificate, timeout, version, help); + return new Options(requestContentType, xmlRootName, ignoreCertificate, timeout, version, help, stopAutoRedirects); } } diff --git a/tests/https.Tests/HttpsResult.cs b/tests/https.Tests/HttpsResult.cs index 75a749d..0983be0 100644 --- a/tests/https.Tests/HttpsResult.cs +++ b/tests/https.Tests/HttpsResult.cs @@ -33,7 +33,7 @@ public HttpsResult(int exitCode, MemoryStream stdout, MemoryStream stderr) if (pos > -1) { var key = line.Substring(0, pos); - var value = line.Substring(pos + 1); + var value = line.Substring(pos + 2); headers[key] = value; } } diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/IntegrationTests.cs index 26b53b4..2288567 100644 --- a/tests/https.Tests/IntegrationTests.cs +++ b/tests/https.Tests/IntegrationTests.cs @@ -15,7 +15,7 @@ public async Task MirrorTests() { var args = new[] { - "post", _fixture.Url, "--json", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.Url}/Mirror", "--json", "foo=bar", "lorem=ipsum" }; var result = await Https.ExecuteAsync(args); @@ -23,5 +23,19 @@ public async Task MirrorTests() var json = new StreamReader(result.StdOut).ReadToEnd(); Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); } + + [Fact] + public async Task RedirectTest_ShouldShow3XXResponse_GivenStopAutoRedirects() + { + var args = new[] + { + "get", "http://localhost:5000/Redirect", "--stop-auto-redirects" + }; + + var result = await Https.ExecuteAsync(args); + + Assert.Equal("HTTP/1.1 301 Moved Permanently", result.Status); + Assert.Equal("http://localhost:5000/Mirror", result.Headers["Location"]); + } } } diff --git a/tests/https.Tests/MirrorMiddleware.cs b/tests/https.Tests/MirrorMiddleware.cs new file mode 100644 index 0000000..2c789a8 --- /dev/null +++ b/tests/https.Tests/MirrorMiddleware.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; + +namespace Https.Tests +{ + class MirrorMiddleware + { + readonly RequestDelegate _next; + public MirrorMiddleware(RequestDelegate next) => + _next = next; + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/Mirror")) + { + if (!string.IsNullOrEmpty(context.Request.ContentType)) + { + context.Response.ContentType = context.Request.ContentType; + } + + await context.Request.Body.CopyToAsync(context.Response.Body); + } + else + { + await _next(context); + } + } + } +} diff --git a/tests/https.Tests/RedirectMiddleware.cs b/tests/https.Tests/RedirectMiddleware.cs new file mode 100644 index 0000000..25dc0d3 --- /dev/null +++ b/tests/https.Tests/RedirectMiddleware.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; + +namespace Https.Tests +{ + class RedirectMiddleware + { + readonly RequestDelegate _next; + public RedirectMiddleware(RequestDelegate next) => + _next = next; + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/Redirect")) + { + context.Response.StatusCode = StatusCodes.Status301MovedPermanently; + context.Response.Headers.Add("Location", "http://localhost:5000/Mirror"); + } + else + { + await _next(context); + } + } + } +} diff --git a/tests/https.Tests/Startup.cs b/tests/https.Tests/Startup.cs index 552627f..b7fcdbd 100644 --- a/tests/https.Tests/Startup.cs +++ b/tests/https.Tests/Startup.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; namespace Https.Tests { @@ -7,14 +8,12 @@ public class Startup { public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.UseMiddleware(); + app.UseMiddleware(); + app.Run(async (context) => { - if (!string.IsNullOrEmpty(context.Request.ContentType)) - { - context.Response.ContentType = context.Request.ContentType; - } - - await context.Request.Body.CopyToAsync(context.Response.Body); + await context.Response.WriteAsync("Hello!"); }); } } From 01af984e20e1338dca04e4e702f7974ca232d94d Mon Sep 17 00:00:00 2001 From: Jonathan Berube Date: Sun, 28 Jul 2019 10:24:27 -0700 Subject: [PATCH 04/41] Set up CI with Azure Pipelines [skip ci] --- azure-pipelines.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..fdd6487 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,39 @@ +trigger: +- master + +pool: + vmImage: 'ubuntu-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- task: DotNetCoreInstaller@1 + displayName: Install .NET Core 3.0 Preview + inputs: + includePreviewVersions: true + packageType: sdk + version: 3.0.100-preview7-012821 + +- task: DotNetCoreCLI@2 + displayName: Pack https + inputs: + command: pack + publishWebProjects: false + projects: '**/*.csproj' + arguments: --configuration $(buildConfiguration) --output $(build.ArtifactStagingDirectory) + +- task: DotNetCoreCLI@2 + displayName: Test https + inputs: + command: test + projects: '**/*Test*.csproj' + +- task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: '$(build.ArtifactStagingDirectory)' + ArtifactName: 'https' + Parallel: true + From b596be97885c9f3709f7918b546f2176b1d15928 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:26:32 -0700 Subject: [PATCH 05/41] Adds preview language features --- src/https/https.csproj | 2 +- tests/https.Tests/https.Tests.csproj | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/https/https.csproj b/src/https/https.csproj index cb5ee09..1221fa2 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -3,7 +3,7 @@ Exe netcoreapp2.1 - latest + preview diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index 0b4624d..7b20504 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -1,11 +1,10 @@ - + netcoreapp2.1 - false - Https.Tests + preview From 4616f3fa4e7ad99a6ccc8e0708617eafbb8080fc Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:28:59 -0700 Subject: [PATCH 06/41] Fixes merge conflicts --- tests/https.Tests/Startup.cs | 2 +- tests/https.Tests/UnitTest1.cs | 63 ---------------------------------- 2 files changed, 1 insertion(+), 64 deletions(-) delete mode 100644 tests/https.Tests/UnitTest1.cs diff --git a/tests/https.Tests/Startup.cs b/tests/https.Tests/Startup.cs index b7fcdbd..627fc13 100644 --- a/tests/https.Tests/Startup.cs +++ b/tests/https.Tests/Startup.cs @@ -6,7 +6,7 @@ namespace Https.Tests { public class Startup { - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMiddleware(); app.UseMiddleware(); diff --git a/tests/https.Tests/UnitTest1.cs b/tests/https.Tests/UnitTest1.cs deleted file mode 100644 index 742ae2b..0000000 --- a/tests/https.Tests/UnitTest1.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Threading.Tasks; -using Xunit; - -namespace Https.Tests -{ - public class UnitTest1 - { - [Fact] - public async Task Test1() - { - var webHost = WebHost.CreateDefaultBuilder() - .UseUrls("http://localhost:5000") - .UseStartup() - .Build(); - - var task = webHost.RunAsync(); - - var args = new[] - { - "post", "http://localhost:5000", "--json", "foo=bar", "lorem=ipsum" - }; - - using (var stdin = new MemoryStream()) - using (var stdout = new MemoryStream()) - using (var stderr = new MemoryStream()) - { - await new Program(() => stderr, () => stdin, () => stdout, false) - .RunAsync(args); - - stdout.Position = 0; - var json = new StreamReader(stdout).ReadToEnd(); - Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); - stderr.Position = 0; - } - - - - await webHost.StopAsync(); - } - } - - public class Startup - { - public void Configure(IApplicationBuilder app, IHostingEnvironment env) - { - app.Run(async (context) => - { - if (!string.IsNullOrEmpty(context.Request.ContentType)) - { - context.Response.ContentType = context.Request.ContentType; - } - - await context.Request.Body.CopyToAsync(context.Response.Body); - }); - } - } -} From 878b9291a9b239c44b32e9024e2f866b813f0e8e Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:32:17 -0700 Subject: [PATCH 07/41] Updates test project to facilitate Azure DevOps tests --- tests/https.Tests/https.Tests.csproj | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index 7b20504..1830359 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -1,18 +1,21 @@  - netcoreapp2.1 + netcoreapp3.0 false Https.Tests preview - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 52aed7ac49b27ae012ccc1e711ea547c7defabd4 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:37:39 -0700 Subject: [PATCH 08/41] Updates build yaml --- azure-pipelines.yml | 7 ++++--- src/https/https.csproj | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fdd6487..5e16dc4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,14 +22,15 @@ steps: inputs: command: pack publishWebProjects: false - projects: '**/*.csproj' - arguments: --configuration $(buildConfiguration) --output $(build.ArtifactStagingDirectory) + packagesToPack: '**/https.csproj' + configuration: $(buildConfiguration) + packDirectory: $(build.ArtifactStagingDirectory) - task: DotNetCoreCLI@2 displayName: Test https inputs: command: test - projects: '**/*Test*.csproj' + projects: '**/https.Tests.csproj' - task: PublishBuildArtifacts@1 inputs: diff --git a/src/https/https.csproj b/src/https/https.csproj index 1221fa2..50d97dd 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -7,7 +7,7 @@ - 0.1.1 + 0.1.2 local From d7a93ae7444fd273291bdae0e32dda3e38e79bcf Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:41:25 -0700 Subject: [PATCH 09/41] Adds build number to package --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5e16dc4..bfb79f9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,6 +25,7 @@ steps: packagesToPack: '**/https.csproj' configuration: $(buildConfiguration) packDirectory: $(build.ArtifactStagingDirectory) + versioningScheme: byBuildNumber - task: DotNetCoreCLI@2 displayName: Test https From 65ec01c9d9a446358c13f97df6147f0ffd78db22 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:43:14 -0700 Subject: [PATCH 10/41] Trying prerelease number --- azure-pipelines.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index bfb79f9..28cc7ca 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,7 +25,10 @@ steps: packagesToPack: '**/https.csproj' configuration: $(buildConfiguration) packDirectory: $(build.ArtifactStagingDirectory) - versioningScheme: byBuildNumber + versioningScheme: byPrereleaseNumber + majorVersion: 0 + minorVersion: 1 + patchVersion: 2 - task: DotNetCoreCLI@2 displayName: Test https From 0012247bd3509bd4735609c950bc5f9f389cf94d Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 10:44:44 -0700 Subject: [PATCH 11/41] Updates target framework to be latest stable --- src/https/https.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/https/https.csproj b/src/https/https.csproj index 50d97dd..d6295a5 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp2.2 preview From d6be90ac6c5da048322dc903d8d418187d1fb343 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 15:29:44 -0700 Subject: [PATCH 12/41] Updates version in readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69f9704..a040201 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Released under the MIT License. See the [LICENSE][] File for further details. ## Installation Install `https` as a global .NET tool using ```bash -dotnet tool install --global https --version 0.1.1-beta +dotnet tool install --global https --version 0.1.2-* ``` ## Usage From d44686da0a9d3f7ea804631c151082f15b693fce Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 28 Jul 2019 15:30:44 -0700 Subject: [PATCH 13/41] Adds build badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a040201..4d3af82 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # https [![NuGet](https://img.shields.io/nuget/v/https.svg)](https://www.nuget.org/packages/https/) +[![Build Status](https://dev.azure.com/joncloud/joncloud-github/_apis/build/status/joncloud.https?branchName=master)](https://dev.azure.com/joncloud/joncloud-github/_build/latest?definitionId=13&branchName=master) ## Description `https` is a simple CLI for sending HTTP requests. From 090243ef8a6f520d64642d3519c4c65b788f069c Mon Sep 17 00:00:00 2001 From: joncloud Date: Thu, 11 Jun 2020 21:06:25 -0700 Subject: [PATCH 14/41] Changes branch name --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 28cc7ca..8599c00 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,5 +1,5 @@ trigger: -- master +- publish pool: vmImage: 'ubuntu-latest' From 0731836d237ad1ad1ddbd4bc7a21c081297c56bc Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 18:46:10 -0700 Subject: [PATCH 15/41] Upgrades to .NET Core 3.1. --- azure-pipelines.yml | 11 ++--------- src/https/https.csproj | 4 ++-- tests/https.Tests/Startup.cs | 2 +- tests/https.Tests/https.Tests.csproj | 10 +++++----- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8599c00..1d4448f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,13 +10,6 @@ variables: buildConfiguration: 'Release' steps: -- task: DotNetCoreInstaller@1 - displayName: Install .NET Core 3.0 Preview - inputs: - includePreviewVersions: true - packageType: sdk - version: 3.0.100-preview7-012821 - - task: DotNetCoreCLI@2 displayName: Pack https inputs: @@ -27,8 +20,8 @@ steps: packDirectory: $(build.ArtifactStagingDirectory) versioningScheme: byPrereleaseNumber majorVersion: 0 - minorVersion: 1 - patchVersion: 2 + minorVersion: 2 + patchVersion: 0 - task: DotNetCoreCLI@2 displayName: Test https diff --git a/src/https/https.csproj b/src/https/https.csproj index d6295a5..d61a00f 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -2,12 +2,12 @@ Exe - netcoreapp2.2 + netcoreapp3.1 preview - 0.1.2 + 0.2.0 local diff --git a/tests/https.Tests/Startup.cs b/tests/https.Tests/Startup.cs index 627fc13..6c3cc9d 100644 --- a/tests/https.Tests/Startup.cs +++ b/tests/https.Tests/Startup.cs @@ -6,7 +6,7 @@ namespace Https.Tests { public class Startup { - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app) { app.UseMiddleware(); app.UseMiddleware(); diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index 1830359..6e128bb 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -1,18 +1,18 @@  - netcoreapp3.0 + netcoreapp3.1 false Https.Tests preview - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive From cb6d1a84be862d761b204f772f51d4678c2fee66 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:03:11 -0700 Subject: [PATCH 16/41] Adds test to make sure readme and csproj align. --- TestReadmeVersion.ps1 | 30 ++++++++++++++++++++++++++++++ azure-pipelines.yml | 5 +++++ 2 files changed, 35 insertions(+) create mode 100644 TestReadmeVersion.ps1 diff --git a/TestReadmeVersion.ps1 b/TestReadmeVersion.ps1 new file mode 100644 index 0000000..3c22838 --- /dev/null +++ b/TestReadmeVersion.ps1 @@ -0,0 +1,30 @@ +$ReadmePath = Join-Path $PSScriptRoot 'README.md' +If (-not (Test-Path $ReadmePath)) { + Write-Error 'Unable to find README.md' + Exit 1 +} + +$ReadmeContents = Get-Content $ReadmePath -Raw +[regex]$Regex = 'dotnet tool install --global https --version (.+)-\*' +$Match = $Regex.Match($ReadmeContents) +If (-not $Match.Success) { + Write-Error 'Unable to find version information in README.md' + Exit 1 +} + +$HttpsCsprojPath = Join-Path $PSScriptRoot 'src/https/https.csproj' +If (-not (Test-Path $HttpsCsprojPath)) { + Write-Error 'Unable to find https.csproj' + Exit 1 +} + +[xml]$HttpsCsproj = Get-Content $HttpsCsprojPath + +$Expected = $HttpsCsproj.Project.PropertyGroup.VersionPrefix | + Where-Object { $null -ne $_ } | + ForEach-Object { $_.ToString().Trim() } +$Actual = $Match.Groups[1].ToString().Trim() +If ($Expected -ne $Actual) { + Write-Error "Expected to have ${Expected} version in README.md, but found ${Actual}" + Exit 1 +} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d4448f..3549e90 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,6 +10,11 @@ variables: buildConfiguration: 'Release' steps: +- task: PowerShell@2 + inputs: + filePath: 'TestReadmeVersion.ps1' + failOnStderr: true + - task: DotNetCoreCLI@2 displayName: Pack https inputs: From 69b38b7c2eda9b2e318b4e0ce714f45c58a24bd8 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:04:46 -0700 Subject: [PATCH 17/41] Updates display names --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3549e90..ec69b55 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,6 +11,7 @@ variables: steps: - task: PowerShell@2 + displayName: Test Readme inputs: filePath: 'TestReadmeVersion.ps1' failOnStderr: true @@ -35,6 +36,7 @@ steps: projects: '**/https.Tests.csproj' - task: PublishBuildArtifacts@1 + displayName: Upload Artifacts inputs: PathtoPublish: '$(build.ArtifactStagingDirectory)' ArtifactName: 'https' From e2506fd646623f66b6cf2346abc314114944f592 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:04:52 -0700 Subject: [PATCH 18/41] Updates readme version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d3af82..f289033 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Released under the MIT License. See the [LICENSE][] File for further details. ## Installation Install `https` as a global .NET tool using ```bash -dotnet tool install --global https --version 0.1.2-* +dotnet tool install --global https --version 0.2.0-* ``` ## Usage From 46a0e0491c6b6644c894a2eef0bce7b2c838814c Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:11:52 -0700 Subject: [PATCH 19/41] Updates deprecated property --- src/https/https.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/https/https.csproj b/src/https/https.csproj index d61a00f..3ffca04 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -25,7 +25,7 @@ https://github.com/joncloud/https/releases https://github.com/joncloud/https - https://raw.githubusercontent.com/joncloud/https/master/LICENSE.md + MIT git https://github.com/joncloud/https From 7bb20d220e819585200e296338e71374348c3d43 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 2 Aug 2020 20:08:09 -0700 Subject: [PATCH 20/41] Migrates readme tests from adhoc ps1 to xunit * Creates test for validating version * Creates test for validating usage --- README.md | 1 + TestReadmeVersion.ps1 | 30 ------------- azure-pipelines.yml | 6 --- tests/https.Tests/HttpsCsprojFixture.cs | 16 +++++++ tests/https.Tests/Readme.cs | 57 +++++++++++++++++++++++++ tests/https.Tests/ReadmeFixture.cs | 13 ++++++ tests/https.Tests/ReadmeTests.cs | 49 +++++++++++++++++++++ 7 files changed, 136 insertions(+), 36 deletions(-) delete mode 100644 TestReadmeVersion.ps1 create mode 100644 tests/https.Tests/HttpsCsprojFixture.cs create mode 100644 tests/https.Tests/Readme.cs create mode 100644 tests/https.Tests/ReadmeFixture.cs create mode 100644 tests/https.Tests/ReadmeTests.cs diff --git a/README.md b/README.md index f289033..4c21c33 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Options: --timeout= Sets the timeout of the request using System.TimeSpan.TryParse (https://docs.microsoft.com/en-us/dotnet/api/system.timespan.parse) --version Displays the application verison. --xml= Renders the content arguments as application/xml using the optional xml root name. + --stop-auto-redirects Prevents redirects from automatically being processed. Content: Repeat as many content arguments to create content sent with the HTTP request. Alternatively pipe raw content send as the HTTP request content. diff --git a/TestReadmeVersion.ps1 b/TestReadmeVersion.ps1 deleted file mode 100644 index 3c22838..0000000 --- a/TestReadmeVersion.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -$ReadmePath = Join-Path $PSScriptRoot 'README.md' -If (-not (Test-Path $ReadmePath)) { - Write-Error 'Unable to find README.md' - Exit 1 -} - -$ReadmeContents = Get-Content $ReadmePath -Raw -[regex]$Regex = 'dotnet tool install --global https --version (.+)-\*' -$Match = $Regex.Match($ReadmeContents) -If (-not $Match.Success) { - Write-Error 'Unable to find version information in README.md' - Exit 1 -} - -$HttpsCsprojPath = Join-Path $PSScriptRoot 'src/https/https.csproj' -If (-not (Test-Path $HttpsCsprojPath)) { - Write-Error 'Unable to find https.csproj' - Exit 1 -} - -[xml]$HttpsCsproj = Get-Content $HttpsCsprojPath - -$Expected = $HttpsCsproj.Project.PropertyGroup.VersionPrefix | - Where-Object { $null -ne $_ } | - ForEach-Object { $_.ToString().Trim() } -$Actual = $Match.Groups[1].ToString().Trim() -If ($Expected -ne $Actual) { - Write-Error "Expected to have ${Expected} version in README.md, but found ${Actual}" - Exit 1 -} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ec69b55..43ec917 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,12 +10,6 @@ variables: buildConfiguration: 'Release' steps: -- task: PowerShell@2 - displayName: Test Readme - inputs: - filePath: 'TestReadmeVersion.ps1' - failOnStderr: true - - task: DotNetCoreCLI@2 displayName: Pack https inputs: diff --git a/tests/https.Tests/HttpsCsprojFixture.cs b/tests/https.Tests/HttpsCsprojFixture.cs new file mode 100644 index 0000000..eceb514 --- /dev/null +++ b/tests/https.Tests/HttpsCsprojFixture.cs @@ -0,0 +1,16 @@ +using System.Xml.Linq; + +namespace Https.Tests +{ + public class HttpsCsprojFixture + { + public XDocument Document { get; } + + public HttpsCsprojFixture() + { + var path = "../../../../../src/https/https.csproj"; + + Document = XDocument.Load(path); + } + } +} diff --git a/tests/https.Tests/Readme.cs b/tests/https.Tests/Readme.cs new file mode 100644 index 0000000..fb08afa --- /dev/null +++ b/tests/https.Tests/Readme.cs @@ -0,0 +1,57 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace Https.Tests +{ + public class Readme + { + public string InstallationVersion { get; } + public string UsageDocumentation { get; } + + public Readme(string path) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullException(nameof(path)); + + var sb = new StringBuilder(); + InstallationVersion = ""; + UsageDocumentation = ""; + + var lines = File.ReadLines(path); + var record = false; + foreach (var line in lines) + { + + if (line.StartsWith("```bash")) + { + record = true; + } + else if (line.StartsWith("```")) + { + var text = sb.ToString(); + sb.Clear(); + + if (text.StartsWith("dotnet tool")) + { + var match = Regex.Match(text, "dotnet tool install --global https --version (.+)-\\*"); + if (match.Success) + { + InstallationVersion = match.Groups[1].Value; + } + } + else if (text.StartsWith("Usage")) + { + UsageDocumentation = text; + } + + record = false; + } + else if (record) + { + sb.AppendLine(line); + } + } + } + } +} diff --git a/tests/https.Tests/ReadmeFixture.cs b/tests/https.Tests/ReadmeFixture.cs new file mode 100644 index 0000000..4f86dde --- /dev/null +++ b/tests/https.Tests/ReadmeFixture.cs @@ -0,0 +1,13 @@ +namespace Https.Tests +{ + public class ReadmeFixture + { + public Readme Readme { get; } + public ReadmeFixture() + { + Readme = new Readme( + "../../../../../README.md" + ); + } + } +} diff --git a/tests/https.Tests/ReadmeTests.cs b/tests/https.Tests/ReadmeTests.cs new file mode 100644 index 0000000..d31b6de --- /dev/null +++ b/tests/https.Tests/ReadmeTests.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Https.Tests +{ + public class ReadmeTests : IClassFixture, IClassFixture + { + readonly HttpsCsprojFixture _httpsCsprojFixture; + readonly ReadmeFixture _readmeFixture; + public ReadmeTests(HttpsCsprojFixture httpsCsprojFixture, ReadmeFixture readmeFixture) + { + _httpsCsprojFixture = httpsCsprojFixture; + _readmeFixture = readmeFixture; + } + + [Fact] + public void Installation_ShouldListSameVersionAsCsproj() + { + var versionPrefixElement = _httpsCsprojFixture.Document + .Root + .Elements("PropertyGroup") + .Elements("VersionPrefix") + .FirstOrDefault(); + + Assert.NotNull(versionPrefixElement); + + var expected = versionPrefixElement.Value; + var actual = _readmeFixture.Readme.InstallationVersion; + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task Usage_ShouldListHelpDocument() + { + var httpsResult = await Https.ExecuteAsync("--help"); + + using var reader = new StreamReader(httpsResult.StdOut); + + var expected = reader.ReadToEnd().TrimEnd(); + var actual = _readmeFixture.Readme.UsageDocumentation.TrimEnd(); + + Assert.Equal(expected, actual); + } + } +} From 75860a018620685b52bc159f88a520b7bd62818d Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 2 Aug 2020 20:21:50 -0700 Subject: [PATCH 21/41] Documents header usage with help --- README.md | 4 ++++ src/https/Program.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index 4c21c33..7e55b1d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ Options: Content: Repeat as many content arguments to create content sent with the HTTP request. Alternatively pipe raw content send as the HTTP request content. = + +Headers: +Repeat as many header arguments to assign headers for the HTTP request. + : ``` For example `https put httpbin.org/put hello=world` will output: diff --git a/src/https/Program.cs b/src/https/Program.cs index a0f9644..9c405df 100644 --- a/src/https/Program.cs +++ b/src/https/Program.cs @@ -159,6 +159,10 @@ void Help() writer.WriteLine("Repeat as many content arguments to create content sent with the HTTP request. Alternatively pipe raw content send as the HTTP request content."); writer.WriteLine(" ="); writer.WriteLine(""); + writer.WriteLine("Headers:"); + writer.WriteLine("Repeat as many header arguments to assign headers for the HTTP request."); + writer.WriteLine(" :"); + writer.WriteLine(""); writer.Flush(); } From aa83c848ea5f25d6e8a1e9f0c91e5a181a1fbec2 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 2 Aug 2020 20:27:07 -0700 Subject: [PATCH 22/41] Fixes azure devops badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e55b1d..ce3eeee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # https [![NuGet](https://img.shields.io/nuget/v/https.svg)](https://www.nuget.org/packages/https/) -[![Build Status](https://dev.azure.com/joncloud/joncloud-github/_apis/build/status/joncloud.https?branchName=master)](https://dev.azure.com/joncloud/joncloud-github/_build/latest?definitionId=13&branchName=master) +[![Build Status](https://dev.azure.com/joncloud/joncloud-github/_apis/build/status/joncloud.https?branchName=master)](https://dev.azure.com/joncloud/joncloud-github/_build/latest?definitionId=13&branchName=publish) ## Description `https` is a simple CLI for sending HTTP requests. From a700a8e70a7932729b241633eac701a6e99bc852 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sun, 2 Aug 2020 20:27:53 -0700 Subject: [PATCH 23/41] Really fixes it --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce3eeee..239220c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # https [![NuGet](https://img.shields.io/nuget/v/https.svg)](https://www.nuget.org/packages/https/) -[![Build Status](https://dev.azure.com/joncloud/joncloud-github/_apis/build/status/joncloud.https?branchName=master)](https://dev.azure.com/joncloud/joncloud-github/_build/latest?definitionId=13&branchName=publish) +[![Build Status](https://dev.azure.com/joncloud/joncloud-github/_apis/build/status/joncloud.https?branchName=publish)](https://dev.azure.com/joncloud/joncloud-github/_build/latest?definitionId=13&branchName=publish) ## Description `https` is a simple CLI for sending HTTP requests. From 313bad46513ea350f0d807691afe8ddb9538e835 Mon Sep 17 00:00:00 2001 From: joncloud Date: Mon, 3 Aug 2020 19:58:33 -0700 Subject: [PATCH 24/41] Changes publish branch to be release --- .gitignore | 2 ++ azure-pipelines.yml | 13 +++++++++++++ src/https/https.csproj | 1 - 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e927ef2..6900cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,5 @@ ASALocalRun/ .localhistory/ launchSettings.json + +.ionide diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 43ec917..d8e9aaa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,10 +8,23 @@ variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' + shouldPublish: $[eq(variables['Build.SourceBranch'], 'refs/heads/publish')] steps: - task: DotNetCoreCLI@2 displayName: Pack https + condition: eq(variables.shouldPublish, true) + inputs: + command: pack + publishWebProjects: false + packagesToPack: '**/https.csproj' + configuration: $(buildConfiguration) + packDirectory: $(build.ArtifactStagingDirectory) + versioningScheme: off + +- task: DotNetCoreCLI@2 + displayName: Pack https + condition: eq(variables.shouldPublish, false) inputs: command: pack publishWebProjects: false diff --git a/src/https/https.csproj b/src/https/https.csproj index 3ffca04..6dc70ac 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -8,7 +8,6 @@ 0.2.0 - local From 8252ab1780f5378e7b89df0ec799664ebaac5bd4 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 4 Aug 2020 19:20:41 -0700 Subject: [PATCH 25/41] Adds Coverlet Code Coverage --- azure-pipelines.yml | 6 ++++++ tests/https.Tests/https.Tests.csproj | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d8e9aaa..21a6f18 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -41,6 +41,12 @@ steps: inputs: command: test projects: '**/https.Tests.csproj' + arguments: '--configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura -- xunit.parallelizeAssembly=true xunit.parallelizeTestCollections=true' + +- script: | + dotnet tool install -g dotnet-reportgenerator-globaltool + reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura + displayName: Code Coverage Report - task: PublishBuildArtifacts@1 displayName: Upload Artifacts diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index 6e128bb..261f038 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -9,6 +9,14 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 0c786848866635f2b7f15ff0a377cfe569038a46 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 4 Aug 2020 19:25:34 -0700 Subject: [PATCH 26/41] Adds dotnet to prevent pathing issues --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 21a6f18..84b1e5c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -45,7 +45,7 @@ steps: - script: | dotnet tool install -g dotnet-reportgenerator-globaltool - reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura + dotnet reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura displayName: Code Coverage Report - task: PublishBuildArtifacts@1 From 2ed96165506a49d87f5ab72cba4b510af527cdd6 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 4 Aug 2020 19:29:48 -0700 Subject: [PATCH 27/41] Installs to specific path --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 84b1e5c..f6f4957 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -44,8 +44,8 @@ steps: arguments: '--configuration $(buildConfiguration) /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura -- xunit.parallelizeAssembly=true xunit.parallelizeTestCollections=true' - script: | - dotnet tool install -g dotnet-reportgenerator-globaltool - dotnet reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura + dotnet tool install dotnet-reportgenerator-globaltool --tool-path tools + ./tools/reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura displayName: Code Coverage Report - task: PublishBuildArtifacts@1 From feb9892ce65f9c5575c77dbe1a79b6002a612bc9 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 4 Aug 2020 19:33:14 -0700 Subject: [PATCH 28/41] Duh --- azure-pipelines.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f6f4957..3877c30 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -46,7 +46,13 @@ steps: - script: | dotnet tool install dotnet-reportgenerator-globaltool --tool-path tools ./tools/reportgenerator -reports:$(Build.SourcesDirectory)/tests/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/CodeCoverage -reporttypes:Cobertura - displayName: Code Coverage Report + displayName: Generate Code Coverage Report + +- task: PublishCodeCoverageResults@1 + displayName: Publish Code Coverage Report + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' - task: PublishBuildArtifacts@1 displayName: Upload Artifacts From a00769882d745684541758d559361dabb3a47b20 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 4 Aug 2020 20:29:50 -0700 Subject: [PATCH 29/41] Adds tests for forms and xml --- tests/https.Tests/IntegrationTests.cs | 47 +++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/IntegrationTests.cs index 2288567..1614edc 100644 --- a/tests/https.Tests/IntegrationTests.cs +++ b/tests/https.Tests/IntegrationTests.cs @@ -1,5 +1,8 @@ using System.IO; +using System.Text; using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; using Xunit; namespace Https.Tests @@ -11,7 +14,21 @@ public IntegrationTests(WebHostFixture fixture) => _fixture = fixture; [Fact] - public async Task MirrorTests() + public async Task MirrorTest_ShouldReflectFormUrlEncoded() + { + var args = new[] + { + "post", $"{_fixture.Url}/Mirror", "--form", "foo=bar", "lorem=ipsum" + }; + + var result = await Https.ExecuteAsync(args); + + var actual = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("foo=bar&lorem=ipsum", actual); + } + + [Fact] + public async Task MirrorTest_ShouldReflectJson() { var args = new[] { @@ -20,8 +37,32 @@ public async Task MirrorTests() var result = await Https.ExecuteAsync(args); - var json = new StreamReader(result.StdOut).ReadToEnd(); - Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", json); + var actual = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", actual); + } + + [Fact] + public async Task MirrorTest_ShouldReflectXml() + { + var args = new[] + { + "post", $"{_fixture.Url}/Mirror", "--xml=root", "foo=bar", "lorem=ipsum" + }; + + var result = await Https.ExecuteAsync(args); + + var expected = new XDocument( + new XElement( + "root", + new XElement("foo", "bar"), + new XElement("lorem", "ipsum") + ) + ).ToString(); + var actual = XDocument.Load( + new StreamReader(result.StdOut) + ).ToString(); + + Assert.Equal(expected, actual); } [Fact] From 75e9ace543b183a9b77aee0970fdfaec7896eb35 Mon Sep 17 00:00:00 2001 From: joncloud Date: Wed, 5 Aug 2020 18:23:27 -0700 Subject: [PATCH 30/41] Moves types to individual code files --- src/https/Command.cs | 128 +++++++ src/https/Content.cs | 63 ++++ src/https/ContentLocation.cs | 8 + src/https/ContentType.cs | 9 + src/https/Options.cs | 120 ++++++ src/https/Program.cs | 515 -------------------------- src/https/Renderer.cs | 105 ++++++ src/https/RequestContentFormatter.cs | 82 ++++ src/https/ResponseContentFormatter.cs | 39 ++ 9 files changed, 554 insertions(+), 515 deletions(-) create mode 100644 src/https/Command.cs create mode 100644 src/https/Content.cs create mode 100644 src/https/ContentLocation.cs create mode 100644 src/https/ContentType.cs create mode 100644 src/https/Options.cs create mode 100644 src/https/Renderer.cs create mode 100644 src/https/RequestContentFormatter.cs create mode 100644 src/https/ResponseContentFormatter.cs diff --git a/src/https/Command.cs b/src/https/Command.cs new file mode 100644 index 0000000..f8343bb --- /dev/null +++ b/src/https/Command.cs @@ -0,0 +1,128 @@ +using System; +using System.Net.Http; + +namespace Https +{ + struct Command + { + public HttpMethod Method { get; } + public Uri Uri { get; } + + Command(Uri uri) + { + Method = default; + Uri = uri ?? throw new ArgumentNullException(nameof(uri)); + } + Command(HttpMethod method, Uri uri) + { + Method = method ?? throw new ArgumentNullException(nameof(method)); + Uri = uri ?? throw new ArgumentNullException(nameof(uri)); + } + + static bool StartsWithHttp(string s) => + s.Length > 6 && s[0] == 'h' && s[1] == 't' && s[2] == 't' && s[3] == 'p' && (s[4] == ':' || (s[4] == 's' && s[5] == ':')); + + static bool TryParseUri(string s, out Uri uri) + { + if (!StartsWithHttp(s)) + { + s = "https://" + s; + } + + return Uri.TryCreate(s, UriKind.Absolute, out uri); + } + + static bool TryParseMethod(string s, out HttpMethod method) + { + if (s.Equals(nameof(HttpMethod.Delete), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Delete; + return true; + } + else if (s.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Get; + return true; + } + else if (s.Equals(nameof(HttpMethod.Head), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Head; + return true; + } + else if (s.Equals(nameof(HttpMethod.Options), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Options; + return true; + } + else if (s.Equals(nameof(HttpMethod.Patch), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Patch; + return true; + } + else if (s.Equals(nameof(HttpMethod.Post), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Post; + return true; + } + else if (s.Equals(nameof(HttpMethod.Put), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Put; + return true; + } + else if (s.Equals(nameof(HttpMethod.Trace), StringComparison.OrdinalIgnoreCase)) + { + method = HttpMethod.Trace; + return true; + } + + method = default; + return false; + } + + public static bool TryParse(string methodText, string uriText, out Command command) + { + if (TryParseMethod(methodText, out var method) && TryParseUri(uriText, out var uri)) + { + command = new Command(method, uri); + return true; + } + else + { + command = default; + return false; + } + } + + public static bool TryParse(string s, out Command command) + { + s = s.Trim(); + if (s.StartsWith('-') || s == "help") + { + command = default; + return false; + } + + var index = s.IndexOf(' '); + if (index == -1) + { + if (TryParseUri(s, out var uri)) + { + command = new Command(uri); + return true; + } + else + { + command = default; + return false; + } + } + else + { + var methodText = s.Substring(0, index); + var uriText = s.Substring(index + 1); + + return TryParse(methodText, uriText, out command); + } + } + } +} diff --git a/src/https/Content.cs b/src/https/Content.cs new file mode 100644 index 0000000..4e1f866 --- /dev/null +++ b/src/https/Content.cs @@ -0,0 +1,63 @@ +namespace Https +{ + class Content + { + public ContentLocation ContentLocation { get; } + public string Property { get; } + public string Value { get; } + Content(ContentLocation contentLocation, string property, string value) + { + ContentLocation = contentLocation; + Property = property; + Value = value; + } + + public static bool TryParse(string s, out Content content) + { + var equalsIndex = s.IndexOf('='); + var colonIndex = s.IndexOf(':'); + if (equalsIndex == -1 && colonIndex == -1) + { + content = default; + return false; + } + + var contentType = default(ContentLocation); + var index = default(int); + if (equalsIndex > -1 && colonIndex > -1) + { + if (equalsIndex < colonIndex) + { + contentType = ContentLocation.Body; + index = equalsIndex; + } + else + { + contentType = ContentLocation.Header; + index = colonIndex; + } + } + else if (equalsIndex > -1) + { + contentType = ContentLocation.Body; + index = equalsIndex; + } + else + { + contentType = ContentLocation.Header; + index = colonIndex; + } + + var property = s.Substring(0, index); + if (property.Length == 0) + { + content = default; + return false; + } + + var value = s.Substring(index + 1); + content = new Content(contentType, property, value); + return true; + } + } +} diff --git a/src/https/ContentLocation.cs b/src/https/ContentLocation.cs new file mode 100644 index 0000000..560d5a3 --- /dev/null +++ b/src/https/ContentLocation.cs @@ -0,0 +1,8 @@ +namespace Https +{ + enum ContentLocation + { + Body = 1, + Header = 2 + } +} diff --git a/src/https/ContentType.cs b/src/https/ContentType.cs new file mode 100644 index 0000000..6f1aa4e --- /dev/null +++ b/src/https/ContentType.cs @@ -0,0 +1,9 @@ +namespace Https +{ + enum ContentType + { + Json = 1, + FormUrlEncoded = 2, + Xml = 3 + } +} diff --git a/src/https/Options.cs b/src/https/Options.cs new file mode 100644 index 0000000..4531776 --- /dev/null +++ b/src/https/Options.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; + +namespace Https +{ + class Options + { + public ContentType RequestContentType { get; } + public string XmlRootName { get; } + public bool IgnoreCertificate { get; } + public TimeSpan? Timeout { get; } + public bool Version { get; } + public bool Help { get; } + public bool StopAutoRedirects { get; } + + public bool RequiresHandler => IgnoreCertificate || StopAutoRedirects; + + public Options(ContentType requestContentType, string xmlRootName, bool ignoreCertificate, TimeSpan? timeout, bool version, bool help, bool stopAutoRedirects) + { + RequestContentType = requestContentType; + XmlRootName = xmlRootName; + IgnoreCertificate = ignoreCertificate; + Timeout = timeout; + Version = version; + Help = help; + StopAutoRedirects = stopAutoRedirects; + } + + public static IEnumerable GetOptionHelp() + { + yield return "--form Renders the content arguments as application/x-www-form-urlencoded"; + yield return "--help Show command line help."; + yield return "--ignore-certificate Prevents server certificate validation."; + yield return "--json Renders the content arguments as application/json."; + yield return "--timeout= Sets the timeout of the request using System.TimeSpan.TryParse (https://docs.microsoft.com/en-us/dotnet/api/system.timespan.parse)"; + yield return "--version Displays the application verison."; + yield return "--xml= Renders the content arguments as application/xml using the optional xml root name."; + yield return "--stop-auto-redirects Prevents redirects from automatically being processed."; + } + + static int GetArgValueIndex(string arg) + { + var equalsIndex = arg.IndexOf('='); + var spaceIndex = arg.IndexOf(' '); + var index = equalsIndex > -1 && spaceIndex > -1 + ? Math.Min(equalsIndex, spaceIndex) + : Math.Max(equalsIndex, spaceIndex); + + return index == -1 ? index : index + 1; + } + + public static Options Parse(IEnumerable args) + { + var requestContentType = ContentType.Json; + var xmlRootName = default(string); + var ignoreCertificate = false; + var timeout = default(TimeSpan?); + var help = false; + var version = false; + var stopAutoRedirects = false; + foreach (var arg in args) + { + if (arg.StartsWith("--json")) + { + requestContentType = ContentType.Json; + } + else if (arg.StartsWith("--xml")) + { + var index = GetArgValueIndex(arg); + if (index == -1) + { + xmlRootName = "xml"; + } + else + { + xmlRootName = arg.Substring(index).Trim(); + if (string.IsNullOrEmpty(xmlRootName)) + { + xmlRootName = "xml"; + } + } + requestContentType = ContentType.Xml; + } + else if (arg.StartsWith("--form")) + { + requestContentType = ContentType.FormUrlEncoded; + } + else if (arg.StartsWith("--ignore-certificate")) + { + ignoreCertificate = true; + } + else if (arg.StartsWith("--timeout")) + { + var index = GetArgValueIndex(arg); + if (index > -1) + { + var s = arg.Substring(index).Trim(); + if (TimeSpan.TryParse(s, out var to) && to > TimeSpan.Zero) + { + timeout = to; + } + } + } + else if (arg.StartsWith("--version")) + { + version = true; + } + else if (arg.StartsWith("--help") || arg.StartsWith("-?") || arg.StartsWith("help")) + { + help = true; + } + else if (arg.StartsWith("--stop-auto-redirects")) + { + stopAutoRedirects = true; + } + } + return new Options(requestContentType, xmlRootName, ignoreCertificate, timeout, version, help, stopAutoRedirects); + } + } +} diff --git a/src/https/Program.cs b/src/https/Program.cs index 9c405df..8633d30 100644 --- a/src/https/Program.cs +++ b/src/https/Program.cs @@ -1,119 +1,13 @@ using System; -using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using System.Xml.Linq; -using Utf8Json; namespace Https { - static class RequestContentFormatter - { - public static HttpContent As(ContentType requestContentType, IEnumerable contents, string xmlRootName) - { - switch (requestContentType) - { - case ContentType.FormUrlEncoded: - return AsFormUrlEncoded(contents); - case ContentType.Xml: - return AsXml(xmlRootName, contents); - case ContentType.Json: - return AsJson(contents); - default: - throw new ArgumentOutOfRangeException(nameof(requestContentType), requestContentType, "Invalid request content type"); - } - } - - public static HttpContent AsFormUrlEncoded(IEnumerable contents) - { - var pairs = contents.Select(content => new KeyValuePair(content.Property, content.Value)); - return new FormUrlEncodedContent(pairs); - } - - public static HttpContent AsXml(string root, IEnumerable contents) - { - var xdocument = new XDocument( - new XElement( - root, - contents.Select(content => new XElement(content.Property, content.Value)) - ) - ); - - var stream = new MemoryStream(); - xdocument.Save(stream); - stream.Position = 0; - var streamContent = new StreamContent(stream); - streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); - return streamContent; - } - - public static HttpContent AsJson(IEnumerable contents) - { - var bytes = ArrayPool.Shared.Rent(100); - var writer = new JsonWriter(bytes); - - writer.WriteBeginObject(); - var counter = 0; - foreach (var content in contents) - { - if (counter++ > 0) - { - writer.WriteValueSeparator(); - } - - writer.WritePropertyName(content.Property); - - writer.WriteString(content.Value); - } - writer.WriteEndObject(); - - var stream = new MemoryStream(writer.ToUtf8ByteArray()); - var streamContent = new StreamContent(stream); - streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json") - { - CharSet = "utf-8" - }; - return streamContent; - } - } - - static class ResponseContentFormatter - { - public static async Task As(HttpResponseMessage response, StreamWriter target) - { - using (var stream = await response.Content.ReadAsStreamAsync()) - { - switch (response.Content.Headers.ContentType?.MediaType) - { - case "application/json": - await AsJson(stream, target); - break; - default: - await AsOrigin(stream, target); - break; - case "application/xml": - await AsXml(stream, target); - break; - } - } - } - - static async Task AsOrigin(Stream source, StreamWriter target) - { - await source.CopyToAsync(target.BaseStream); - } - - static Task AsXml(Stream source, StreamWriter target) => - AsOrigin(source, target); - - static Task AsJson(Stream source, StreamWriter target) => - AsOrigin(source, target); - } - public class Program { static IEnumerable ParseContents(IEnumerable args) @@ -363,413 +257,4 @@ static HttpClient CreateHttpClient(Options options) return http; } } - - enum ContentType - { - Json = 1, - FormUrlEncoded = 2, - Xml = 3 - } - - class Options - { - public ContentType RequestContentType { get; } - public string XmlRootName { get; } - public bool IgnoreCertificate { get; } - public TimeSpan? Timeout { get; } - public bool Version { get; } - public bool Help { get; } - public bool StopAutoRedirects { get; } - - public bool RequiresHandler => IgnoreCertificate || StopAutoRedirects; - - public Options(ContentType requestContentType, string xmlRootName, bool ignoreCertificate, TimeSpan? timeout, bool version, bool help, bool stopAutoRedirects) - { - RequestContentType = requestContentType; - XmlRootName = xmlRootName; - IgnoreCertificate = ignoreCertificate; - Timeout = timeout; - Version = version; - Help = help; - StopAutoRedirects = stopAutoRedirects; - } - - public static IEnumerable GetOptionHelp() - { - yield return "--form Renders the content arguments as application/x-www-form-urlencoded"; - yield return "--help Show command line help."; - yield return "--ignore-certificate Prevents server certificate validation."; - yield return "--json Renders the content arguments as application/json."; - yield return "--timeout= Sets the timeout of the request using System.TimeSpan.TryParse (https://docs.microsoft.com/en-us/dotnet/api/system.timespan.parse)"; - yield return "--version Displays the application verison."; - yield return "--xml= Renders the content arguments as application/xml using the optional xml root name."; - yield return "--stop-auto-redirects Prevents redirects from automatically being processed."; - } - - static int GetArgValueIndex(string arg) - { - var equalsIndex = arg.IndexOf('='); - var spaceIndex = arg.IndexOf(' '); - var index = equalsIndex > -1 && spaceIndex > -1 - ? Math.Min(equalsIndex, spaceIndex) - : Math.Max(equalsIndex, spaceIndex); - - return index == -1 ? index : index + 1; - } - - public static Options Parse(IEnumerable args) - { - var requestContentType = ContentType.Json; - var xmlRootName = default(string); - var ignoreCertificate = false; - var timeout = default(TimeSpan?); - var help = false; - var version = false; - var stopAutoRedirects = false; - foreach (var arg in args) - { - if (arg.StartsWith("--json")) - { - requestContentType = ContentType.Json; - } - else if (arg.StartsWith("--xml")) - { - var index = GetArgValueIndex(arg); - if (index == -1) - { - xmlRootName = "xml"; - } - else - { - xmlRootName = arg.Substring(index).Trim(); - if (string.IsNullOrEmpty(xmlRootName)) - { - xmlRootName = "xml"; - } - } - requestContentType = ContentType.Xml; - } - else if (arg.StartsWith("--form")) - { - requestContentType = ContentType.FormUrlEncoded; - } - else if (arg.StartsWith("--ignore-certificate")) - { - ignoreCertificate = true; - } - else if (arg.StartsWith("--timeout")) - { - var index = GetArgValueIndex(arg); - if (index > -1) - { - var s = arg.Substring(index).Trim(); - if (TimeSpan.TryParse(s, out var to) && to > TimeSpan.Zero) - { - timeout = to; - } - } - } - else if (arg.StartsWith("--version")) - { - version = true; - } - else if (arg.StartsWith("--help") || arg.StartsWith("-?") || arg.StartsWith("help")) - { - help = true; - } - else if (arg.StartsWith("--stop-auto-redirects")) - { - stopAutoRedirects = true; - } - } - return new Options(requestContentType, xmlRootName, ignoreCertificate, timeout, version, help, stopAutoRedirects); - } - } - - enum ContentLocation - { - Body = 1, - Header = 2 - } - - class Content - { - public ContentLocation ContentLocation { get; } - public string Property { get; } - public string Value { get; } - Content(ContentLocation contentLocation, string property, string value) - { - ContentLocation = contentLocation; - Property = property; - Value = value; - } - - public static bool TryParse(string s, out Content content) - { - var equalsIndex = s.IndexOf('='); - var colonIndex = s.IndexOf(':'); - if (equalsIndex == -1 && colonIndex == -1) - { - content = default; - return false; - } - - var contentType = default(ContentLocation); - var index = default(int); - if (equalsIndex > -1 && colonIndex > -1) - { - if (equalsIndex < colonIndex) - { - contentType = ContentLocation.Body; - index = equalsIndex; - } - else - { - contentType = ContentLocation.Header; - index = colonIndex; - } - } - else if (equalsIndex > -1) - { - contentType = ContentLocation.Body; - index = equalsIndex; - } - else - { - contentType = ContentLocation.Header; - index = colonIndex; - } - - var property = s.Substring(0, index); - if (property.Length == 0) - { - content = default; - return false; - } - - var value = s.Substring(index + 1); - content = new Content(contentType, property, value); - return true; - } - } - - class Renderer - { - readonly StreamWriter _output; - readonly StreamWriter _info; - public Renderer(StreamWriter output, StreamWriter info) - { - _output = output; - _info = info; - } - - public async Task WriteResponse(HttpResponseMessage response) - { - _info.Write("HTTP/"); - _info.Write(response.Version); - _info.Write(" "); - _info.Write((int)response.StatusCode); - _info.Write(" "); - _info.WriteLine(response.ReasonPhrase); - - WriteHeaders(response.Headers, response.Content.Headers); - await ResponseContentFormatter.As(response, _output); - } - - public void WriteHeaders(HttpResponseHeaders responseHeaders, HttpContentHeaders contentHeaders) - { - var headers = responseHeaders.Concat(contentHeaders); - - foreach (var header in headers) - { - foreach (var value in header.Value) - { - _info.Write(header.Key); - _info.Write(":"); - _info.Write(" "); - _info.WriteLine(value); - } - } - } - - public void WriteException(Exception ex) - { - var help = WriteException(ex, 0); - switch (help) - { - case ExceptionHelp.Timeout: - _info.WriteLine("Request failed to complete within timeout. Try increasing the timeout with the --timeout flag"); - break; - case ExceptionHelp.IgnoreCertificate: - _info.WriteLine("Ensure you trust the server certificate or try using the --ignore-certificate flag"); - break; - } - } - - ExceptionHelp WriteException(Exception ex, int depth) - { - if (depth > 0) - { - _info.Write(new string('\t', depth)); - } - _info.WriteLine(ex.Message); - - var exceptionHelp = ExceptionHelp.None; - if (ex is TaskCanceledException || ex is OperationCanceledException) - { - return ExceptionHelp.Timeout; - } - else - { - switch (ex.Message) - { - case "The SSL connection could not be established, see inner exception.": - exceptionHelp = ExceptionHelp.IgnoreCertificate; - break; - } - } - - if (ex.InnerException != null) - { - var otherHelp = WriteException(ex.InnerException, depth + 1); - if (otherHelp != ExceptionHelp.None) - { - return otherHelp; - } - } - - return exceptionHelp; - } - - enum ExceptionHelp - { - None = 0, - IgnoreCertificate = 1, - Timeout = 2 - } - } - - - struct Command - { - public HttpMethod Method { get; } - public Uri Uri { get; } - - Command(Uri uri) - { - Method = default; - Uri = uri ?? throw new ArgumentNullException(nameof(uri)); - } - Command(HttpMethod method, Uri uri) - { - Method = method ?? throw new ArgumentNullException(nameof(method)); - Uri = uri ?? throw new ArgumentNullException(nameof(uri)); - } - - static bool StartsWithHttp(string s) => - s.Length > 6 && s[0] == 'h' && s[1] == 't' && s[2] == 't' && s[3] == 'p' && (s[4] == ':' || (s[4] == 's' && s[5] == ':')); - - static bool TryParseUri(string s, out Uri uri) - { - if (!StartsWithHttp(s)) - { - s = "https://" + s; - } - - return Uri.TryCreate(s, UriKind.Absolute, out uri); - } - - static bool TryParseMethod(string s, out HttpMethod method) - { - if (s.Equals(nameof(HttpMethod.Delete), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Delete; - return true; - } - else if (s.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Get; - return true; - } - else if (s.Equals(nameof(HttpMethod.Head), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Head; - return true; - } - else if (s.Equals(nameof(HttpMethod.Options), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Options; - return true; - } - else if (s.Equals(nameof(HttpMethod.Patch), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Patch; - return true; - } - else if (s.Equals(nameof(HttpMethod.Post), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Post; - return true; - } - else if (s.Equals(nameof(HttpMethod.Put), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Put; - return true; - } - else if (s.Equals(nameof(HttpMethod.Trace), StringComparison.OrdinalIgnoreCase)) - { - method = HttpMethod.Trace; - return true; - } - - method = default; - return false; - } - - public static bool TryParse(string methodText, string uriText, out Command command) - { - if (TryParseMethod(methodText, out var method) && TryParseUri(uriText, out var uri)) - { - command = new Command(method, uri); - return true; - } - else - { - command = default; - return false; - } - } - - public static bool TryParse(string s, out Command command) - { - s = s.Trim(); - if (s.StartsWith('-') || s == "help") - { - command = default; - return false; - } - - var index = s.IndexOf(' '); - if (index == -1) - { - if (TryParseUri(s, out var uri)) - { - command = new Command(uri); - return true; - } - else - { - command = default; - return false; - } - } - else - { - var methodText = s.Substring(0, index); - var uriText = s.Substring(index + 1); - - return TryParse(methodText, uriText, out command); - } - } - } } diff --git a/src/https/Renderer.cs b/src/https/Renderer.cs new file mode 100644 index 0000000..436461d --- /dev/null +++ b/src/https/Renderer.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; + +namespace Https +{ + class Renderer + { + readonly StreamWriter _output; + readonly StreamWriter _info; + public Renderer(StreamWriter output, StreamWriter info) + { + _output = output; + _info = info; + } + + public async Task WriteResponse(HttpResponseMessage response) + { + _info.Write("HTTP/"); + _info.Write(response.Version); + _info.Write(" "); + _info.Write((int)response.StatusCode); + _info.Write(" "); + _info.WriteLine(response.ReasonPhrase); + + WriteHeaders(response.Headers, response.Content.Headers); + await ResponseContentFormatter.As(response, _output); + } + + public void WriteHeaders(HttpResponseHeaders responseHeaders, HttpContentHeaders contentHeaders) + { + var headers = responseHeaders.Concat(contentHeaders); + + foreach (var header in headers) + { + foreach (var value in header.Value) + { + _info.Write(header.Key); + _info.Write(":"); + _info.Write(" "); + _info.WriteLine(value); + } + } + } + + public void WriteException(Exception ex) + { + var help = WriteException(ex, 0); + switch (help) + { + case ExceptionHelp.Timeout: + _info.WriteLine("Request failed to complete within timeout. Try increasing the timeout with the --timeout flag"); + break; + case ExceptionHelp.IgnoreCertificate: + _info.WriteLine("Ensure you trust the server certificate or try using the --ignore-certificate flag"); + break; + } + } + + ExceptionHelp WriteException(Exception ex, int depth) + { + if (depth > 0) + { + _info.Write(new string('\t', depth)); + } + _info.WriteLine(ex.Message); + + var exceptionHelp = ExceptionHelp.None; + if (ex is TaskCanceledException || ex is OperationCanceledException) + { + return ExceptionHelp.Timeout; + } + else + { + switch (ex.Message) + { + case "The SSL connection could not be established, see inner exception.": + exceptionHelp = ExceptionHelp.IgnoreCertificate; + break; + } + } + + if (ex.InnerException != null) + { + var otherHelp = WriteException(ex.InnerException, depth + 1); + if (otherHelp != ExceptionHelp.None) + { + return otherHelp; + } + } + + return exceptionHelp; + } + + enum ExceptionHelp + { + None = 0, + IgnoreCertificate = 1, + Timeout = 2 + } + } +} diff --git a/src/https/RequestContentFormatter.cs b/src/https/RequestContentFormatter.cs new file mode 100644 index 0000000..532ff07 --- /dev/null +++ b/src/https/RequestContentFormatter.cs @@ -0,0 +1,82 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Xml.Linq; +using Utf8Json; + +namespace Https +{ + static class RequestContentFormatter + { + public static HttpContent As(ContentType requestContentType, IEnumerable contents, string xmlRootName) + { + switch (requestContentType) + { + case ContentType.FormUrlEncoded: + return AsFormUrlEncoded(contents); + case ContentType.Xml: + return AsXml(xmlRootName, contents); + case ContentType.Json: + return AsJson(contents); + default: + throw new ArgumentOutOfRangeException(nameof(requestContentType), requestContentType, "Invalid request content type"); + } + } + + public static HttpContent AsFormUrlEncoded(IEnumerable contents) + { + var pairs = contents.Select(content => new KeyValuePair(content.Property, content.Value)); + return new FormUrlEncodedContent(pairs); + } + + public static HttpContent AsXml(string root, IEnumerable contents) + { + var xdocument = new XDocument( + new XElement( + root, + contents.Select(content => new XElement(content.Property, content.Value)) + ) + ); + + var stream = new MemoryStream(); + xdocument.Save(stream); + stream.Position = 0; + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/xml"); + return streamContent; + } + + public static HttpContent AsJson(IEnumerable contents) + { + var bytes = ArrayPool.Shared.Rent(100); + var writer = new JsonWriter(bytes); + + writer.WriteBeginObject(); + var counter = 0; + foreach (var content in contents) + { + if (counter++ > 0) + { + writer.WriteValueSeparator(); + } + + writer.WritePropertyName(content.Property); + + writer.WriteString(content.Value); + } + writer.WriteEndObject(); + + var stream = new MemoryStream(writer.ToUtf8ByteArray()); + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json") + { + CharSet = "utf-8" + }; + return streamContent; + } + } +} diff --git a/src/https/ResponseContentFormatter.cs b/src/https/ResponseContentFormatter.cs new file mode 100644 index 0000000..17a29b4 --- /dev/null +++ b/src/https/ResponseContentFormatter.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Https +{ + static class ResponseContentFormatter + { + public static async Task As(HttpResponseMessage response, StreamWriter target) + { + using (var stream = await response.Content.ReadAsStreamAsync()) + { + switch (response.Content.Headers.ContentType?.MediaType) + { + case "application/json": + await AsJson(stream, target); + break; + default: + await AsOrigin(stream, target); + break; + case "application/xml": + await AsXml(stream, target); + break; + } + } + } + + static async Task AsOrigin(Stream source, StreamWriter target) + { + await source.CopyToAsync(target.BaseStream); + } + + static Task AsXml(Stream source, StreamWriter target) => + AsOrigin(source, target); + + static Task AsJson(Stream source, StreamWriter target) => + AsOrigin(source, target); + } +} From 823c9c5109b28a95d50438a4108fbe28e523878e Mon Sep 17 00:00:00 2001 From: joncloud Date: Wed, 5 Aug 2020 18:34:24 -0700 Subject: [PATCH 31/41] Adds certificate tests --- azure-pipelines.yml | 4 +++ tests/https.Tests/CertificateTests.cs | 40 +++++++++++++++++++++++++++ tests/https.Tests/IntegrationTests.cs | 6 ++-- tests/https.Tests/WebHostFixture.cs | 8 ++++-- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 tests/https.Tests/CertificateTests.cs diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3877c30..1fd91f2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -36,6 +36,10 @@ steps: minorVersion: 2 patchVersion: 0 +- script: | + dotnet dev-certs https + displayName: Setup Tests + - task: DotNetCoreCLI@2 displayName: Test https inputs: diff --git a/tests/https.Tests/CertificateTests.cs b/tests/https.Tests/CertificateTests.cs new file mode 100644 index 0000000..966c0bd --- /dev/null +++ b/tests/https.Tests/CertificateTests.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace Https.Tests +{ + public class CertificateTests : IClassFixture + { + readonly WebHostFixture _fixture; + public CertificateTests(WebHostFixture fixture) => + _fixture = fixture; + + [Fact] + public async Task UntrustedCertificate_ShouldPresentErrorMessage_GivenCertificatesAreHonored() + { + var args = new[] + { + "post", $"{_fixture.HttpsUrl}/Mirror", "--form", "foo=bar", "lorem=ipsum" + }; + + var result = await Https.ExecuteAsync(args); + + Assert.Equal(1, result.ExitCode); + } + + [Fact] + public async Task UntrustedCertificate_ShouldProcess_GivenCertificatesAreIgnored() + { + var args = new[] + { + "post", $"{_fixture.HttpsUrl}/Mirror", "--form", "--ignore-certificate", "foo=bar", "lorem=ipsum" + }; + + var result = await Https.ExecuteAsync(args); + + var actual = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("foo=bar&lorem=ipsum", actual); + } + } +} diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/IntegrationTests.cs index 1614edc..e686a2d 100644 --- a/tests/https.Tests/IntegrationTests.cs +++ b/tests/https.Tests/IntegrationTests.cs @@ -18,7 +18,7 @@ public async Task MirrorTest_ShouldReflectFormUrlEncoded() { var args = new[] { - "post", $"{_fixture.Url}/Mirror", "--form", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.HttpUrl}/Mirror", "--form", "foo=bar", "lorem=ipsum" }; var result = await Https.ExecuteAsync(args); @@ -32,7 +32,7 @@ public async Task MirrorTest_ShouldReflectJson() { var args = new[] { - "post", $"{_fixture.Url}/Mirror", "--json", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.HttpUrl}/Mirror", "--json", "foo=bar", "lorem=ipsum" }; var result = await Https.ExecuteAsync(args); @@ -46,7 +46,7 @@ public async Task MirrorTest_ShouldReflectXml() { var args = new[] { - "post", $"{_fixture.Url}/Mirror", "--xml=root", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.HttpUrl}/Mirror", "--xml=root", "foo=bar", "lorem=ipsum" }; var result = await Https.ExecuteAsync(args); diff --git a/tests/https.Tests/WebHostFixture.cs b/tests/https.Tests/WebHostFixture.cs index 7982689..067690d 100644 --- a/tests/https.Tests/WebHostFixture.cs +++ b/tests/https.Tests/WebHostFixture.cs @@ -11,12 +11,16 @@ public class WebHostFixture : IDisposable readonly IWebHost _webHost; readonly Task _task; readonly CancellationTokenSource _cts; - public string Url { get; } + public string HttpUrl { get; } + public string HttpsUrl { get; } public WebHostFixture() { _webHost = WebHost.CreateDefaultBuilder() - .UseUrls(Url = "http://localhost:5000") + .UseUrls( + HttpUrl = "http://localhost:5000", + HttpsUrl = "https://localhost:5001" + ) .UseStartup() .Build(); _cts = new CancellationTokenSource(); From 56b1cf47db16ef9373e9e4d766327820a426b1e1 Mon Sep 17 00:00:00 2001 From: joncloud Date: Wed, 5 Aug 2020 18:38:58 -0700 Subject: [PATCH 32/41] Separates tests out a bit --- ...ntegrationTests.cs => ContentTypeTests.cs} | 20 ++------------ tests/https.Tests/RedirectTests.cs | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) rename tests/https.Tests/{IntegrationTests.cs => ContentTypeTests.cs} (73%) create mode 100644 tests/https.Tests/RedirectTests.cs diff --git a/tests/https.Tests/IntegrationTests.cs b/tests/https.Tests/ContentTypeTests.cs similarity index 73% rename from tests/https.Tests/IntegrationTests.cs rename to tests/https.Tests/ContentTypeTests.cs index e686a2d..906d3e1 100644 --- a/tests/https.Tests/IntegrationTests.cs +++ b/tests/https.Tests/ContentTypeTests.cs @@ -1,16 +1,14 @@ using System.IO; -using System.Text; using System.Threading.Tasks; -using System.Xml; using System.Xml.Linq; using Xunit; namespace Https.Tests { - public class IntegrationTests : IClassFixture + public class ContentTypeTests : IClassFixture { readonly WebHostFixture _fixture; - public IntegrationTests(WebHostFixture fixture) => + public ContentTypeTests(WebHostFixture fixture) => _fixture = fixture; [Fact] @@ -64,19 +62,5 @@ public async Task MirrorTest_ShouldReflectXml() Assert.Equal(expected, actual); } - - [Fact] - public async Task RedirectTest_ShouldShow3XXResponse_GivenStopAutoRedirects() - { - var args = new[] - { - "get", "http://localhost:5000/Redirect", "--stop-auto-redirects" - }; - - var result = await Https.ExecuteAsync(args); - - Assert.Equal("HTTP/1.1 301 Moved Permanently", result.Status); - Assert.Equal("http://localhost:5000/Mirror", result.Headers["Location"]); - } } } diff --git a/tests/https.Tests/RedirectTests.cs b/tests/https.Tests/RedirectTests.cs new file mode 100644 index 0000000..ad5d005 --- /dev/null +++ b/tests/https.Tests/RedirectTests.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Https.Tests +{ + public class RedirectTests : IClassFixture + { + readonly WebHostFixture _fixture; + public RedirectTests(WebHostFixture fixture) => + _fixture = fixture; + + [Fact] + public async Task RedirectTest_ShouldShow3XXResponse_GivenStopAutoRedirects() + { + var args = new[] + { + "get", $"{_fixture.HttpUrl}/Redirect", "--stop-auto-redirects" + }; + + var result = await Https.ExecuteAsync(args); + + Assert.Equal("HTTP/1.1 301 Moved Permanently", result.Status); + Assert.Equal($"{_fixture.HttpUrl}/Mirror", result.Headers["Location"]); + } + } +} From 911c932156242546540f87b6f4a1ffc65ab1cfca Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:22:29 -0700 Subject: [PATCH 33/41] Upgrades to .NET 5.0 --- README.md | 2 +- azure-pipelines.yml | 6 ++++++ global.json | 5 +++++ src/https/https.csproj | 4 ++-- tests/https.Tests/https.Tests.csproj | 6 +++--- 5 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 global.json diff --git a/README.md b/README.md index 239220c..9c8d3de 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Released under the MIT License. See the [LICENSE][] File for further details. ## Installation Install `https` as a global .NET tool using ```bash -dotnet tool install --global https --version 0.2.0-* +dotnet tool install --global https --version 0.3.0-* ``` ## Usage diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1fd91f2..eda07a8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -11,6 +11,12 @@ variables: shouldPublish: $[eq(variables['Build.SourceBranch'], 'refs/heads/publish')] steps: +- task: UseDotNet@2 + displayName: Use .NET (global.json) + inputs: + packageType: 'sdk' + useGlobalJson: true + - task: DotNetCoreCLI@2 displayName: Pack https condition: eq(variables.shouldPublish, true) diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4ee6c0 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "5.*" + } +} \ No newline at end of file diff --git a/src/https/https.csproj b/src/https/https.csproj index 6dc70ac..92a7b2d 100644 --- a/src/https/https.csproj +++ b/src/https/https.csproj @@ -2,12 +2,12 @@ Exe - netcoreapp3.1 + net5.0 preview - 0.2.0 + 0.3.0 diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index 261f038..a1d99d5 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net5.0 false Https.Tests preview @@ -17,8 +17,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all From 00b3da149bb3cd7574cf6e94fed187a124a8e7b5 Mon Sep 17 00:00:00 2001 From: joncloud Date: Sat, 1 Aug 2020 19:25:53 -0700 Subject: [PATCH 34/41] Sets static version --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index f4ee6c0..2a023e8 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "5.*" + "version": "5.0.100-preview.7.20366.6" } } \ No newline at end of file From e9d330d7cfc8cf86fc7197def8688a130444da99 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 09:07:09 -0800 Subject: [PATCH 35/41] Bumps to stable --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 2a023e8..1be13b5 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "5.0.100-preview.7.20366.6" + "version": "5.0.0" } } \ No newline at end of file From 2ef9db9da73bb3779f99bcfa6ff2d10811757b00 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 09:09:10 -0800 Subject: [PATCH 36/41] Upgrades to stable versions --- tests/https.Tests/https.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/https.Tests/https.Tests.csproj b/tests/https.Tests/https.Tests.csproj index a1d99d5..d94fd49 100644 --- a/tests/https.Tests/https.Tests.csproj +++ b/tests/https.Tests/https.Tests.csproj @@ -17,8 +17,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all From 52c3c92b5e6a7d782f94d1ad7f744bacda80f779 Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 09:10:20 -0800 Subject: [PATCH 37/41] Corrected version number --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 1be13b5..2cb2ac9 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "5.0.0" + "version": "5.0.100" } } \ No newline at end of file From 9f2dfa25767ab61b05ac5fa468327824e44e5e3c Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 09:23:51 -0800 Subject: [PATCH 38/41] Fixes tests to share WebHostFixture --- tests/https.Tests/CertificateTests.cs | 3 ++- tests/https.Tests/ContentTypeTests.cs | 3 ++- tests/https.Tests/RedirectTests.cs | 3 ++- tests/https.Tests/WebHostFixture.cs | 4 ++++ 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/https.Tests/CertificateTests.cs b/tests/https.Tests/CertificateTests.cs index 966c0bd..0b79c26 100644 --- a/tests/https.Tests/CertificateTests.cs +++ b/tests/https.Tests/CertificateTests.cs @@ -4,7 +4,8 @@ namespace Https.Tests { - public class CertificateTests : IClassFixture + [Collection(nameof(WebHostFixture))] + public class CertificateTests { readonly WebHostFixture _fixture; public CertificateTests(WebHostFixture fixture) => diff --git a/tests/https.Tests/ContentTypeTests.cs b/tests/https.Tests/ContentTypeTests.cs index 906d3e1..e2d2488 100644 --- a/tests/https.Tests/ContentTypeTests.cs +++ b/tests/https.Tests/ContentTypeTests.cs @@ -5,7 +5,8 @@ namespace Https.Tests { - public class ContentTypeTests : IClassFixture + [Collection(nameof(WebHostFixture))] + public class ContentTypeTests { readonly WebHostFixture _fixture; public ContentTypeTests(WebHostFixture fixture) => diff --git a/tests/https.Tests/RedirectTests.cs b/tests/https.Tests/RedirectTests.cs index ad5d005..98959e0 100644 --- a/tests/https.Tests/RedirectTests.cs +++ b/tests/https.Tests/RedirectTests.cs @@ -3,7 +3,8 @@ namespace Https.Tests { - public class RedirectTests : IClassFixture + [Collection(nameof(WebHostFixture))] + public class RedirectTests { readonly WebHostFixture _fixture; public RedirectTests(WebHostFixture fixture) => diff --git a/tests/https.Tests/WebHostFixture.cs b/tests/https.Tests/WebHostFixture.cs index 067690d..6954139 100644 --- a/tests/https.Tests/WebHostFixture.cs +++ b/tests/https.Tests/WebHostFixture.cs @@ -3,9 +3,13 @@ using System; using System.Threading; using System.Threading.Tasks; +using Xunit; namespace Https.Tests { + [CollectionDefinition(nameof(WebHostFixture))] + public class WebHostCollection : ICollectionFixture { } + public class WebHostFixture : IDisposable { readonly IWebHost _webHost; From 6d9a627643d4edaf7f63dcbb30e14d2a39ab5c3e Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 19:20:41 -0800 Subject: [PATCH 39/41] Improves test coverage * Adds tests for various xml root names * Refactors csproj VersionPrefix to be shared * Adds tests to validate version option * Adds tests to validate timeout --- tests/https.Tests/ContentTypeTests.cs | 60 +++++++++++++++++++++---- tests/https.Tests/HttpsCsprojFixture.cs | 20 ++++++++- tests/https.Tests/HttpsResult.cs | 6 +++ tests/https.Tests/ReadmeTests.cs | 8 +--- tests/https.Tests/Startup.cs | 1 + tests/https.Tests/TimeoutMiddleware.cs | 29 ++++++++++++ tests/https.Tests/TimeoutTests.cs | 43 ++++++++++++++++++ tests/https.Tests/VersionTests.cs | 34 ++++++++++++++ 8 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 tests/https.Tests/TimeoutMiddleware.cs create mode 100644 tests/https.Tests/TimeoutTests.cs create mode 100644 tests/https.Tests/VersionTests.cs diff --git a/tests/https.Tests/ContentTypeTests.cs b/tests/https.Tests/ContentTypeTests.cs index e2d2488..276aa69 100644 --- a/tests/https.Tests/ContentTypeTests.cs +++ b/tests/https.Tests/ContentTypeTests.cs @@ -40,15 +40,62 @@ public async Task MirrorTest_ShouldReflectJson() Assert.Equal("{\"foo\":\"bar\",\"lorem\":\"ipsum\"}", actual); } + static async Task RunXmlTestAsync(string[] args, XDocument expected) + { + var result = await Https.ExecuteAsync(args); + + var actual = XDocument.Load( + new StreamReader(result.StdOut) + ).ToString(); + + Assert.Equal(expected.ToString(), actual); + } + [Fact] - public async Task MirrorTest_ShouldReflectXml() + public async Task MirrorTest_ShouldReflectXmlWithDefaultRootElementName() { var args = new[] { - "post", $"{_fixture.HttpUrl}/Mirror", "--xml=root", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.HttpUrl}/Mirror", "--xml", "foo=bar", "lorem=ipsum" }; - var result = await Https.ExecuteAsync(args); + var expected = new XDocument( + new XElement( + "xml", + new XElement("foo", "bar"), + new XElement("lorem", "ipsum") + ) + ); + + await RunXmlTestAsync(args, expected); + } + + [Fact] + public async Task MirrorTest_ShouldReflectXmlWithEmptyRootElementName() + { + var args = new[] + { + "post", $"{_fixture.HttpUrl}/Mirror", "--xml", "foo=bar", "lorem=ipsum" + }; + + var expected = new XDocument( + new XElement( + "xml", + new XElement("foo", "bar"), + new XElement("lorem", "ipsum") + ) + ); + + await RunXmlTestAsync(args, expected); + } + + [Fact] + public async Task MirrorTest_ShouldReflectXmlWithRootElementName() + { + var args = new[] + { + "post", $"{_fixture.HttpUrl}/Mirror", "--xml=root", "foo=bar", "lorem=ipsum" + }; var expected = new XDocument( new XElement( @@ -56,12 +103,9 @@ public async Task MirrorTest_ShouldReflectXml() new XElement("foo", "bar"), new XElement("lorem", "ipsum") ) - ).ToString(); - var actual = XDocument.Load( - new StreamReader(result.StdOut) - ).ToString(); + ); - Assert.Equal(expected, actual); + await RunXmlTestAsync(args, expected); } } } diff --git a/tests/https.Tests/HttpsCsprojFixture.cs b/tests/https.Tests/HttpsCsprojFixture.cs index eceb514..9b35638 100644 --- a/tests/https.Tests/HttpsCsprojFixture.cs +++ b/tests/https.Tests/HttpsCsprojFixture.cs @@ -1,4 +1,6 @@ -using System.Xml.Linq; +using System.Linq; +using System.Xml.Linq; +using Xunit; namespace Https.Tests { @@ -6,6 +8,22 @@ public class HttpsCsprojFixture { public XDocument Document { get; } + public XElement VersionPrefix + { + get + { + var value = Document + .Root + .Elements("PropertyGroup") + .Elements("VersionPrefix") + .FirstOrDefault(); + + Assert.NotNull(value); + + return value; + } + } + public HttpsCsprojFixture() { var path = "../../../../../src/https/https.csproj"; diff --git a/tests/https.Tests/HttpsResult.cs b/tests/https.Tests/HttpsResult.cs index 0983be0..192cc42 100644 --- a/tests/https.Tests/HttpsResult.cs +++ b/tests/https.Tests/HttpsResult.cs @@ -9,6 +9,7 @@ public class HttpsResult : IDisposable { public int ExitCode { get; } public MemoryStream StdOut { get; } + public MemoryStream StdErr { get; } public string Status { get; } public IReadOnlyDictionary Headers { get; } @@ -19,6 +20,11 @@ public HttpsResult(int exitCode, MemoryStream stdout, MemoryStream stderr) StdOut = stdout; StdOut.Position = 0; + stderr.Position = 0; + StdErr = new MemoryStream(); + stderr.CopyTo(StdErr); + StdErr.Position = 0; + stderr.Position = 0; var lines = new StreamReader(stderr) .ReadToEnd() diff --git a/tests/https.Tests/ReadmeTests.cs b/tests/https.Tests/ReadmeTests.cs index d31b6de..941ffac 100644 --- a/tests/https.Tests/ReadmeTests.cs +++ b/tests/https.Tests/ReadmeTests.cs @@ -19,13 +19,7 @@ public ReadmeTests(HttpsCsprojFixture httpsCsprojFixture, ReadmeFixture readmeFi [Fact] public void Installation_ShouldListSameVersionAsCsproj() { - var versionPrefixElement = _httpsCsprojFixture.Document - .Root - .Elements("PropertyGroup") - .Elements("VersionPrefix") - .FirstOrDefault(); - - Assert.NotNull(versionPrefixElement); + var versionPrefixElement = _httpsCsprojFixture.VersionPrefix; var expected = versionPrefixElement.Value; var actual = _readmeFixture.Readme.InstallationVersion; diff --git a/tests/https.Tests/Startup.cs b/tests/https.Tests/Startup.cs index 6c3cc9d..2e4dbfa 100644 --- a/tests/https.Tests/Startup.cs +++ b/tests/https.Tests/Startup.cs @@ -10,6 +10,7 @@ public void Configure(IApplicationBuilder app) { app.UseMiddleware(); app.UseMiddleware(); + app.UseMiddleware(); app.Run(async (context) => { diff --git a/tests/https.Tests/TimeoutMiddleware.cs b/tests/https.Tests/TimeoutMiddleware.cs new file mode 100644 index 0000000..cd13bfe --- /dev/null +++ b/tests/https.Tests/TimeoutMiddleware.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Http; +using System.Linq; +using System.Threading.Tasks; + +namespace Https.Tests +{ + class TimeoutMiddleware + { + readonly RequestDelegate _next; + public TimeoutMiddleware(RequestDelegate next) => + _next = next; + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments("/Timeout") && + context.Request.Query.TryGetValue("delay", out var delayValues) && + int.TryParse(delayValues.FirstOrDefault(), out var delay)) + { + await Task.Delay(delay); + context.Response.StatusCode = StatusCodes.Status200OK; + await context.Response.WriteAsync("Ignore this"); + } + else + { + await _next(context); + } + } + } +} diff --git a/tests/https.Tests/TimeoutTests.cs b/tests/https.Tests/TimeoutTests.cs new file mode 100644 index 0000000..2b767b3 --- /dev/null +++ b/tests/https.Tests/TimeoutTests.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Https.Tests +{ + [Collection(nameof(WebHostFixture))] + public class TimeoutTests + { + readonly WebHostFixture _fixture; + public TimeoutTests(WebHostFixture fixture) => + _fixture = fixture; + + [Fact] + public async Task TimeoutTest_ShouldRespectTimeoutOption() + { + var delay = 1_000; + var args = new[] + { + "post", $"{_fixture.HttpUrl}/Timeout?delay={delay * 4}", $"--timeout={TimeSpan.FromMilliseconds(delay)}" + }; + + var result = await Https.ExecuteAsync(args); + + Assert.Equal(1, result.ExitCode); + + var stdout = new StreamReader(result.StdOut).ReadToEnd(); + var expectedOut = ""; + Assert.Equal(expectedOut, stdout); + + var stderr = new StreamReader(result.StdErr).ReadToEnd(); + var expectedErr = string.Join(Environment.NewLine, new[] + { + "The request was canceled due to the configured HttpClient.Timeout of 1 seconds elapsing.", + "Request failed to complete within timeout. Try increasing the timeout with the --timeout flag", + "" + }); + Assert.Equal(expectedErr, stderr); + } + } +} diff --git a/tests/https.Tests/VersionTests.cs b/tests/https.Tests/VersionTests.cs new file mode 100644 index 0000000..f47093a --- /dev/null +++ b/tests/https.Tests/VersionTests.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Https.Tests +{ + public class VersionTests : IClassFixture + { + readonly HttpsCsprojFixture _httpsCsprojFixture; + public VersionTests(HttpsCsprojFixture httpsCsprojFixture) + { + _httpsCsprojFixture = httpsCsprojFixture; + } + + [Fact] + public async Task VersionFlag_ShouldReportVersion() + { + var args = new[] + { + "--version" + }; + + var versionPrefix = _httpsCsprojFixture.VersionPrefix.Value; + + var expected = $"dotnet-https {versionPrefix}.0" + Environment.NewLine; + var result = await Https.ExecuteAsync(args); + var actual = new StreamReader(result.StdOut).ReadToEnd(); + + Assert.Equal(expected, actual); + } + } +} \ No newline at end of file From a7621c0460614926d2c4bc2183eb3691bf217c1b Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 19:23:51 -0800 Subject: [PATCH 40/41] Fixes copied test --- tests/https.Tests/ContentTypeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/https.Tests/ContentTypeTests.cs b/tests/https.Tests/ContentTypeTests.cs index 276aa69..99d7840 100644 --- a/tests/https.Tests/ContentTypeTests.cs +++ b/tests/https.Tests/ContentTypeTests.cs @@ -75,7 +75,7 @@ public async Task MirrorTest_ShouldReflectXmlWithEmptyRootElementName() { var args = new[] { - "post", $"{_fixture.HttpUrl}/Mirror", "--xml", "foo=bar", "lorem=ipsum" + "post", $"{_fixture.HttpUrl}/Mirror", "--xml= ", "foo=bar", "lorem=ipsum" }; var expected = new XDocument( From 12e19597b5e1a035f5506ee572257138021d5b3b Mon Sep 17 00:00:00 2001 From: joncloud Date: Tue, 10 Nov 2020 19:35:32 -0800 Subject: [PATCH 41/41] Adds tests for various http methods --- tests/https.Tests/MethodTests.cs | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/https.Tests/MethodTests.cs diff --git a/tests/https.Tests/MethodTests.cs b/tests/https.Tests/MethodTests.cs new file mode 100644 index 0000000..0644f6c --- /dev/null +++ b/tests/https.Tests/MethodTests.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace Https.Tests +{ + [Collection(nameof(WebHostFixture))] + public class MethodTests + { + readonly WebHostFixture _fixture; + public MethodTests(WebHostFixture fixture) => + _fixture = fixture; + + [Fact] + public async Task MethodTest_ShouldHandleHead() + { + var args = new[] + { + "head", $"{_fixture.HttpUrl}/Mirror" + }; + + var result = await Https.ExecuteAsync(args); + + var actual = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("", actual); + Assert.Equal(0, result.ExitCode); + } + + // Most of these methods probably should be tested differently, + // but apparently they work through HttpClient and ASP.NET Core. + [InlineData(nameof(HttpMethod.Delete))] + [InlineData(nameof(HttpMethod.Get))] + [InlineData(nameof(HttpMethod.Options))] + [InlineData(nameof(HttpMethod.Patch))] + [InlineData(nameof(HttpMethod.Post))] + [InlineData(nameof(HttpMethod.Put))] + [InlineData(nameof(HttpMethod.Trace))] + [Theory] + public async Task MethodTest_ShouldHandleMethod(string method) + { + var args = new[] + { + method, $"{_fixture.HttpUrl}/Mirror", "--form", "foo=bar", "lorem=ipsum" + }; + + var result = await Https.ExecuteAsync(args); + + var actual = new StreamReader(result.StdOut).ReadToEnd(); + Assert.Equal("foo=bar&lorem=ipsum", actual); + } + } +}