From 5718ec0bd1568bab6ae8489bb3ebaf2dd4b9eb51 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Thu, 19 Nov 2020 21:09:25 +0100 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 /// /// From b3ba2238b405fe4dbc5e91ef643e13e519ec23f1 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Wed, 30 Dec 2020 15:23:03 +0100 Subject: [PATCH 06/14] maintenance --- .github/workflows/codeql-analysis.yml | 37 +----------------------- .github/workflows/sonarqube-analysis.yml | 2 +- README.md | 4 +-- 3 files changed, 4 insertions(+), 39 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c5584a6..80734df 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,15 +1,3 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# ******** NOTE ******** - name: "CodeQL Analyze" on: @@ -26,9 +14,6 @@ jobs: fail-fast: false matrix: language: [ 'csharp' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: Checkout repository @@ -41,31 +26,11 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v1 \ No newline at end of file diff --git a/.github/workflows/sonarqube-analysis.yml b/.github/workflows/sonarqube-analysis.yml index a034b0d..da534ce 100644 --- a/.github/workflows/sonarqube-analysis.yml +++ b/.github/workflows/sonarqube-analysis.yml @@ -21,7 +21,7 @@ jobs: with: dotnet-version: '5.0.x' - name: SonarScanner for .NET Core with pull request decoration support - uses: highbyte/sonarscan-dotnet@2.0-beta + uses: highbyte/sonarscan-dotnet@2.0 with: sonarProjectKey: qatoolkit_qatoolkit-engine-httptester-net sonarProjectName: qatoolkit_qatoolkit-engine-httptester-net diff --git a/README.md b/README.md index c62e509..6733bb3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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) +[![Discord](https://img.shields.io/discord/787220825127780354?color=%23267CB9&label=Discord%20chat)](https://discord.gg/hYs6ayYQC5) ## 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. @@ -12,7 +12,7 @@ Supported .NET frameworks and standards: `netstandard2.0`, `netstandard2.1`, `ne 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) +[![Discord](https://img.shields.io/discord/787220825127780354?color=%23267CB9&label=Discord%20chat)](https://discord.gg/hYs6ayYQC5) ### HttpTesterClient From 3d5b02fe0abcd1a1f56a4da88e92f5f9ce5cf88e Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Sat, 2 Jan 2021 13:39:41 +0100 Subject: [PATCH 07/14] year update --- LICENSE | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 345345f..b4bc1c7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Miha Jakovac +Copyright (c) 2020-2021 Miha Jakovac Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6733bb3..8d093ba 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ using (var client = new HttpTesterClient()) MIT License -Copyright (c) 2020 Miha Jakovac +Copyright (c) 2020-2021 Miha Jakovac Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 9989309adac4dbc61ff494533e14580541f23f1d Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Mon, 22 Feb 2021 19:49:16 +0100 Subject: [PATCH 08/14] Http duration added, XML deserializer for HTTP response content --- .gitignore | 1 + Directory.Build.props | 2 +- README.md | 41 ++++++++-- .../Fixtures/XMLFixture.cs | 47 +++++++++++ .../HttpTestAsserterTests.cs | 5 +- .../HttpTesterClientTests.cs | 2 +- .../QAToolKit.Engine.HttpTester.Test.csproj | 6 +- .../XMLDeserializerTests.cs | 31 ++++++++ .../HttpResponseMessageExtensions.cs | 34 +++++++- .../HttpTesterClient.cs | 77 ++++++++----------- 10 files changed, 185 insertions(+), 61 deletions(-) create mode 100644 src/QAToolKit.Engine.HttpTester.Test/Fixtures/XMLFixture.cs create mode 100644 src/QAToolKit.Engine.HttpTester.Test/XMLDeserializerTests.cs diff --git a/.gitignore b/.gitignore index 8a03c82..151f47c 100644 --- a/.gitignore +++ b/.gitignore @@ -361,3 +361,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /src/QAToolKit.Engine.HttpTester.Test/global.json +/.idea/ diff --git a/Directory.Build.props b/Directory.Build.props index eee5155..9c576a9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.2.11 + 0.3.0 \ No newline at end of file diff --git a/README.md b/README.md index 8d093ba..852e9ae 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Get in touch with me on: A sample on how to easily call the HTTP request with .NET `HttpClient`: -**GET request** +##### GET request ```csharp using (var client = new HttpTesterClient()) { @@ -35,12 +35,13 @@ using (var client = new HttpTesterClient()) var expecterResponse = BicycleFixture.GetBicycles().ToExpectedObject(); expecterResponse.ShouldEqual(msg); - Assert.True(client.Duration < 2000); + Assert.True(client.Duration < 2000); //Start() method execution duration + Assert.True(client.HttpDuration < 2000); //HTTP request duration Assert.True(response.IsSuccessStatusCode); } ``` -**POST request** +##### POST request ```csharp //Payload object @@ -74,7 +75,7 @@ using (var client = new HttpTesterClient()) } ``` -**POST File Upload request** +##### 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. @@ -100,7 +101,31 @@ using (var client = new HttpTesterClient()) 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** +##### Deserialize HttpResponse message body + +`QAToolKit.Engine.HttpTester` supports 4 `HttpResponse` helper methods to give you maximum flexibility when reading reponse body. + +- `GetResponseBodyString`: return response content as a string. +- `GetResponseJsonBody`: deserialize response content from JSON to object +- `GetResponseXmlBody`: deserialize response content from XML to object. +- `GetResponseBodyBytes`: return response content as byte array. +- [Obsolete] `GetResponseBody`: this is obsolete, it deserializes response content from JSON to object. It's replaced by `GetResponseJsonBody`. + +##### HttpClient execution time is measured + +The library extends `HttpClient` object with `Duration` and `HttpDuration` properties. The first returns the measured duration of `Start();` method and the latter return the duration of HTTP request execution. + +```csharp + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .WithQueryParams(new Dictionary() { { "api-version", "1" } }) + .Start(); + + client.Duration; //Start() method execution duration + client.HttpDuration; //HTTP request duration +``` + +##### Create Tester client from QAToolKit Swagger request If you are using QAToolKit Swagger library to generate `HttpRequest` object you can use a `CreateHttpRequest` override. @@ -141,7 +166,7 @@ using (var client = new HttpTesterClient()) Currently `HttpTesterClient` supports: -**Basic authentication** +##### Basic authentication ```csharp var response = await client @@ -151,7 +176,7 @@ Currently `HttpTesterClient` supports: .Start(); ``` -**Bearer token authentication** +##### Bearer token authentication ```csharp var response = await client @@ -161,7 +186,7 @@ Currently `HttpTesterClient` supports: .Start(); ``` -**NTLM authentication** +##### NTLM authentication ```csharp var response = await client diff --git a/src/QAToolKit.Engine.HttpTester.Test/Fixtures/XMLFixture.cs b/src/QAToolKit.Engine.HttpTester.Test/Fixtures/XMLFixture.cs new file mode 100644 index 0000000..6e31847 --- /dev/null +++ b/src/QAToolKit.Engine.HttpTester.Test/Fixtures/XMLFixture.cs @@ -0,0 +1,47 @@ +namespace QAToolKit.Engine.HttpTester.Test.Fixtures +{ +// NOTE: Generated code may require at least .NET Framework 4.5 or .NET Core/Standard 2.0. + /// + [System.SerializableAttribute()] + [System.ComponentModel.DesignerCategoryAttribute("code")] + [System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] + [System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)] + public partial class note + { + private string toField; + + private string fromField; + + private string headingField; + + private string bodyField; + + /// + public string to + { + get { return this.toField; } + set { this.toField = value; } + } + + /// + public string from + { + get { return this.fromField; } + set { this.fromField = value; } + } + + /// + public string heading + { + get { return this.headingField; } + set { this.headingField = value; } + } + + /// + public string body + { + get { return this.bodyField; } + set { this.bodyField = value; } + } + } +} \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index 9b08823..5365428 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Xunit; @@ -114,9 +115,11 @@ public async Task HttpTestAsserterHeaderMissing_Fails() var asserter = new HttpTestAsserter(response); var duration = client.Duration; + var httpDuration = client.HttpDuration; Assert.Throws(() => asserter .ResponseContentContains("scott") .RequestDurationEquals(duration, (x) => x < 2000) + .RequestDurationEquals(httpDuration, (x) => x < 1800) .ResponseStatusCodeEquals(HttpStatusCode.OK) .ResponseHasHttpHeader(null) .AssertAll()); @@ -196,7 +199,7 @@ public async Task HttpTestAsserterDeleteIsBodyEmpty_Success() var duration = client.Duration; var assertResults = asserter .ResponseBodyIsEmpty() - .RequestDurationEquals(duration, (x) => (x > 100 && x < 1000)) + .RequestDurationEquals(duration, (x) => (x > 100 && x < 2000)) .ResponseStatusCodeIsSuccess() .AssertAll(); diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index 0b59a8f..dd87390 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -413,7 +413,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMDefaultAuthori .WithNTLMAuthentication() .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); 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 e984667..3458651 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj +++ b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -15,13 +15,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/QAToolKit.Engine.HttpTester.Test/XMLDeserializerTests.cs b/src/QAToolKit.Engine.HttpTester.Test/XMLDeserializerTests.cs new file mode 100644 index 0000000..ee1eb9c --- /dev/null +++ b/src/QAToolKit.Engine.HttpTester.Test/XMLDeserializerTests.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using QAToolKit.Engine.HttpTester.Extensions; +using QAToolKit.Engine.HttpTester.Test.Fixtures; +using Xunit; + +namespace QAToolKit.Engine.HttpTester.Test +{ + public class XmlDeserializerTests + { + [Fact] + public async Task HttpTesterClientSimple_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://www.w3schools.com/xml/note.xml")) + .WithMethod(HttpMethod.Get) + .Start(); + + var msg = await response.GetResponseXmlBody(); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Reminder", msg.heading); + Assert.Equal("Jani", msg.from); + } + } + } +} \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs b/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs index f82c3d4..981ac5c 100644 --- a/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs +++ b/src/QAToolKit.Engine.HttpTester/Extensions/HttpResponseMessageExtensions.cs @@ -1,6 +1,9 @@ -using Newtonsoft.Json; +using System; +using System.IO; +using Newtonsoft.Json; using System.Net.Http; using System.Threading.Tasks; +using System.Xml.Serialization; namespace QAToolKit.Engine.HttpTester.Extensions { @@ -15,11 +18,40 @@ public static class HttpResponseMessageExtensions /// /// /// + [Obsolete("This method is obsolete and will be deprecated. Use 'GetResponseJsonBody' instead.")] public static async Task GetResponseBody(this HttpResponseMessage httpResponseMessage) { var bodyResponse = await httpResponseMessage.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject(bodyResponse); } + + /// + /// Deserialize JSON response body to object + /// + /// + /// + /// + public static async Task GetResponseJsonBody(this HttpResponseMessage httpResponseMessage) + { + var bodyResponse = await httpResponseMessage.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(bodyResponse); + } + + /// + /// Deserialize XML response body to object + /// + /// + /// + /// + public static async Task GetResponseXmlBody(this HttpResponseMessage httpResponseMessage) + { + var bodyResponse = await httpResponseMessage.Content.ReadAsStringAsync(); + var xmlSerialize = new XmlSerializer(typeof(T)); + + var xmlResult = (T)xmlSerialize.Deserialize(new StringReader(bodyResponse)); + + return xmlResult ?? default(T); + } /// /// Get response as string diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index b2f305d..4ed535b 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -33,9 +33,14 @@ public class HttpTesterClient : IHttpTesterClient, IDisposable private MultipartFormDataContent _multipartFormDataContent = null; /// - /// Measured HTTP request duration + /// Measured duration of the whole request execution /// public long Duration { get; private set; } + + /// + /// Measured duration of the HTTP request execution (HttpClient.StartAsync(...)) + /// + public long HttpDuration { get; private set; } /// /// Create HTTP request client with or without certificate validation @@ -52,10 +57,7 @@ public IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertifi { HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; HttpHandler.ServerCertificateCustomValidationCallback = - (httpRequestMessage, cert, cetChain, policyErrors) => - { - return true; - }; + (httpRequestMessage, cert, cetChain, policyErrors) => true; } HttpClient = new HttpClient(HttpHandler) @@ -69,7 +71,7 @@ public IHttpTesterClient CreateHttpRequest(Uri baseAddress, bool validateCertifi /// /// 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. + /// Create tester client with BaseUrl, Path, HttpMethod, Headers and URL Query parameters read from HttpRequest object. Specify other values and parameters manually. /// /// public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validateCertificate = true) @@ -88,10 +90,7 @@ public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validat { HttpHandler.ClientCertificateOptions = ClientCertificateOption.Manual; HttpHandler.ServerCertificateCustomValidationCallback = - (httpRequestMessage, cert, cetChain, policyErrors) => - { - return true; - }; + (httpRequestMessage, cert, cetChain, policyErrors) => true; } HttpClient = new HttpClient(HttpHandler) @@ -109,8 +108,7 @@ public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validat _httpMethod = httpRequest.Method; //Query parameters - if (_queryParameters == null) - _queryParameters = new Dictionary(); + _queryParameters ??= new Dictionary(); foreach (var parameter in httpRequest.Parameters.Where(t => t.Location == Location.Query && t.Value != null)) { @@ -118,8 +116,7 @@ public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validat } //Headers - if (_headers == null) - _headers = new Dictionary(); + _headers ??= new Dictionary(); foreach (var header in httpRequest.Parameters.Where(t => t.Location == Location.Header && t.Value != null)) { @@ -136,16 +133,13 @@ public IHttpTesterClient CreateHttpRequest(HttpRequest httpRequest, bool validat /// public IHttpTesterClient WithPath(string urlPath) { - if (urlPath == null) - throw new ArgumentException($"{nameof(urlPath)} is null."); - - _path = urlPath; + _path = urlPath ?? throw new ArgumentException($"{nameof(urlPath)} is null."); return this; } /// - /// Replace URL path with path parametrs from passed dictionary + /// Replace URL path with path parameters from passed dictionary /// /// /// @@ -172,10 +166,7 @@ public IHttpTesterClient WithPathReplacementValues(Dictionary pa /// public IHttpTesterClient WithHeaders(Dictionary headers) { - if (headers == null) - throw new ArgumentException($"{nameof(headers)} is null."); - - _headers = headers; + _headers = headers ?? throw new ArgumentException($"{nameof(headers)} is null."); return this; } @@ -196,7 +187,7 @@ public IHttpTesterClient WithJsonBody(T bodyObject) return this; } - if (typeof(T) == typeof(String)) + if (typeof(T) == typeof(string)) { _body = bodyObject.ToString(); } @@ -224,16 +215,13 @@ public IHttpTesterClient WithMethod(HttpMethod httpMethod) } /// - /// Specify HTTP query paramters to HTTP client + /// Specify HTTP query parameters to HTTP client /// /// /// public IHttpTesterClient WithQueryParams(Dictionary queryParameters) { - if (queryParameters == null) - throw new ArgumentException($"{nameof(queryParameters)} is null."); - - _queryParameters = queryParameters; + _queryParameters = queryParameters ?? throw new ArgumentException($"{nameof(queryParameters)} is null."); return this; } @@ -252,7 +240,7 @@ public IHttpTesterClient WithBasicAuthentication(string userName, string passwor throw new ArgumentException($"{nameof(password)} is null."); var authenticationString = $"{userName}:{password}"; - var base64EncodedAuthenticationString = Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(authenticationString)); + var base64EncodedAuthenticationString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); return this; @@ -323,10 +311,7 @@ public IHttpTesterClient WithMultipart(byte[] fileByteArray, string httpContentN if (string.IsNullOrEmpty(httpContentName)) throw new ArgumentNullException($"{nameof(httpContentName)} is null."); - if (_multipartFormDataContent == null) - { - _multipartFormDataContent = new MultipartFormDataContent(); - } + _multipartFormDataContent ??= new MultipartFormDataContent(); _multipartFormDataContent.Add(new ByteArrayContent(fileByteArray), httpContentName, fileName); @@ -348,10 +333,7 @@ public IHttpTesterClient WithMultipart(string httpContentName, string value) if (value == null) throw new ArgumentNullException($"{nameof(value)} is null."); - if (_multipartFormDataContent == null) - { - _multipartFormDataContent = new MultipartFormDataContent(); - } + _multipartFormDataContent ??= new MultipartFormDataContent(); _multipartFormDataContent.Add(new StringContent(value), httpContentName); @@ -364,6 +346,9 @@ public IHttpTesterClient WithMultipart(string httpContentName, string value) /// public async Task Start() { + var sw = new Stopwatch(); + sw.Start(); + if (HttpClient == null) throw new QAToolKitEngineHttpTesterException("HttpClient is null. Create an object first with 'CreateHttpRequest'."); if (_httpMethod == null) @@ -371,22 +356,16 @@ public async Task Start() if (_httpMethod == HttpMethod.Get && _body != null) throw new QAToolKitEngineHttpTesterException("'Get' method can not have a HTTP body."); - StringBuilder queryString = new StringBuilder(); + var queryString = new StringBuilder(); if (_queryParameters != null) { queryString.Append("?"); - List array = new List(); - foreach (var query in _queryParameters) - { - array.Add($"{query.Key}={query.Value}"); - } + var array = _queryParameters.Select(query => $"{query.Key}={query.Value}").ToList(); queryString.Append(string.Join("&", array)); } - var sw = new Stopwatch(); - sw.Start(); using (var requestMessage = new HttpRequestMessage(_httpMethod, _path + queryString.ToString())) { if (_headers != null) @@ -407,8 +386,14 @@ public async Task Start() requestMessage.Content = _multipartFormDataContent; } + var swHttp = new Stopwatch(); + swHttp.Start(); _responseMessage = await HttpClient.SendAsync(requestMessage); + swHttp.Stop(); + + HttpDuration = swHttp.ElapsedMilliseconds; } + sw.Stop(); Duration = sw.ElapsedMilliseconds; From c99d168038a4b25d349171c1193fb0a384427c75 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Tue, 13 Apr 2021 17:59:17 +0200 Subject: [PATCH 09/14] HTTP authentication with client certificate --- .github/workflows/dotnet-core.yml | 4 +- Directory.Build.props | 2 +- Nuget.config | 16 +++ README.md | 24 +++- ...QAToolKitEngineHttpTesterExceptionTests.cs | 1 + .../HttpTestAsserterTests.cs | 12 +- .../HttpTesterClientTests.cs | 128 +++++++++++++----- .../QAToolKit.Engine.HttpTester.Test.csproj | 7 +- .../QAToolKitEngineHttpTesterException.cs | 7 - .../HttpTesterClient.cs | 26 ++++ .../Interfaces/IHttpTesterClient.cs | 6 + .../QAToolKit.Engine.HttpTester.csproj | 4 +- 12 files changed, 176 insertions(+), 61 deletions(-) create mode 100644 Nuget.config diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 7427075..ead8c3d 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -24,8 +24,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.x' + - name: Clean + run: dotnet clean --configuration Release && dotnet nuget locals all --clear - name: Install dependencies - run: dotnet restore + run: dotnet restore qatoolkit-engine-httptester-net.sln - name: Build run: dotnet build --configuration Release --no-restore - name: Test diff --git a/Directory.Build.props b/Directory.Build.props index 9c576a9..2fb84b8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.3.0 + 0.3.1 \ No newline at end of file diff --git a/Nuget.config b/Nuget.config new file mode 100644 index 0000000..9028450 --- /dev/null +++ b/Nuget.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 852e9ae..9286598 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ using (var client = new HttpTesterClient()) .WithBearerAuthentication("eXy....") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); - var expecterResponse = BicycleFixture.GetBicycles().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetBicycles().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); //Start() method execution duration Assert.True(client.HttpDuration < 2000); //HTTP request duration @@ -64,10 +64,10 @@ using (var client = new HttpTesterClient()) .WithBasicAuthentication("user", "pass") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = bicycle.ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = bicycle.ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -196,6 +196,18 @@ Currently `HttpTesterClient` supports: .Start(); ``` +##### Client certificate authentication + +You can pass `X509Certificate2` objet to the `WithCertificateAuthentication` method from certificate store or file. + +```csharp + var response = await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) + .... + .WithCertificateAuthentication(new X509Certificate2(...)) + .Start(); +``` + ### HttpTestAsserter This is an implementation of the HTTP response message asserter, which can be used to assert different parameters. diff --git a/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs b/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs index 74247c9..0fdd524 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs @@ -1,5 +1,6 @@ using QAToolKit.Engine.HttpTester.Exceptions; using System; +using System.Runtime.Serialization; using Xunit; namespace QAToolKit.Engine.HttpTester.Test.Exceptions diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index 5365428..2bf9423 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -25,7 +25,7 @@ public async Task HttpTestAsserterSimple_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); @@ -56,7 +56,7 @@ public async Task HttpTestAsserterDoesNotContainHeader_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); @@ -83,7 +83,7 @@ public async Task HttpTestAsserterDoesNotContainKeywordInBody_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); @@ -110,7 +110,7 @@ public async Task HttpTestAsserterHeaderMissing_Fails() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); @@ -138,7 +138,7 @@ public async Task HttpTestAsserterBodyNull_Fails() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); @@ -163,7 +163,7 @@ public async Task HttpTestAsserterAlternativeDurationPredicate_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); var asserter = new HttpTestAsserter(response); var duration = client.Duration; diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs index dd87390..7441686 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTesterClientTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Xunit; @@ -52,7 +53,7 @@ public async Task HttpTesterClientWithSwagger_Success() .WithJsonBody(BicycleFixture.Get()) .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -72,10 +73,10 @@ public async Task HttpTesterClientSimple_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); - var expecterResponse = BicycleFixture.GetBicycles().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetBicycles().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -94,10 +95,10 @@ public async Task HttpTesterClientSimpleGet_Success() .WithPath("/api/bicycles/1") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = BicycleFixture.GetFoil().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetFoil().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -118,10 +119,10 @@ public async Task HttpTesterClientWithoutHeaders_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = BicycleFixture.GetCfr().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetCfr().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -229,10 +230,10 @@ public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithValidCert_S .WithPath("/api/bicycles/1") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = BicycleFixture.GetFoil().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetFoil().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -252,10 +253,33 @@ public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithInvalidCert .WithPath("/api/bicycles/1") .Start(); + var msg = await response.GetResponseJsonBody(); + + var expectedResponse = BicycleFixture.GetFoil().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); + + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + Assert.Equal("Scott", msg.Brand); + } + } + + [Fact] + public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithInvalidCertAndResponseBody_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://swagger-demo.qatoolkit.io/"), false) + .WithQueryParams(new Dictionary() { { "api-version", "1" } }) + .WithMethod(HttpMethod.Get) + .WithPath("/api/bicycles/1") + .Start(); + var msg = await response.GetResponseBody(); - var expecterResponse = BicycleFixture.GetFoil().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetFoil().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -263,6 +287,26 @@ public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithInvalidCert } } + [Fact] + public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithInvalidCertAndResponseBytes_Success() + { + using (var client = new HttpTesterClient()) + { + var response = await client + .CreateHttpRequest(new Uri("https://swagger-demo.qatoolkit.io/"), false) + .WithQueryParams(new Dictionary() { { "api-version", "1" } }) + .WithMethod(HttpMethod.Get) + .WithPath("/api/bicycles/1") + .Start(); + + var bytes = await response.GetResponseBodyBytes(); + + Assert.NotNull(bytes); + Assert.True(client.Duration < 2000); + Assert.True(response.IsSuccessStatusCode); + } + } + [Fact] public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithHttpUrl_Exception() { @@ -275,17 +319,17 @@ public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithHttpUrl_Exc .WithPath("/api/bicycles/1") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = BicycleFixture.GetFoil().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetFoil().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); Assert.Equal("Scott", msg.Brand); } } - + [Fact] public async Task HttpTesterClientGetWithBodyDisableSSLValidationWithInvalidCertAndUrl2_Exception() { @@ -314,7 +358,7 @@ public async Task HttpTesterClientReturnDynamic_Success() .WithPath("/api/bicycles") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -351,7 +395,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrl_Success() .WithMethod(HttpMethod.Post) .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -371,7 +415,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithBasicAuthorization .WithBasicAuthentication("user", "pass") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(client.HttpClient.DefaultRequestHeaders.Contains("Authorization")); @@ -392,7 +436,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithBearerAuthorizatio .WithBearerAuthentication("123") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(client.HttpClient.DefaultRequestHeaders.Contains("Authorization")); @@ -433,7 +477,7 @@ public async Task HttpTesterClientPostObjectBodyWithFulUrlWithNTLMAuthorization_ .WithNTLMAuthentication("user", "pass") .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -446,7 +490,7 @@ public async Task HttpTesterClientFileUpload_Success() { using (var client = new HttpTesterClient()) { - byte[] image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); + var image = new WebClient().DownloadData("https://qatoolkit.io/assets/logo.png"); var response = await client .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net")) @@ -577,7 +621,7 @@ public async Task HttpTesterClientAddPostHttpRequest_Success() .WithJsonBody(BicycleFixture.Get()) .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -608,10 +652,10 @@ public async Task HttpTesterClientAddGetHttpRequest_Success() .CreateHttpRequest(requests.FirstOrDefault()) .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); - var expecterResponse = BicycleFixture.GetBicycles().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetBicycles().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -643,10 +687,10 @@ public async Task HttpTesterClientGetBikeByIdRequest_Success() .WithQueryParams(new Dictionary() { { "api-version", "2" } }) .Start(); - var msg = await response.GetResponseBody(); + var msg = await response.GetResponseJsonBody(); - var expecterResponse = BicycleFixture.GetCannondale().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetCannondale().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); @@ -677,14 +721,28 @@ public async Task HttpTesterClientGetBikesByTypeHttpRequest_Success() .WithQueryParams(new Dictionary() { { "api-version", "2" }, {"bicycleType", "1" } }) .Start(); - var msg = await response.GetResponseBody>(); + var msg = await response.GetResponseJsonBody>(); - var expecterResponse = BicycleFixture.GetCannondaleArray().ToExpectedObject(); - expecterResponse.ShouldEqual(msg); + var expectedResponse = BicycleFixture.GetCannondaleArray().ToExpectedObject(); + expectedResponse.ShouldEqual(msg); Assert.True(client.Duration < 2000); Assert.True(response.IsSuccessStatusCode); } } + + [Fact] + public async Task HttpTesterClientPostObjectBodyWithBlankCertificateDefaultAuthorization_Success() + { + using (var client = new HttpTesterClient()) + { + await Assert.ThrowsAsync(async () => await client + .CreateHttpRequest(new Uri("https://qatoolkitapi.azurewebsites.net/api/bicycles?api-version=1")) + .WithJsonBody(BicycleFixture.GetCfr()) + .WithMethod(HttpMethod.Post) + .WithCertificateAuthentication(new X509Certificate2()) + .Start()); + } + } } } 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 3458651..b843411 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj +++ b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj @@ -11,11 +11,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + - - - + + + all diff --git a/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs b/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs index c4f7df1..f050139 100644 --- a/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs +++ b/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs @@ -23,12 +23,5 @@ public QAToolKitEngineHttpTesterException(string message) : base(message) public QAToolKitEngineHttpTesterException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// QA Toolkit core exception - /// - protected QAToolKitEngineHttpTesterException(SerializationInfo info, StreamingContext context) : base(info, context) - { - } } } diff --git a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs index 4ed535b..f8fa60f 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTesterClient.cs @@ -9,6 +9,8 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; @@ -295,6 +297,30 @@ public IHttpTesterClient WithNTLMAuthentication() return this; } + /// + /// Use client certificate for authentication + /// + /// + public IHttpTesterClient WithCertificateAuthentication(X509Certificate2 certificate) + { + HttpHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => + { + if (sslPolicyErrors == SslPolicyErrors.None) + { + return true; + } + + throw new QAToolKitEngineHttpTesterException("Certificate validation failed."); + }; + + if (certificate != null) + { + HttpHandler.ClientCertificates.Add(certificate); + } + + return this; + } + /// /// Create a multipart form data content and add a file content /// diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs index d892500..2466ce3 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTesterClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; namespace QAToolKit.Engine.HttpTester.Interfaces @@ -88,6 +89,11 @@ public interface IHttpTesterClient /// IHttpTesterClient WithNTLMAuthentication(); /// + /// Authenticate with client certificate + /// + /// + IHttpTesterClient WithCertificateAuthentication(X509Certificate2 certificate); + /// /// Upload a file /// /// diff --git a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj index 5fddda3..3db3c9e 100644 --- a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj +++ b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj @@ -34,7 +34,7 @@ - - + + From 2bc133ab813830e5997b299d23b3c70a612e6e42 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Tue, 17 Aug 2021 21:31:50 +0200 Subject: [PATCH 10/14] nuget updates --- Directory.Build.props | 2 +- .../QAToolKit.Engine.HttpTester.Test.csproj | 6 +++--- .../QAToolKit.Engine.HttpTester.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2fb84b8..bf8f8fa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.3.1 + 0.3.2 \ No newline at end of file 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 b843411..4ed442e 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj +++ b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj @@ -7,14 +7,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj index 3db3c9e..9541e3b 100644 --- a/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj +++ b/src/QAToolKit.Engine.HttpTester/QAToolKit.Engine.HttpTester.csproj @@ -35,6 +35,6 @@ - + From 4787e818dd3512a530fb1d9629c2ed8c54b7365e Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Tue, 17 Aug 2021 21:42:47 +0200 Subject: [PATCH 11/14] nuget updates --- .../QAToolKit.Engine.HttpTester.Test.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 b843411..4ed442e 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj +++ b/src/QAToolKit.Engine.HttpTester.Test/QAToolKit.Engine.HttpTester.Test.csproj @@ -7,14 +7,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 5795435c594aaf0832889fb0e122bd1cf7b0efb6 Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Mon, 1 Nov 2021 17:35:51 +0100 Subject: [PATCH 12/14] Implementation of assert named ResponseContentTypeEquals (#32) --- Directory.Build.props | 2 +- README.md | 13 ++++----- .../HttpTestAsserterTests.cs | 7 ++++- .../HttpTestAsserter.cs | 27 +++++++++++++++++++ .../Interfaces/IHttpTestAsserter.cs | 7 +++++ 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index bf8f8fa..59ed3a3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.3.2 + 0.3.3 \ No newline at end of file diff --git a/README.md b/README.md index 9286598..7391949 100644 --- a/README.md +++ b/README.md @@ -213,12 +213,13 @@ You can pass `X509Certificate2` objet to the `WithCertificateAuthentication` met 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 +- `ResponseContentContains`: HTTP body contains a string (ignores case). +- `RequestDurationEquals`: Verify request duration. +- `ResponseStatusCodeEquals`: Verify if response code equals to specified. +- `ResponseHasHttpHeader`: HTTP response contains a header. +- `ResponseStatusCodeIsSuccess`: HTTP response status code is one of 2xx. +- `ResponseBodyIsEmpty`: HTTP response body is empty. +- `ResponseContentTypeEquals`: Check if HTTP response media type equals to specified. Asserter produces a list of `AssertResult`: diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index 2bf9423..581f806 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Runtime.InteropServices; using System.Threading.Tasks; using Xunit; @@ -34,6 +35,7 @@ public async Task HttpTestAsserterSimple_Success() .ResponseContentContains("scott") .RequestDurationEquals(duration, (x) => x < 2000) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseContentTypeEquals("application/json") .ResponseHasHttpHeader("Date") .AssertAll(); @@ -121,6 +123,7 @@ public async Task HttpTestAsserterHeaderMissing_Fails() .RequestDurationEquals(duration, (x) => x < 2000) .RequestDurationEquals(httpDuration, (x) => x < 1800) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseContentTypeEquals("application/json") .ResponseHasHttpHeader(null) .AssertAll()); } @@ -146,6 +149,7 @@ public async Task HttpTestAsserterBodyNull_Fails() Assert.Throws(() => asserter .ResponseContentContains(null) .RequestDurationEquals(duration, (x) => x < 1000) + .ResponseContentTypeEquals("application/json") .ResponseStatusCodeEquals(HttpStatusCode.OK) .AssertAll()); } @@ -172,10 +176,11 @@ public async Task HttpTestAsserterAlternativeDurationPredicate_Success() .ResponseContentContains("id") .RequestDurationEquals(duration, (x) => (x > 100 && x < 1000)) .ResponseStatusCodeEquals(HttpStatusCode.OK) + .ResponseContentTypeEquals("application/json") .ResponseStatusCodeIsSuccess() .AssertAll(); - Assert.Equal(5, assertResults.ToList().Count); + Assert.Equal(6, assertResults.ToList().Count); foreach (var result in assertResults) { Assert.True(result.IsTrue, result.Message); diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index 6564097..776ffb1 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; namespace QAToolKit.Engine.HttpTester { @@ -59,6 +61,31 @@ public IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsens return this; } + + /// + /// Check if the response contains specified Content Type + /// + /// + /// + /// + public IHttpTestAsserter ResponseContentTypeEquals(string contentType) + { + if (contentType == null) + { + throw new ArgumentNullException($"{nameof(contentType)} is null."); + } + + var bodyString = _httpResponseMessage.Content.Headers.ContentType; + + _assertResults.Add(new AssertResult() + { + Name = nameof(ResponseContentTypeEquals), + Message = $"Response content-type equals '{contentType}'.", + IsTrue = _httpResponseMessage.Content.Headers.ContentType.MediaType == contentType + }); + + return this; + } /// /// Verify request duration diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs index 0f795d0..d621001 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http.Headers; namespace QAToolKit.Engine.HttpTester.Interfaces { @@ -47,6 +48,12 @@ public interface IHttpTestAsserter /// IHttpTestAsserter ResponseBodyIsEmpty(); /// + /// Check if the response contains specified Content Type + /// + /// + /// + IHttpTestAsserter ResponseContentTypeEquals(string contentType); + /// /// Return all Assert messages of the Asserter /// /// From d78599574256b4734717e9b80497070501268cae Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Wed, 3 Nov 2021 10:43:20 +0100 Subject: [PATCH 13/14] asserter updates (#34) --- Directory.Build.props | 2 +- ...QAToolKitEngineHttpTesterExceptionTests.cs | 1 - .../HttpTestAsserterTests.cs | 6 +- .../QAToolKitEngineHttpTesterException.cs | 1 - .../HttpTestAsserter.cs | 101 ++++++++++++------ .../Interfaces/IHttpTestAsserter.cs | 6 +- 6 files changed, 77 insertions(+), 40 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 59ed3a3..83b1ab4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 0.3.3 + 0.3.4 \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs b/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs index 0fdd524..74247c9 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/Exceptions/QAToolKitEngineHttpTesterExceptionTests.cs @@ -1,6 +1,5 @@ using QAToolKit.Engine.HttpTester.Exceptions; using System; -using System.Runtime.Serialization; using Xunit; namespace QAToolKit.Engine.HttpTester.Test.Exceptions diff --git a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs index 581f806..6919688 100644 --- a/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs +++ b/src/QAToolKit.Engine.HttpTester.Test/HttpTestAsserterTests.cs @@ -5,13 +5,11 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Xunit; namespace QAToolKit.Engine.HttpTester.Test -{ +{ public class HttpTestAsserterTests { [Fact] @@ -120,7 +118,7 @@ public async Task HttpTestAsserterHeaderMissing_Fails() var httpDuration = client.HttpDuration; Assert.Throws(() => asserter .ResponseContentContains("scott") - .RequestDurationEquals(duration, (x) => x < 2000) + .RequestDurationEquals(duration, (x) => x < Convert.ToInt64("2000"), "x < 2000") .RequestDurationEquals(httpDuration, (x) => x < 1800) .ResponseStatusCodeEquals(HttpStatusCode.OK) .ResponseContentTypeEquals("application/json") diff --git a/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs b/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs index f050139..e3814ba 100644 --- a/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs +++ b/src/QAToolKit.Engine.HttpTester/Exceptions/QAToolKitEngineHttpTesterException.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.Serialization; namespace QAToolKit.Engine.HttpTester.Exceptions { diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index 776ffb1..acc13a8 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -3,10 +3,9 @@ using QAToolKit.Engine.HttpTester.Models; using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.Mime; namespace QAToolKit.Engine.HttpTester { @@ -24,7 +23,8 @@ public class HttpTestAsserter : IHttpTestAsserter /// public HttpTestAsserter(HttpResponseMessage httpResponseMessage) { - _httpResponseMessage = httpResponseMessage ?? throw new ArgumentNullException($"{nameof(httpResponseMessage)} is null."); + _httpResponseMessage = httpResponseMessage ?? + throw new ArgumentNullException($"{nameof(httpResponseMessage)} is null."); _assertResults = new List(); } @@ -40,8 +40,8 @@ public IEnumerable AssertAll() /// /// HTTP body contains a string (ignores case) /// - /// - /// + /// Check if the HTTP response body contains a keyword. + /// Use case sensitive string comparison. /// public IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsensitive = true) { @@ -52,20 +52,27 @@ public IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsens var bodyString = _httpResponseMessage.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - _assertResults.Add(new AssertResult() + var assertResult = new AssertResult() { - Name = nameof(ResponseContentContains), - Message = $"Body contains '{keyword}'.", - IsTrue = caseInsensitive ? StringHelper.ContainsCaseInsensitive(bodyString, keyword) : bodyString.Contains(keyword) - }); + Name = nameof(ResponseContentContains) + }; + + assertResult.IsTrue = caseInsensitive + ? StringHelper.ContainsCaseInsensitive(bodyString, keyword) + : bodyString.Contains(keyword); + assertResult.Message = assertResult.IsTrue + ? $"Response body contains keyword '{keyword}'." + : $"Response body does not contain keyword '{keyword}'."; + + _assertResults.Add(assertResult); return this; } - + /// /// Check if the response contains specified Content Type /// - /// + /// Check if the response content type equals the parameter. /// /// public IHttpTestAsserter ResponseContentTypeEquals(string contentType) @@ -75,12 +82,12 @@ public IHttpTestAsserter ResponseContentTypeEquals(string contentType) throw new ArgumentNullException($"{nameof(contentType)} is null."); } - var bodyString = _httpResponseMessage.Content.Headers.ContentType; - + var responseContentType = _httpResponseMessage.Content.Headers.ContentType.MediaType; + _assertResults.Add(new AssertResult() { Name = nameof(ResponseContentTypeEquals), - Message = $"Response content-type equals '{contentType}'.", + Message = $"Expected content-type = '{contentType}', actual = '{responseContentType}'.", IsTrue = _httpResponseMessage.Content.Headers.ContentType.MediaType == contentType }); @@ -90,18 +97,44 @@ public IHttpTestAsserter ResponseContentTypeEquals(string contentType) /// /// Verify request duration /// - /// - /// + /// Actual duration of the HTTP request execution + /// It's a function that validates the duration + /// String predicateFunctionExpression for the report. If set it will be used in the assert result message. /// - public IHttpTestAsserter RequestDurationEquals(long duration, Func predicateFunction) + public IHttpTestAsserter RequestDurationEquals(long duration, Expression> predicateFunction, string predicateFunctionExpression = null) { - var isTrue = predicateFunction.Invoke(duration); - _assertResults.Add(new AssertResult() + var isTrue = predicateFunction.Compile()(duration); + + var assertResult = new AssertResult() { Name = nameof(RequestDurationEquals), - Message = $"Duration is '{duration}'.", IsTrue = isTrue - }); + }; + + if (isTrue) + { + if (string.IsNullOrEmpty(predicateFunctionExpression)) + { + assertResult.Message = $"Duration is '{duration}ms' and is valid."; + } + else + { + assertResult.Message = $"Duration is '{duration}ms' and is valid with predicateFunctionExpression '{predicateFunctionExpression}'."; + } + } + else + { + if (string.IsNullOrEmpty(predicateFunctionExpression)) + { + assertResult.Message = $"Duration is '{duration}ms' and is invalid."; + } + else + { + assertResult.Message = $"Duration is '{duration}ms' and is invalid with predicateFunctionExpression '{predicateFunctionExpression}'."; + } + } + + _assertResults.Add(assertResult); return this; } @@ -109,7 +142,7 @@ public IHttpTestAsserter RequestDurationEquals(long duration, Func p /// /// HTTP response contains a header /// - /// + /// Check if the HTTP response contains the header with the name. /// public IHttpTestAsserter ResponseHasHttpHeader(string headerName) { @@ -118,12 +151,17 @@ public IHttpTestAsserter ResponseHasHttpHeader(string headerName) throw new ArgumentNullException($"{nameof(headerName)} is null."); } - _assertResults.Add(new AssertResult() + var assertResult = new AssertResult { Name = nameof(ResponseHasHttpHeader), - Message = $"Contains header '{headerName}'.", IsTrue = _httpResponseMessage.Headers.TryGetValues(headerName, out var values) - }); + }; + + assertResult.Message = assertResult.IsTrue + ? $"Response message contains header '{headerName}'." + : $"Response message does not contain header '{headerName}'."; + + _assertResults.Add(assertResult); return this; } @@ -131,14 +169,15 @@ public IHttpTestAsserter ResponseHasHttpHeader(string headerName) /// /// Verify if response code equals /// - /// + /// Check if the HTTP response status code equals to this parameter. /// public IHttpTestAsserter ResponseStatusCodeEquals(HttpStatusCode httpStatusCode) { _assertResults.Add(new AssertResult() { Name = nameof(ResponseStatusCodeEquals), - Message = $"Expected status code is '{httpStatusCode}' return code is '{_httpResponseMessage.StatusCode}'.", + Message = + $"Expected status code = '{httpStatusCode}', actual = '{_httpResponseMessage.StatusCode}'.", IsTrue = _httpResponseMessage.StatusCode == httpStatusCode }); @@ -154,7 +193,7 @@ public IHttpTestAsserter ResponseStatusCodeIsSuccess() _assertResults.Add(new AssertResult() { Name = nameof(ResponseStatusCodeIsSuccess), - Message = $"Expected status code is '2xx' return code is '{_httpResponseMessage.StatusCode}'.", + Message = $"Expected status code = '2xx', actual = '{_httpResponseMessage.StatusCode}'.", IsTrue = _httpResponseMessage.IsSuccessStatusCode }); @@ -172,11 +211,11 @@ public IHttpTestAsserter ResponseBodyIsEmpty() _assertResults.Add(new AssertResult() { Name = nameof(ResponseBodyIsEmpty), - Message = $"Expected empty body, returned body is '{bodyString}'.", + Message = $"Expected empty response body, actual = '{bodyString}'.", IsTrue = string.IsNullOrEmpty(bodyString) }); return this; } } -} +} \ No newline at end of file diff --git a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs index d621001..94e9be4 100644 --- a/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/Interfaces/IHttpTestAsserter.cs @@ -1,8 +1,8 @@ using QAToolKit.Engine.HttpTester.Models; using System; using System.Collections.Generic; +using System.Linq.Expressions; using System.Net; -using System.Net.Http.Headers; namespace QAToolKit.Engine.HttpTester.Interfaces { @@ -18,13 +18,15 @@ public interface IHttpTestAsserter /// /// IHttpTestAsserter ResponseContentContains(string keyword, bool caseInsensitive = true); + /// /// Verify request duration /// /// /// + /// /// - IHttpTestAsserter RequestDurationEquals(long duration, Func predicateFunction); + IHttpTestAsserter RequestDurationEquals(long duration, Expression> predicateFunction, string predicateFunctionExpression = null); /// /// Verify if response code equals /// From df7891e33eb5bc6e8daa383b96db4c603710346b Mon Sep 17 00:00:00 2001 From: Miha Jakovac Date: Wed, 3 Nov 2021 10:53:25 +0100 Subject: [PATCH 14/14] removed duplicate function --- .../HttpTestAsserter.cs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs index f903529..ec3aa55 100644 --- a/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs +++ b/src/QAToolKit.Engine.HttpTester/HttpTestAsserter.cs @@ -95,31 +95,6 @@ public IHttpTestAsserter ResponseContentTypeEquals(string contentType) return this; } - - /// - /// Check if the response contains specified Content Type - /// - /// - /// - /// - public IHttpTestAsserter ResponseContentTypeEquals(string contentType) - { - if (contentType == null) - { - throw new ArgumentNullException($"{nameof(contentType)} is null."); - } - - var bodyString = _httpResponseMessage.Content.Headers.ContentType; - - _assertResults.Add(new AssertResult() - { - Name = nameof(ResponseContentTypeEquals), - Message = $"Response content-type equals '{contentType}'.", - IsTrue = _httpResponseMessage.Content.Headers.ContentType.MediaType == contentType - }); - - return this; - } /// /// Verify request duration