From 5718ec0bd1568bab6ae8489bb3ebaf2dd4b9eb51 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Thu, 19 Nov 2020 21:09:25 +0100 Subject: [PATCH 1/5] Added NTLM authentication and small updates * nullable check * readme update * Added NTLM authentication * small asserter refactoring and readme file updates * version bump --- Directory.Build.props | 2 +- README.md | 42 ++++++++++- .../HttpTesterClientTests.cs | 40 ++++++++++ .../HttpTestAsserter.cs | 10 +-- .../HttpTesterClient.cs | 73 ++++++++++++------- .../Interfaces/IHttpTesterClient.cs | 12 +++ 6 files changed, 142 insertions(+), 37 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c2a8bf4..964bde6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.0 + 0.2.5 \ No newline at end of file diff --git a/README.md b/README.md index 7c80212..37260c6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # QAToolKit Engine HttpTester library -![https://github.com/qatoolkit/qatoolkit-engine-httptester-net/actions](![Build .NET Library](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/Build%20.NET%20Library/badge.svg)) -![https://github.com/qatoolkit/qatoolkit-engine-httptester-net/security/code-scanning](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/CodeQL%20Analyze/badge.svg) -![https://sonarcloud.io/dashboard?id=qatoolkit_qatoolkit-engine-httptester-net](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/Sonarqube%20Analyze/badge.svg) -![https://www.nuget.org/packages/QAToolKit.Engine.HttpTester/](https://img.shields.io/nuget/v/QAToolKit.Engine.HttpTester?label=QAToolKit.Engine.HttpTester) +[![Build .NET Library](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/Build%20.NET%20Library/badge.svg)](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/actions) +[![CodeQL](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/CodeQL%20Analyze/badge.svg)](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/security/code-scanning) +[![Sonarcloud Quality gate](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/Sonarqube%20Analyze/badge.svg)](https://sonarcloud.io/dashboard?id=qatoolkit_qatoolkit-engine-httptester-net) +[![NuGet package](https://img.shields.io/nuget/v/QAToolKit.Engine.HttpTester?label=QAToolKit.Engine.HttpTester)](https://www.nuget.org/packages/QAToolKit.Engine.HttpTester/) ## Description `QAToolKit.Engine.HttpTester` is a .NET Standard 2.1 library, that that contains an implementation of `IHttpTesterClient` that is a thin wrapper around .NET `HttpClient` to allow to write easy Http Request calls. @@ -69,6 +69,40 @@ using (var client = new HttpTesterClient()) } ``` +#### HttpTesterClient Authentication + +Currently `HttpTesterClient` supports: + +**Basic authentication** + +```csharp + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .... + .WithBasicAuthentication("user", "pass") + .Start(); +``` + +**Bearer token authentication** + +```csharp + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .... + .WithBearerAuthentication("eXy....") + .Start(); +``` + +**NTLM authentication** + +```csharp + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .... + .WithNTKMAuthentication("user", "pass") // or default security context .WithNTKMAuthentication() + .Start(); +``` + ### HttpTestAsserter This is an implementation of the HTTP response message asserter, which can be used to assert different paramters. diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index 3fa99ce..ec33461 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -401,5 +401,45 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithBearerAuthorizatio Assert.Equal("Giant", msg.brand.ToString()); } } + + [Fact] + public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMDefaultAuthorization_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net/api/bicycles?api-version=1")) + .WithJsonBody(BicycleFixture.GetCfr()) + .WithMethod(HttpMethod.Post) + .WithNTLMAuthentication() + .Start(); + + var msg = await response.GetResponseBody(); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Giant", msg.brand.ToString()); + } + } + + [Fact] + public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMAuthorization_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net/api/bicycles?api-version=1")) + .WithJsonBody(BicycleFixture.GetCfr()) + .WithMethod(HttpMethod.Post) + .WithNTLMAuthentication("user","pass") + .Start(); + + var msg = await response.GetResponseBody(); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Giant", msg.brand.ToString()); + } + } } } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index 364824a..ff7ae62 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -22,7 +22,7 @@ public class HttpTestAsserter : IHttpTestAsserter /// public HttpTestAsserter(HttpResponseMessage httpResponseMessage) { - _httpResponseMessage = httpResponseMessage; + _httpResponseMessage = httpResponseMessage ?? throw new ArgumentNullException($"{nameof(httpResponseMessage)} is null."); _assertResults = new List(); } @@ -45,7 +45,7 @@ public IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsens { if (string.IsNullOrEmpty(keyword)) { - throw new ArgumentNullException($"Keyword is null."); + throw new ArgumentNullException($"{nameof(keyword)} is null."); } var bodyString = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); @@ -54,7 +54,7 @@ public IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsens { Name = nameof(ResponseContentContains), Message = $"Body contains '{keyword}'.", - IsTrue = caseInsensitive == true ? StringHelper.ContainsCaseInsensitive(bodyString, keyword) : bodyString.Contains(keyword) + IsTrue = caseInsensitive ? StringHelper.ContainsCaseInsensitive(bodyString, keyword) : bodyString.Contains(keyword) }); return this; @@ -88,7 +88,7 @@ public IHttpTestAsserter ResponseHasHttpHeader(string headerName) { if (string.IsNullOrEmpty(headerName)) { - throw new ArgumentNullException($"Header name is null."); + throw new ArgumentNullException($"{nameof(headerName)} is null."); } _assertResults.Add(new AssertResult() @@ -111,7 +111,7 @@ public IHttpTestAsserter ResponseStatusCodeEquals(HttpStatusCode httpStatusCode) _assertResults.Add(new AssertResult() { Name = nameof(ResponseStatusCodeEquals), - Message = $"Expected status code is {httpStatusCode} returne code is {_httpResponseMessage.StatusCode}.", + Message = $"Expected status code is '{httpStatusCode}' return code is '{_httpResponseMessage.StatusCode}'.", IsTrue = _httpResponseMessage.StatusCode == httpStatusCode }); diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index 6bf490b..f7f3d23 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -20,6 +21,7 @@ public class HttpTesterClient : IHttpTesterClient, IDisposable /// HttpClient object /// public HttpClient HttpClient { get; private set; } + private HttpClientHandler HttpHandler { get; set; } private string _path = null; private Dictionary _headers = null; private string _body = null; @@ -40,43 +42,25 @@ public class HttpTesterClient : IHttpTesterClient, IDisposable /// public IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertificate = true) { + HttpHandler = new HttpClientHandler(); + if (!validateCertificate && (baseAddress.Scheme == Uri.UriSchemeHttp || baseAddress.Scheme == Uri.UriSchemeHttps)) { - NonValidatingClient(baseAddress); - } - else - { - ValidatingClient(baseAddress); - } - - return this; - } - - private void ValidatingClient(Uri baseAddress) - { - HttpClient = new HttpClient() - { - BaseAddress = baseAddress - }; - } - - private void NonValidatingClient(Uri baseAddress) - { - var handler = new HttpClientHandler - { - ClientCertificateOptions = ClientCertificateOption.Manual, - ServerCertificateCustomValidationCallback = + HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + HttpHandler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => { return true; - } - }; + }; + } - HttpClient = new HttpClient(handler) + HttpClient = new HttpClient(HttpHandler) { BaseAddress = baseAddress }; + + return this; } /// @@ -179,6 +163,40 @@ public IHttpTesterClient WithBearerAuthentication(string accessToken) return this; } + /// + /// Use NTLM authentication + /// + /// + /// + /// + public IHttpTesterClient WithNTLMAuthentication(string userName, string password) + { + if (string.IsNullOrEmpty(userName)) + throw new ArgumentNullException($"{nameof(userName)} is null."); + if (string.IsNullOrEmpty(password)) + throw new ArgumentNullException($"{nameof(password)} is null."); + + var credentials = new NetworkCredential(userName, password); + + var credentialsCache = new CredentialCache { { HttpClient.BaseAddress, "NTLM", credentials } }; + HttpHandler.Credentials = credentialsCache; + + return this; + } + + /// + /// Use NTLM authentication which represents the authentication credentials for the current security context in which the application is running. + /// + /// + public IHttpTesterClient WithNTLMAuthentication() + { + var credentials = CredentialCache.DefaultNetworkCredentials; + var credentialsCache = new CredentialCache { { HttpClient.BaseAddress, "NTLM", credentials } }; + HttpHandler.Credentials = credentialsCache; + + return this; + } + /// /// Start the HTTP request /// @@ -253,6 +271,7 @@ public void Dispose() protected virtual void Dispose(bool disposing) { HttpClient?.Dispose(); + HttpHandler?.Dispose(); } } } diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs index aafc59d..04df98a 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs @@ -62,6 +62,18 @@ public interface IHttpTesterClient /// IHttpTesterClient WithBearerAuthentication(string accessToken); /// + /// Use NTLM authentication + /// + /// + /// + /// + IHttpTesterClient WithNTLMAuthentication(string userName, string password); + /// + /// Use NTLM authentication which represents the authentication credentials for the current security context in which the application is running. + /// + /// + IHttpTesterClient WithNTLMAuthentication(); + /// /// Start the HTTP request /// /// From 8e60581cacd4ba2be9bae597010d93e4793c17d1 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Mon, 7 Dec 2020 19:36:13 +0100 Subject: [PATCH 2/5] Upload files with multipart content type * Upload files with multipart content type --- Directory.Build.props | 2 +- README.md | 25 ++++ .../HttpTesterClientTests.cs | 114 +++++++++++++++++- .../HttpResponseMessageExtensions.cs | 20 +++ .../HttpTesterClient.cs | 61 ++++++++++ .../Interfaces/IHttpTesterClient.cs | 15 +++ 6 files changed, 235 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 964bde6..e4f3b4c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.5 + 0.2.6 \ No newline at end of file diff --git a/README.md b/README.md index 37260c6..437014e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,31 @@ using (var client = new HttpTesterClient()) } ``` +**POST File Upload request** +You can upload files with `multipart/form-data` content type like shown below. +There are 2 overloads of `WithMultipart`, one for uploading binary data and the other for string data. + +```csharp +using (var client = new HttpTesterClient()) +{ + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "1" } }) + .WithMethod(HttpMethod.Post) + .WithMultipart(image, "FileContent", "logo.png") + .WithMultipart("MetaData", "My metadata.") + .WithPath("/api/bicycles/1/images") + .Start(); + + var msg = await response.GetResponseBodyString(); + + Assert.Equal("File name: miha.txt, length: 119305", msg); + Assert.True(response.IsSuccessStatusCode); +} +``` + +There is double content-type safety built-in and you can not do `WithMultipart` and `WithJsonBody` in the same request. + #### HttpTesterClient Authentication Currently `HttpTesterClient` supports: diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index ec33461..e92db6b 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -431,7 +431,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMAuthorization_ .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net/api/bicycles?api-version=1")) .WithJsonBody(BicycleFixture.GetCfr()) .WithMethod(HttpMethod.Post) - .WithNTLMAuthentication("user","pass") + .WithNTLMAuthentication("user", "pass") .Start(); var msg = await response.GetResponseBody(); @@ -441,5 +441,117 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMAuthorization_ Assert.Equal("Giant", msg.brand.ToString()); } } + + [Fact] + public async Task HttpTesterClientFileUpload_Success() + { + using (var client = new HttpTesterClient()) + { + byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .WithMethod(HttpMethod.Post) + .WithPath("/api/bicycles/1/images") + .WithMultipart(image, "FileContent", "logo.png") + .WithMultipart("FileName", "miha.txt") + .Start(); + + var msg = await response.GetResponseBodyString(); + + Assert.Equal("File name: miha.txt, length: 119305", msg); + Assert.True(response.IsSuccessStatusCode); + } + } + + [Fact] + public async Task HttpTesterClientFileUpload2_Success() + { + using (var client = new HttpTesterClient()) + { + byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .WithMethod(HttpMethod.Post) + .WithPath("/api/bicycles/1/images") + .WithMultipart(image, "FileContent", "logo.png") + .WithMultipart("FileName", "miha.txt") + .Start(); + + var msg = await response.GetResponseBodyString(); + + Assert.Equal("File name: miha.txt, length: 119305", msg); + Assert.True(response.IsSuccessStatusCode); + } + } + + [Fact] + public async Task HttpTesterClientBrochureUpload_Success() + { + using (var client = new HttpTesterClient()) + { + byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .WithMethod(HttpMethod.Post) + .WithPath("/api/bicycles/1/brochures") + .WithMultipart(image, "Image.FileContent", "logo.png") + .WithMultipart("Image.FileName", "miha.txt") + .WithMultipart("Metadata.Year", "2000") + .WithMultipart("Metadata.Name", "Brochure 2000") + .Start(); + + var msg = await response.GetResponseBodyString(); + + Assert.Equal("File name: Brochure 2000, image name: miha.txt, length: 119305", msg); + Assert.True(response.IsSuccessStatusCode); + } + } + + [Fact] + public void HttpTesterClientBrochureUploadBodyPresent_Fails() + { + using (var client = new HttpTesterClient()) + { + byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + + var response = client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .WithMethod(HttpMethod.Post) + .WithPath("/api/bicycles/1/brochures") + .WithJsonBody("1234"); + + var exception = Assert.Throws(() => client.WithMultipart(image, "Image.FileContent", "logo.png")); + Assert.StartsWith("Body application/json already defined on", exception.Message); + } + } + + [Fact] + public void HttpTesterClientBrochureUploadMultipartPresent_Fails() + { + using (var client = new HttpTesterClient()) + { + byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + + var response = client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .WithMethod(HttpMethod.Post) + .WithPath("/api/bicycles/1/brochures") + .WithMultipart(image, "Image.FileContent", "logo.png") + .WithMultipart("Image.FileName", "miha.txt") + .WithMultipart("Metadata.Year", "2000") + .WithMultipart("Metadata.Name", "Brochure 2000"); + + var exception = Assert.Throws(() => client.WithJsonBody("1234")); + Assert.StartsWith("Body multipart/form-data already defined", exception.Message); + } + } } } diff --git a/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs b/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs index 24569a8..f82c3d4 100644 --- a/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs +++ b/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs @@ -20,5 +20,25 @@ public static async Task GetResponseBody(this HttpResponseMessage httpResp var bodyResponse = await httpResponseMessage.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(bodyResponse); } + + /// + /// Get response as string + /// + /// + /// + public static async Task GetResponseBodyString(this HttpResponseMessage httpResponseMessage) + { + return await httpResponseMessage.Content.ReadAsStringAsync(); + } + + /// + /// Get response as byte array + /// + /// + /// + public static async Task GetResponseBodyBytes(this HttpResponseMessage httpResponseMessage) + { + return await httpResponseMessage.Content.ReadAsByteArrayAsync(); + } } } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index f7f3d23..4cb6413 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -28,6 +29,7 @@ public class HttpTesterClient : IHttpTesterClient, IDisposable private HttpMethod _httpMethod; private Dictionary _queryParameters = null; private HttpResponseMessage _responseMessage = null; + private MultipartFormDataContent _multipartFormDataContent = null; /// /// Measured HTTP request duration @@ -95,6 +97,9 @@ public IHttpTesterClient WithHeaders(Dictionary headers) /// public IHttpTesterClient WithJsonBody(T bodyObject) { + if (_multipartFormDataContent != null) + throw new QAToolKitEngineHttpTesterException("Body multipart/form-data already defined on the HTTP client. Can not add application/json content type."); + if (bodyObject == null) { return this; @@ -197,6 +202,57 @@ public IHttpTesterClient WithNTLMAuthentication() return this; } + /// + /// Create a multipart form data content and add a file content + /// + /// + /// + /// + /// + public IHttpTesterClient WithMultipart(byte[] fileByteArray, string httpContentName, string fileName) + { + if (!string.IsNullOrEmpty(_body)) + throw new QAToolKitEngineHttpTesterException("Body application/json already defined on the HTTP client. Can not add multipart/form-data content type."); + if (fileByteArray == null) + throw new ArgumentNullException($"{nameof(fileByteArray)} is null."); + if (string.IsNullOrEmpty(httpContentName)) + throw new ArgumentNullException($"{nameof(httpContentName)} is null."); + + if (_multipartFormDataContent == null) + { + _multipartFormDataContent = new MultipartFormDataContent(); + } + + _multipartFormDataContent.Add(new ByteArrayContent(fileByteArray), httpContentName, fileName); + + return this; + } + + /// + /// Add a string to a multipart form data + /// + /// + /// + /// + public IHttpTesterClient WithMultipart(string httpContentName, string value) + { + if (!string.IsNullOrEmpty(_body)) + throw new QAToolKitEngineHttpTesterException("Body application/json already defined on the HTTP client. Can not add multipart/form-data content type."); + if (string.IsNullOrEmpty(httpContentName)) + throw new ArgumentNullException($"{nameof(httpContentName)} is null."); + if (value == null) + throw new ArgumentNullException($"{nameof(value)} is null."); + + if (_multipartFormDataContent == null) + { + _multipartFormDataContent = new MultipartFormDataContent(); + } + + _multipartFormDataContent.Add(new StringContent(value), httpContentName); + + return this; + } + /// /// Start the HTTP request /// @@ -246,6 +302,11 @@ public async Task Start() requestMessage.Content = new StringContent(_body, Encoding.UTF8, "application/json"); } + if (_multipartFormDataContent != null) + { + requestMessage.Content = _multipartFormDataContent; + } + _responseMessage = await HttpClient.SendAsync(requestMessage); } sw.Stop(); diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs index 04df98a..af25107 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs @@ -74,6 +74,21 @@ public interface IHttpTesterClient /// IHttpTesterClient WithNTLMAuthentication(); /// + /// Upload a file + /// + /// + /// + /// + /// + IHttpTesterClient WithMultipart(byte[] fileByteArray, string httpContentName, string fileName); + /// + /// Upload a file + /// + /// + /// + /// + IHttpTesterClient WithMultipart(string httpContentName, string value); + /// /// Start the HTTP request /// /// From f0f9f5e480827943aa0f2f8bd61e3a9b2f50633e Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Sat, 12 Dec 2020 10:52:50 +0100 Subject: [PATCH 3/5] CreateHttpRequest override that accepts HttpRequest object --- Directory.Build.props | 2 +- README.md | 47 +++++++++++- .../HttpTesterClientTests.cs | 74 +++++++++++++++++-- .../HttpTesterClient.cs | 71 ++++++++++++++++++ .../Interfaces/IHttpTesterClient.cs | 10 ++- 5 files changed, 195 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e4f3b4c..e217958 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.6 + 0.2.8 \ No newline at end of file diff --git a/README.md b/README.md index 437014e..047bbb7 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,17 @@ [![CodeQL](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/CodeQL%20Analyze/badge.svg)](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/security/code-scanning) [![Sonarcloud Quality gate](https://github.com/qatoolkit/qatoolkit-engine-httptester-net/workflows/Sonarqube%20Analyze/badge.svg)](https://sonarcloud.io/dashboard?id=qatoolkit_qatoolkit-engine-httptester-net) [![NuGet package](https://img.shields.io/nuget/v/QAToolKit.Engine.HttpTester?label=QAToolKit.Engine.HttpTester)](https://www.nuget.org/packages/QAToolKit.Engine.HttpTester/) +[![Discord](https://img.shields.io/discord/787220825127780354?color=%23267CB9&label=Discord%20chat)](https://discord.com/invite/tu3WDV5Z?utm_source=Discord%20Widget&utm_medium=Connect) ## Description `QAToolKit.Engine.HttpTester` is a .NET Standard 2.1 library, that that contains an implementation of `IHttpTesterClient` that is a thin wrapper around .NET `HttpClient` to allow to write easy Http Request calls. Supported .NET frameworks and standards: `netstandard2.0`, `netstandard2.1`, `netcoreapp3.1`, `net5.0` +Get in touch with me on: + +[![Discord](https://img.shields.io/discord/787220825127780354?color=%23267CB9&label=Discord%20chat)](https://discord.com/invite/tu3WDV5Z?utm_source=Discord%20Widget&utm_medium=Connect) + ### HttpTesterClient A sample on how to easily call the HTTP request with .NET `HttpClient`: @@ -70,6 +75,7 @@ using (var client = new HttpTesterClient()) ``` **POST File Upload request** + You can upload files with `multipart/form-data` content type like shown below. There are 2 overloads of `WithMultipart`, one for uploading binary data and the other for string data. @@ -92,7 +98,44 @@ using (var client = new HttpTesterClient()) } ``` -There is double content-type safety built-in and you can not do `WithMultipart` and `WithJsonBody` in the same request. +There is content-type safety built-in and you can not do `WithMultipart` and `WithJsonBody` in the same request. + +**Create Tester client from QAToolKit Swagger request** + +If you are using QAToolKit Swagger library to generate `HttpRequest` object you can use a `CreateHttpRequest` override. + +In this sample below, `CreateHttpRequest` accepts a first request from the list of requests generated by Swagger library. + +```csharp +//GET Requests from Swagger file +var urlSource = new SwaggerUrlSource(); + +var requests = await urlSource.Load(new Uri[] { + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v1/swagger.json") +}); + +using (var client = new HttpTesterClient()) +{ + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .Start(); + .... +} +``` + +The `CreateHttpRequest` override will assign only `BaseUrl`, `Path`, `HttpMethod`, `Query parameters` and `Header parameters`. You need to assign `HttpBody` manually with `WithJsonBody`. +For example: + +```csharp +using (var client = new HttpTesterClient()) +{ + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .WithJsonBody(object) + .Start(); + .... +} +``` #### HttpTesterClient Authentication @@ -124,7 +167,7 @@ Currently `HttpTesterClient` supports: var response = await client .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) .... - .WithNTKMAuthentication("user", "pass") // or default security context .WithNTKMAuthentication() + .WithNTLMAuthentication("user", "pass") // or default security context .WithNTLMAuthentication() .Start(); ``` diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index e92db6b..cae6429 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -333,11 +333,10 @@ public async Task HttpTesterClientPostStringBodyWithFulUrl_Success() .WithMethod(HttpMethod.Post) .Start(); - var msg = await response.Content.ReadAsStringAsync(); + var msg = await response.GetResponseBodyString(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); - //Assert.Equal("Giant", msg.brand.ToString()); } } @@ -460,7 +459,7 @@ public async Task HttpTesterClientFileUpload_Success() var msg = await response.GetResponseBodyString(); - Assert.Equal("File name: miha.txt, length: 119305", msg); + Assert.Equal("\"File name: miha.txt, length: 119305\"", msg); Assert.True(response.IsSuccessStatusCode); } } @@ -483,7 +482,7 @@ public async Task HttpTesterClientFileUpload2_Success() var msg = await response.GetResponseBodyString(); - Assert.Equal("File name: miha.txt, length: 119305", msg); + Assert.Equal("\"File name: miha.txt, length: 119305\"", msg); Assert.True(response.IsSuccessStatusCode); } } @@ -508,7 +507,7 @@ public async Task HttpTesterClientBrochureUpload_Success() var msg = await response.GetResponseBodyString(); - Assert.Equal("File name: Brochure 2000, image name: miha.txt, length: 119305", msg); + Assert.Equal("\"File name: Brochure 2000, image name: miha.txt, length: 119305\"", msg); Assert.True(response.IsSuccessStatusCode); } } @@ -553,5 +552,70 @@ public void HttpTesterClientBrochureUploadMultipartPresent_Fails() Assert.StartsWith("Body multipart/form-data already defined", exception.Message); } } + + [Fact] + public async Task HttpTesterClientAddPostHttpRequest_Success() + { + var urlSource = new SwaggerUrlSource(options => + { + options.AddBaseUrl(new Uri("https://qatoolkitapi.azurewebsites.net/")); + options.AddRequestFilters(new RequestFilter() + { + EndpointNameWhitelist = new string[] { "NewBike" } + }); + options.UseSwaggerExampleValues = true; + }); + + var requests = await urlSource.Load(new Uri[] { + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v1/swagger.json") + }); + + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .WithJsonBody(BicycleFixture.Get()) + .Start(); + + var msg = await response.GetResponseBody(); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Giant", msg.Brand); + } + } + + [Fact] + public async Task HttpTesterClientAddGetHttpRequest_Success() + { + var urlSource = new SwaggerUrlSource(options => + { + options.AddBaseUrl(new Uri("https://qatoolkitapi.azurewebsites.net/")); + options.AddRequestFilters(new RequestFilter() + { + EndpointNameWhitelist = new string[] { "GetAllBikes" } + }); + options.UseSwaggerExampleValues = true; + }); + + var requests = await urlSource.Load(new Uri[] { + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v1/swagger.json") + }); + + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .Start(); + + var msg = await response.GetResponseBody>(); + + var expecterResponse = BicycleFixture.GetBicycles().ToExpectedObject(); + expecterResponse.ShouldEqual(msg); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + } + } } } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index 4cb6413..0c0b729 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -1,10 +1,12 @@ using Newtonsoft.Json; +using QAToolKit.Core.Models; using QAToolKit.Engine.HttpTester.Exceptions; using QAToolKit.Engine.HttpTester.Interfaces; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -253,6 +255,75 @@ public IHttpTesterClient WithMultipart(string httpContentName, string value) return this; } + /// + /// Create a HTTP tester client from QAToolKit HttpRequest object + /// + /// Create tester client with BaseUrl, Path, HttpMethod, Headers and URL Query paramteres read from HttpRequest object. Specify other values and parameters manually. + /// + /// + public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validateCertificate = true) + { + if (httpRequest == null) + { + throw new QAToolKitEngineHttpTesterException("'HttpRequest' is null. Pass in the valid object."); + } + + if (HttpClient != null) + { + throw new QAToolKitEngineHttpTesterException("HttpClient is already instantiated. Create new 'HttpTesterClient'."); + } + + var baseAddress = new Uri(httpRequest.BasePath); + + HttpHandler = new HttpClientHandler(); + + if (!validateCertificate && + (baseAddress.Scheme == Uri.UriSchemeHttp || baseAddress.Scheme == Uri.UriSchemeHttps)) + { + HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + HttpHandler.ServerCertificateCustomValidationCallback = + (httpRequestMessage, cert, cetChain, policyErrors) => + { + return true; + }; + } + + HttpClient = new HttpClient(HttpHandler) + { + BaseAddress = baseAddress + }; + + + if (string.IsNullOrEmpty(httpRequest.Path)) + { + throw new QAToolKitEngineHttpTesterException("HttpRequest Path is required."); + } + + _path = httpRequest.Path; + + _httpMethod = httpRequest.Method; + + //Query parameters + if (_queryParameters == null) + _queryParameters = new Dictionary(); + + foreach (var parameter in httpRequest.Parameters.Where(t => t.Location == Location.Query && t.Value != null)) + { + _queryParameters.Add(parameter.Name, parameter.Value); + } + + //Headers + if (_headers == null) + _headers = new Dictionary(); + + foreach (var header in httpRequest.Parameters.Where(t => t.Location == Location.Header && t.Value != null)) + { + _headers.Add(header.Name, header.Value); + } + + return this; + } + /// /// Start the HTTP request /// diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs index af25107..f59a0d3 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs @@ -1,4 +1,5 @@ -using System; +using QAToolKit.Core.Models; +using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -18,6 +19,13 @@ public interface IHttpTesterClient /// IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertificate = true); /// + /// Create a HTTP request client from QAToolKit HttpRequest object + /// + /// + /// + /// + IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validateCertificate = true); + /// /// Add URL path to the HTTP client /// /// From f05c3cd911e681181ac76b36088f6cf6a67e12ef Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Mon, 14 Dec 2020 15:17:48 +0100 Subject: [PATCH 4/5] Asserter has new methods ResponseStatusCodeIsSuccess and ResponseBodyIsEmpty, small updates --- Directory.Build.props | 2 +- README.md | 15 ++++++- .../HttpTestAsserterTests.cs | 32 ++++++++++++++- .../HttpTesterClientTests.cs | 4 +- .../QAToolKit.Engine.HttpTester.Test.csproj | 4 +- .../HttpTestAsserter.cs | 39 ++++++++++++++++++- .../Interfaces/IHttpTestAsserter.cs | 10 +++++ .../QAToolKit.Engine.HttpTester.csproj | 2 +- 8 files changed, 97 insertions(+), 11 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e217958..abb452e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.8 + 0.2.9 \ No newline at end of file diff --git a/README.md b/README.md index 047bbb7..c62e509 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,15 @@ Currently `HttpTesterClient` supports: ### HttpTestAsserter -This is an implementation of the HTTP response message asserter, which can be used to assert different paramters. +This is an implementation of the HTTP response message asserter, which can be used to assert different parameters. + +Here is a list of Asserters: +- `ResponseContentContains`: HTTP body contains a string (ignores case) +- `RequestDurationEquals`: Verify request duration +- `ResponseStatusCodeEquals`: Verify if response code equals +- `ResponseHasHttpHeader`: HTTP response contains a header +- `ResponseStatusCodeIsSuccess`: HTTP response status code is one of 2xx +- `ResponseBodyIsEmpty`: HTTP response body is empty Asserter produces a list of `AssertResult`: @@ -220,6 +228,7 @@ using (var client = new HttpTesterClient()) .ResponseContentContains("scott") .RequestDurationEquals(duration, (x) => x < 1000) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseStatusCodeIsSuccess() .AssertAll(); //if you use xUnit, you can assert the results like this @@ -232,7 +241,9 @@ using (var client = new HttpTesterClient()) ## To-do -- **This library is an early alpha version** +- **This library is a beta version** +- Add more Http Asserters +- Cover more test cases with HTTP Tester client ## License diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index e570a8a..b52d28b 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -33,6 +33,7 @@ public async Task HttpTestAsserterSimple_Success() .ResponseContentContains("scott") .RequestDurationEquals(duration, (x) => x < 1000) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseHasHttpHeader("Date") .AssertAll(); foreach (var result in assertResults) @@ -168,9 +169,38 @@ public async Task HttpTestAsserterAlternativeDurationPredicate_Success() .ResponseContentContains("id") .RequestDurationEquals(duration, (x) => (x > 100 && x < 1000)) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseStatusCodeIsSuccess() .AssertAll(); - Assert.Equal(4, assertResults.ToList().Count); + Assert.Equal(5, assertResults.ToList().Count); + foreach (var result in assertResults) + { + Assert.True(result.IsTrue, result.Message); + } + } + } + + [Fact] + public async Task HttpTestAsserterDeleteIsBodyEmpty_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "1" } }) + .WithMethod(HttpMethod.Delete) + .WithPath("/api/bicycles/1") + .Start(); + + var asserter = new HttpTestAsserter(response); + var duration = client.Duration; + var assertResults = asserter + .ResponseBodyIsEmpty() + .RequestDurationEquals(duration, (x) => (x > 100 && x < 1000)) + .ResponseStatusCodeIsSuccess() + .AssertAll(); + + Assert.Equal(3, assertResults.ToList().Count); foreach (var result in assertResults) { Assert.True(result.IsTrue, result.Message); diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index cae6429..c53b0ad 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -567,7 +567,7 @@ public async Task HttpTesterClientAddPostHttpRequest_Success() }); var requests = await urlSource.Load(new Uri[] { - new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v1/swagger.json") + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v2/swagger.json") }); using (var client = new HttpTesterClient()) @@ -599,7 +599,7 @@ public async Task HttpTesterClientAddGetHttpRequest_Success() }); var requests = await urlSource.Load(new Uri[] { - new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v1/swagger.json") + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v2/swagger.json") }); using (var client = new HttpTesterClient()) diff --git a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj index be051ab..e984667 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj +++ b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index ff7ae62..21dae33 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -3,6 +3,7 @@ using QAToolKit.Engine.HttpTester.Models; using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; @@ -95,8 +96,8 @@ public IHttpTestAsserter ResponseHasHttpHeader(string headerName) { Name = nameof(ResponseHasHttpHeader), Message = $"Contains header '{headerName}'.", - IsTrue = _httpResponseMessage.Headers.Contains(headerName) - }); + IsTrue = _httpResponseMessage.Headers.TryGetValues(headerName, out var values) + }); return this; } @@ -117,5 +118,39 @@ public IHttpTestAsserter ResponseStatusCodeEquals(HttpStatusCode httpStatusCode) return this; } + + /// + /// HTTP response status code is one of 2xx + /// + /// + public IHttpTestAsserter ResponseStatusCodeIsSuccess() + { + _assertResults.Add(new AssertResult() + { + Name = nameof(ResponseStatusCodeIsSuccess), + Message = $"Expected status code is '2xx' return code is '{_httpResponseMessage.StatusCode}'.", + IsTrue = _httpResponseMessage.IsSuccessStatusCode + }); + + return this; + } + + /// + /// HTTP response body is empty + /// + /// + public IHttpTestAsserter ResponseBodyIsEmpty() + { + var bodyString = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + + _assertResults.Add(new AssertResult() + { + Name = nameof(ResponseBodyIsEmpty), + Message = $"Expected empty body, returned body is '{bodyString}'.", + IsTrue = string.IsNullOrEmpty(bodyString) + }); + + return this; + } } } diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs index d6d2ad6..0f795d0 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs @@ -37,6 +37,16 @@ public interface IHttpTestAsserter /// IHttpTestAsserter ResponseHasHttpHeader(string headerName); /// + /// HTTP response status code is one of 2xx + /// + /// + IHttpTestAsserter ResponseStatusCodeIsSuccess(); + /// + /// HTTP response body is empty + /// + /// + IHttpTestAsserter ResponseBodyIsEmpty(); + /// /// Return all Assert messages of the Asserter /// /// diff --git a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj index 34a86f0..5fddda3 100644 --- a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj +++ b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj @@ -35,6 +35,6 @@ - + From 4f55262d7219141aa8a56601fe63092f3a6667c8 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Mon, 14 Dec 2020 22:18:52 +0100 Subject: [PATCH 5/5] HttpTester client now has WithPathReplacementValues, bug fix with query parameters * HttpTester client now has WithPathReplacementValues, bug fix with query parameters * small update --- Directory.Build.props | 2 +- .../Fixtures/BicycleFixture.cs | 25 +++ .../HttpTestAsserterTests.cs | 4 +- .../HttpTesterClientTests.cs | 71 ++++++- .../HttpTestAsserter.cs | 3 +- .../HttpTesterClient.cs | 195 ++++++++++-------- .../Interfaces/IHttpTesterClient.cs | 6 + 7 files changed, 217 insertions(+), 89 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index abb452e..eee5155 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.9 + 0.2.11 \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester.Test/Fixtures/BicycleFixture.cs b/src/QAToolKit.Engine.HttpTester.Test/Fixtures/BicycleFixture.cs index 671dccf..a044426 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/Fixtures/BicycleFixture.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/Fixtures/BicycleFixture.cs @@ -15,6 +15,31 @@ public static Bicycle Get() }; } + public static Bicycle GetCannondale() + { + return new Bicycle + { + Id = 2, + Name = "CAADX", + Brand = "Cannondale", + Type = BicycleType.Gravel + }; + } + + public static List GetCannondaleArray() + { + return new List() + { + new Bicycle() + { + Id = 2, + Name = "CAADX", + Brand = "Cannondale", + Type = BicycleType.Gravel + } + }; + } + public static Bicycle GetFoil() { return new Bicycle diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index b52d28b..9b08823 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -31,7 +31,7 @@ public async Task HttpTestAsserterSimple_Success() var duration = client.Duration; var assertResults = asserter .ResponseContentContains("scott") - .RequestDurationEquals(duration, (x) => x < 1000) + .RequestDurationEquals(duration, (x) => x < 2000) .ResponseStatusCodeEquals(HttpStatusCode.OK) .ResponseHasHttpHeader("Date") .AssertAll(); @@ -116,7 +116,7 @@ public async Task HttpTestAsserterHeaderMissing_Fails() var duration = client.Duration; Assert.Throws(() => asserter .ResponseContentContains("scott") - .RequestDurationEquals(duration, (x) => x < 1000) + .RequestDurationEquals(duration, (x) => x < 2000) .ResponseStatusCodeEquals(HttpStatusCode.OK) .ResponseHasHttpHeader(null) .AssertAll()); diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index c53b0ad..0b59a8f 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -548,7 +548,7 @@ public void HttpTesterClientBrochureUploadMultipartPresent_Fails() .WithMultipart("Metadata.Year", "2000") .WithMultipart("Metadata.Name", "Brochure 2000"); - var exception = Assert.Throws(() => client.WithJsonBody("1234")); + var exception = Assert.Throws(() => client.WithJsonBody("1234")); Assert.StartsWith("Body multipart/form-data already defined", exception.Message); } } @@ -617,5 +617,74 @@ public async Task HttpTesterClientAddGetHttpRequest_Success() Assert.True(response.IsSuccessStatusCode); } } + + [Fact] + public async Task HttpTesterClientGetBikeByIdRequest_Success() + { + var urlSource = new SwaggerUrlSource(options => + { + options.AddBaseUrl(new Uri("https://qatoolkitapi.azurewebsites.net/")); + options.AddRequestFilters(new RequestFilter() + { + EndpointNameWhitelist = new string[] { "GetBike" } + }); + options.UseSwaggerExampleValues = true; + }); + + var requests = await urlSource.Load(new Uri[] { + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v2/swagger.json") + }); + + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .WithPathReplacementValues(new Dictionary() { { "id", "2" } }) + .WithQueryParams(new Dictionary() { { "api-version", "2" } }) + .Start(); + + var msg = await response.GetResponseBody(); + + var expecterResponse = BicycleFixture.GetCannondale().ToExpectedObject(); + expecterResponse.ShouldEqual(msg); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + } + } + + [Fact] + public async Task HttpTesterClientGetBikesByTypeHttpRequest_Success() + { + var urlSource = new SwaggerUrlSource(options => + { + options.AddBaseUrl(new Uri("https://qatoolkitapi.azurewebsites.net/")); + options.AddRequestFilters(new RequestFilter() + { + EndpointNameWhitelist = new string[] { "GetAllBikes" } + }); + options.UseSwaggerExampleValues = true; + }); + + var requests = await urlSource.Load(new Uri[] { + new Uri("https://qatoolkitapi.azurewebsites.net/swagger/v2/swagger.json") + }); + + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(requests.FirstOrDefault()) + .WithQueryParams(new Dictionary() { { "api-version", "2" }, {"bicycleType", "1" } }) + .Start(); + + var msg = await response.GetResponseBody>(); + + var expecterResponse = BicycleFixture.GetCannondaleArray().ToExpectedObject(); + expecterResponse.ShouldEqual(msg); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + } + } } } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index 21dae33..6564097 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -3,7 +3,6 @@ using QAToolKit.Engine.HttpTester.Models; using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; @@ -97,7 +96,7 @@ public IHttpTestAsserter ResponseHasHttpHeader(string headerName) Name = nameof(ResponseHasHttpHeader), Message = $"Contains header '{headerName}'.", IsTrue = _httpResponseMessage.Headers.TryGetValues(headerName, out var values) - }); + }); return this; } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index 0c0b729..b2f305d 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -67,6 +66,69 @@ public IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertifi return this; } + /// + /// Create a HTTP tester client from QAToolKit HttpRequest object + /// + /// Create tester client with BaseUrl, Path, HttpMethod, Headers and URL Query paramteres read from HttpRequest object. Specify other values and parameters manually. + /// + /// + public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validateCertificate = true) + { + if (httpRequest == null) + throw new QAToolKitEngineHttpTesterException("'HttpRequest' is null. Pass in the valid object."); + if (HttpClient != null) + throw new QAToolKitEngineHttpTesterException("HttpClient is already instantiated. Create new 'HttpTesterClient'."); + + var baseAddress = new Uri(httpRequest.BasePath); + + HttpHandler = new HttpClientHandler(); + + if (!validateCertificate && + (baseAddress.Scheme == Uri.UriSchemeHttp || baseAddress.Scheme == Uri.UriSchemeHttps)) + { + HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + HttpHandler.ServerCertificateCustomValidationCallback = + (httpRequestMessage, cert, cetChain, policyErrors) => + { + return true; + }; + } + + HttpClient = new HttpClient(HttpHandler) + { + BaseAddress = baseAddress + }; + + if (string.IsNullOrEmpty(httpRequest.Path)) + { + throw new QAToolKitEngineHttpTesterException("HttpRequest Path is required."); + } + + _path = httpRequest.Path; + + _httpMethod = httpRequest.Method; + + //Query parameters + if (_queryParameters == null) + _queryParameters = new Dictionary(); + + foreach (var parameter in httpRequest.Parameters.Where(t => t.Location == Location.Query && t.Value != null)) + { + _queryParameters.Add(parameter.Name, parameter.Value); + } + + //Headers + if (_headers == null) + _headers = new Dictionary(); + + foreach (var header in httpRequest.Parameters.Where(t => t.Location == Location.Header && t.Value != null)) + { + _headers.Add(header.Name, header.Value); + } + + return this; + } + /// /// Add URL path to the HTTP client /// @@ -74,11 +136,35 @@ public IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertifi /// public IHttpTesterClient WithPath(string urlPath) { + if (urlPath == null) + throw new ArgumentException($"{nameof(urlPath)} is null."); + _path = urlPath; return this; } + /// + /// Replace URL path with path parametrs from passed dictionary + /// + /// + /// + public IHttpTesterClient WithPathReplacementValues(Dictionary pathParameters) + { + if (pathParameters == null) + throw new ArgumentException($"{nameof(pathParameters)} is null."); + + if (string.IsNullOrEmpty(_path)) + throw new QAToolKitEngineHttpTesterException("Uri Path is empty. Use 'WithPath' before calling 'WithPathReplacementValues'."); + + foreach (var parameter in pathParameters) + { + _path = _path.Replace($"{{{parameter.Key}}}", parameter.Value); + } + + return this; + } + /// /// Add HTTP headers to the HTTP client /// @@ -86,6 +172,9 @@ public IHttpTesterClient WithPath(string urlPath) /// public IHttpTesterClient WithHeaders(Dictionary headers) { + if (headers == null) + throw new ArgumentException($"{nameof(headers)} is null."); + _headers = headers; return this; @@ -126,6 +215,9 @@ public IHttpTesterClient WithJsonBody(T bodyObject) /// public IHttpTesterClient WithMethod(HttpMethod httpMethod) { + if (httpMethod == null) + throw new ArgumentException($"{nameof(httpMethod)} is null."); + _httpMethod = httpMethod; return this; @@ -138,6 +230,9 @@ public IHttpTesterClient WithMethod(HttpMethod httpMethod) /// public IHttpTesterClient WithQueryParams(Dictionary queryParameters) { + if (queryParameters == null) + throw new ArgumentException($"{nameof(queryParameters)} is null."); + _queryParameters = queryParameters; return this; @@ -151,6 +246,11 @@ public IHttpTesterClient WithQueryParams(Dictionary queryParamet /// public IHttpTesterClient WithBasicAuthentication(string userName, string password) { + if (userName == null) + throw new ArgumentException($"{nameof(userName)} is null."); + if (password == null) + throw new ArgumentException($"{nameof(password)} is null."); + var authenticationString = $"{userName}:{password}"; var base64EncodedAuthenticationString = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(authenticationString)); HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); @@ -165,6 +265,9 @@ public IHttpTesterClient WithBasicAuthentication(string userName, string passwor /// public IHttpTesterClient WithBearerAuthentication(string accessToken) { + if (accessToken == null) + throw new ArgumentException($"{nameof(accessToken)} is null."); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); return this; @@ -255,75 +358,6 @@ public IHttpTesterClient WithMultipart(string httpContentName, string value) return this; } - /// - /// Create a HTTP tester client from QAToolKit HttpRequest object - /// - /// Create tester client with BaseUrl, Path, HttpMethod, Headers and URL Query paramteres read from HttpRequest object. Specify other values and parameters manually. - /// - /// - public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validateCertificate = true) - { - if (httpRequest == null) - { - throw new QAToolKitEngineHttpTesterException("'HttpRequest' is null. Pass in the valid object."); - } - - if (HttpClient != null) - { - throw new QAToolKitEngineHttpTesterException("HttpClient is already instantiated. Create new 'HttpTesterClient'."); - } - - var baseAddress = new Uri(httpRequest.BasePath); - - HttpHandler = new HttpClientHandler(); - - if (!validateCertificate && - (baseAddress.Scheme == Uri.UriSchemeHttp || baseAddress.Scheme == Uri.UriSchemeHttps)) - { - HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; - HttpHandler.ServerCertificateCustomValidationCallback = - (httpRequestMessage, cert, cetChain, policyErrors) => - { - return true; - }; - } - - HttpClient = new HttpClient(HttpHandler) - { - BaseAddress = baseAddress - }; - - - if (string.IsNullOrEmpty(httpRequest.Path)) - { - throw new QAToolKitEngineHttpTesterException("HttpRequest Path is required."); - } - - _path = httpRequest.Path; - - _httpMethod = httpRequest.Method; - - //Query parameters - if (_queryParameters == null) - _queryParameters = new Dictionary(); - - foreach (var parameter in httpRequest.Parameters.Where(t => t.Location == Location.Query && t.Value != null)) - { - _queryParameters.Add(parameter.Name, parameter.Value); - } - - //Headers - if (_headers == null) - _headers = new Dictionary(); - - foreach (var header in httpRequest.Parameters.Where(t => t.Location == Location.Header && t.Value != null)) - { - _headers.Add(header.Name, header.Value); - } - - return this; - } - /// /// Start the HTTP request /// @@ -331,34 +365,29 @@ public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validat public async Task Start() { if (HttpClient == null) - { throw new QAToolKitEngineHttpTesterException("HttpClient is null. Create an object first with 'CreateHttpRequest'."); - } - if (_httpMethod == null) - { throw new QAToolKitEngineHttpTesterException("Define method for a HTTP request."); - } - if (_httpMethod == HttpMethod.Get && _body != null) - { throw new QAToolKitEngineHttpTesterException("'Get' method can not have a HTTP body."); - } - string queryString = ""; + StringBuilder queryString = new StringBuilder(); if (_queryParameters != null) { - queryString = "?"; + queryString.Append("?"); + List array = new List(); foreach (var query in _queryParameters) { - queryString += $"{query.Key}={query.Value}"; + array.Add($"{query.Key}={query.Value}"); } + + queryString.Append(string.Join("&", array)); } var sw = new Stopwatch(); sw.Start(); - using (var requestMessage = new HttpRequestMessage(_httpMethod, _path + queryString)) + using (var requestMessage = new HttpRequestMessage(_httpMethod, _path + queryString.ToString())) { if (_headers != null) { @@ -406,4 +435,4 @@ protected virtual void Dispose(bool disposing) HttpHandler?.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs index f59a0d3..d892500 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs @@ -32,6 +32,12 @@ public interface IHttpTesterClient /// IHttpTesterClient WithPath(string urlPath); /// + /// Replace URL path with path parameters from passed dictionary + /// + /// + /// + IHttpTesterClient WithPathReplacementValues(Dictionary pathParameters); + /// /// Add HTTP method to the HTTP client /// ///