diff --git a/RestAssured.Net.Tests/LoggingTests.cs b/RestAssured.Net.Tests/LoggingTests.cs index 2b82ece..ebaf795 100644 --- a/RestAssured.Net.Tests/LoggingTests.cs +++ b/RestAssured.Net.Tests/LoggingTests.cs @@ -16,6 +16,8 @@ namespace RestAssured.Tests { using NUnit.Framework; + using RestAssured.Logging; + using RestAssured.Request.Builders; using RestAssured.Request.Logging; using RestAssured.Response.Logging; using WireMock.RequestBuilders; @@ -39,8 +41,35 @@ public void RequestDetailsCanBeWrittenToStandardOutputForJson() { this.CreateStubForLoggingJsonResponse(); + var logConfig = new LogConfiguration + { + RequestLogLevel = Logging.RequestLogLevel.All, + }; + + Given() + .Log(logConfig) + .And() + .Accept("application/json") + .Header("CustomHeader", "custom header value") + .ContentType("application/json") + .Body(this.jsonBody) + .When() + .Get($"{MOCK_SERVER_BASE_URL}/log-json-response") + .Then() + .StatusCode(200); + } + + /// + /// A test demonstrating RestAssuredNet syntax for logging + /// JSON request details to the standard output. + /// + [Test] + public void RequestLogLevelCanBeSpecifiedUsingObsoleteMethod() + { + this.CreateStubForLoggingJsonResponse(); + Given() - .Log(RequestLogLevel.All) + .Log(RestAssured.Request.Logging.RequestLogLevel.All) .And() .Accept("application/json") .Header("CustomHeader", "custom header value") @@ -61,8 +90,13 @@ public void RequestDetailsCanBeWrittenToStandardOutputForXml() { this.CreateStubForLoggingXmlResponse(); + var logConfig = new LogConfiguration + { + RequestLogLevel = Logging.RequestLogLevel.All, + }; + Given() - .Log(RequestLogLevel.All) + .Log(logConfig) .ContentType("application/xml") .Body(this.GetLocationAsXmlString()) .When() @@ -80,11 +114,33 @@ public void ResponseDetailsCanBeWrittenToStandardOutputForJson() { this.CreateStubForLoggingJsonResponse(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.All, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-json-response") .Then() - .Log(ResponseLogLevel.All) + .StatusCode(200); + } + + /// + /// A test demonstrating RestAssuredNet syntax for logging + /// XML response details to the standard output. + /// + [Test] + public void ResponseDetailsCanBeWrittenToStandardOutputForXmlUsingObsoleteMethod() + { + this.CreateStubForLoggingXmlResponse(); + + Given() + .When() + .Get($"{MOCK_SERVER_BASE_URL}/log-xml-response") + .Then() + .Log(RestAssured.Response.Logging.ResponseLogLevel.All) .And() .StatusCode(200); } @@ -98,12 +154,16 @@ public void ResponseDetailsCanBeWrittenToStandardOutputForXml() { this.CreateStubForLoggingXmlResponse(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.All, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-xml-response") .Then() - .Log(ResponseLogLevel.All) - .And() .StatusCode(200); } @@ -117,12 +177,16 @@ public void NoResponseBodyDoesntThrowNullReferenceException() { this.CreateStubForLoggingResponseWithoutBody(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.All, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-no-response-body") .Then() - .Log(ResponseLogLevel.All) - .And() .StatusCode(200); } @@ -136,8 +200,13 @@ public void NoRequestBodyDoesntThrowNullReferenceException() { this.CreateStubForLoggingResponseWithoutBody(); + var logConfig = new LogConfiguration + { + RequestLogLevel = Logging.RequestLogLevel.All, + }; + Given() - .Log(RequestLogLevel.All) + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-no-response-body") .Then() @@ -155,11 +224,16 @@ public void ResponseBodyDetailsAreLoggedOnlyOnErrorResponseCode() { this.CreateStubForErrorResponse(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.OnError, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/error-response-body") .Then() - .Log(ResponseLogLevel.OnError) .StatusCode(404); } @@ -174,11 +248,16 @@ public void ResponseBodyDetailsAreNotLoggedOnOkResponseCode() { this.CreateStubForLoggingJsonResponse(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.OnError, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-json-response") .Then() - .Log(ResponseLogLevel.OnError) .StatusCode(200); } @@ -193,11 +272,49 @@ public void ResponseBodyDetailsAreLoggedIfVerificationFails() { this.CreateStubForLoggingJsonResponse(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.OnVerificationFailure, + }; + + Given() + .Log(logConfig) + .When() + .Get($"{MOCK_SERVER_BASE_URL}/log-json-response") + .Then() + .StatusCode(200); + } + + /// + /// A test demonstrating RestAssuredNet syntax for logging + /// response details to the standard output, overwriting logging details from + /// a request specification with specific settings. + /// + [Test] + public void ResponseBodyDetailsAreLoggedCorrectlyOverwritingRequestSpecificationSettings() + { + this.CreateStubForLoggingJsonResponse(); + + var originalLogConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.All, + }; + + var requestSpecification = new RequestSpecBuilder() + .WithLogConfiguration(originalLogConfig) + .Build(); + + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.ResponseTime, + }; + Given() + .Spec(requestSpecification) + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-json-response") .Then() - .Log(ResponseLogLevel.OnVerificationFailure) .StatusCode(200); } @@ -210,11 +327,16 @@ public void ResponseCookieDetailsAreLogged() { this.CreateStubForLoggingResponseWithCookie(); + var logConfig = new LogConfiguration + { + ResponseLogLevel = Logging.ResponseLogLevel.All, + }; + Given() + .Log(logConfig) .When() .Get($"{MOCK_SERVER_BASE_URL}/log-response-cookie") .Then() - .Log(ResponseLogLevel.All) .StatusCode(200); } diff --git a/RestAssured.Net/Configuration/RestAssuredConfiguration.cs b/RestAssured.Net/Configuration/RestAssuredConfiguration.cs index fa886bd..ac562bf 100644 --- a/RestAssured.Net/Configuration/RestAssuredConfiguration.cs +++ b/RestAssured.Net/Configuration/RestAssuredConfiguration.cs @@ -15,7 +15,9 @@ // namespace RestAssured.Configuration { + using System; using System.Net.Http; + using RestAssured.Logging; using RestAssured.Request.Logging; using RestAssured.Response.Logging; @@ -29,15 +31,22 @@ public class RestAssuredConfiguration /// public bool DisableSslCertificateValidation { get; set; } = false; + /// + /// Configuration for be used when logging request and response details. + /// + public LogConfiguration LogConfiguration { get; set; } = new LogConfiguration(); + /// /// Setting to configure request logging level for all tests. /// - public RequestLogLevel RequestLogLevel { get; set; } = RequestLogLevel.None; + [Obsolete("Use the LogConfiguration property to set request logging options instead. This property will be removed in RestAssured.Net 5.0.0")] + public Request.Logging.RequestLogLevel RequestLogLevel { get; set; } = Request.Logging.RequestLogLevel.None; /// /// Setting to configure response logging level for all tests. /// - public ResponseLogLevel ResponseLogLevel { get; set; } = ResponseLogLevel.None; + [Obsolete("Use the LogConfiguration property to set response logging options instead. This property will be removed in RestAssured.Net 5.0.0")] + public Response.Logging.ResponseLogLevel ResponseLogLevel { get; set; } = Response.Logging.ResponseLogLevel.None; /// /// Setting to configure the for all tests. diff --git a/RestAssured.Net/Logging/LogConfiguration.cs b/RestAssured.Net/Logging/LogConfiguration.cs new file mode 100644 index 0000000..8b8b770 --- /dev/null +++ b/RestAssured.Net/Logging/LogConfiguration.cs @@ -0,0 +1,46 @@ +// +// Copyright 2019 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace RestAssured.Logging +{ + using System.Collections.Generic; + + /// + /// Defines the configuration to be used when logging request or response details. + /// + public class LogConfiguration + { + /// + /// The request logging level for this request. + /// + public RequestLogLevel RequestLogLevel { get; set; } = RequestLogLevel.None; + + /// + /// The response logging level for this request. + /// + public ResponseLogLevel ResponseLogLevel { get; set; } = ResponseLogLevel.None; + + /// + /// A list of sensitive request header and cookie names that should be redacted when logging request details. + /// + public List SensitiveRequestHeadersAndCookies { get; set; } = new List(); + + /// + /// A list of sensitive response header and cookie names that should be redacted when logging response details. + /// + public List SensitiveResponseHeadersAndCookies { get; set; } = new List(); + } +} diff --git a/RestAssured.Net/Logging/RequestLogLevel.cs b/RestAssured.Net/Logging/RequestLogLevel.cs new file mode 100644 index 0000000..8b04afe --- /dev/null +++ b/RestAssured.Net/Logging/RequestLogLevel.cs @@ -0,0 +1,49 @@ +// +// Copyright 2019 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace RestAssured.Logging +{ + /// + /// Contains the different logging levels for request logging. + /// + public enum RequestLogLevel + { + /// + /// Nothing will be logged to the console. + /// + None = 0, + + /// + /// The HTTP method and the endpoint will be logged to the console. + /// + Endpoint = 1, + + /// + /// Request headers will be logged to the console. + /// + Headers = 2, + + /// + /// Request body will be logged to the console. + /// + Body = 3, + + /// + /// All request details will be logged to the console. + /// + All = 4, + } +} diff --git a/RestAssured.Net/Logging/RequestResponseLogger.cs b/RestAssured.Net/Logging/RequestResponseLogger.cs new file mode 100644 index 0000000..ed590dc --- /dev/null +++ b/RestAssured.Net/Logging/RequestResponseLogger.cs @@ -0,0 +1,279 @@ +// +// Copyright 2019 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace RestAssured.Logging +{ + using System; + using System.Collections.Generic; + using System.Net; + using System.Net.Http; + using System.Xml.Linq; + using Newtonsoft.Json; + using RestAssured.Response; + + /// + /// Provides methods for logging request and response details. + /// + internal class RequestResponseLogger + { + private LogConfiguration logConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The to use when logging request and response details. + public RequestResponseLogger(LogConfiguration logConfiguration) + { + this.logConfiguration = logConfiguration; + } + + /// + /// Logs request details to the console. + /// + /// The to be logged to the console. + /// The associated with this request. + public void LogRequest(HttpRequestMessage request, CookieCollection cookieCollection) + { + if (this.logConfiguration.RequestLogLevel >= RequestLogLevel.Endpoint) + { + Console.WriteLine($"{request.Method} {request.RequestUri}"); + } + + if (this.logConfiguration.RequestLogLevel == RequestLogLevel.Headers) + { + LogRequestHeaders(request, this.logConfiguration.SensitiveRequestHeadersAndCookies); + LogRequestCookies(cookieCollection, this.logConfiguration.SensitiveRequestHeadersAndCookies); + } + + if (this.logConfiguration.RequestLogLevel == RequestLogLevel.Body) + { + LogRequestBody(request); + } + + if (this.logConfiguration.RequestLogLevel == RequestLogLevel.All) + { + LogRequestHeaders(request, this.logConfiguration.SensitiveRequestHeadersAndCookies); + LogRequestCookies(cookieCollection, this.logConfiguration.SensitiveRequestHeadersAndCookies); + LogRequestBody(request); + } + } + + /// + /// Logs response details to the console. + /// + /// The to log to the console. + public void LogResponse(VerifiableResponse verifiableResponse) + { + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.OnVerificationFailure) + { + return; + } + + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.OnError) + { + if ((int)verifiableResponse.Response.StatusCode >= 400) + { + LogResponseStatusCode(verifiableResponse.Response); + LogResponseHeaders(verifiableResponse.Response, this.logConfiguration.SensitiveResponseHeadersAndCookies); + LogResponseCookies(verifiableResponse.CookieContainer, this.logConfiguration.SensitiveResponseHeadersAndCookies); + LogResponseBody(verifiableResponse.Response); + LogResponseTime(verifiableResponse.ElapsedTime); + } + + return; + } + + if (this.logConfiguration.ResponseLogLevel > ResponseLogLevel.None) + { + LogResponseStatusCode(verifiableResponse.Response); + } + + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.Headers) + { + LogResponseHeaders(verifiableResponse.Response, this.logConfiguration.SensitiveResponseHeadersAndCookies); + LogResponseCookies(verifiableResponse.CookieContainer, this.logConfiguration.SensitiveResponseHeadersAndCookies); + } + + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.Body) + { + LogResponseBody(verifiableResponse.Response); + } + + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.ResponseTime) + { + LogResponseTime(verifiableResponse.ElapsedTime); + } + + if (this.logConfiguration.ResponseLogLevel == ResponseLogLevel.All) + { + LogResponseHeaders(verifiableResponse.Response, this.logConfiguration.SensitiveResponseHeadersAndCookies); + LogResponseCookies(verifiableResponse.CookieContainer, this.logConfiguration.SensitiveResponseHeadersAndCookies); + LogResponseBody(verifiableResponse.Response); + LogResponseTime(verifiableResponse.ElapsedTime); + } + } + + private static void LogRequestHeaders(HttpRequestMessage request, List sensitiveRequestHeadersAndCookies) + { + if (request.Content != null) + { + Console.WriteLine($"Content-Type: {request.Content.Headers.ContentType}"); + Console.WriteLine($"Content-Length: {request.Content.Headers.ContentLength}"); + } + + foreach (KeyValuePair> header in request.Headers) + { + if (sensitiveRequestHeadersAndCookies.Contains(header.Key)) + { + Console.WriteLine($"{header.Key}: *****"); + } + else + { + Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}"); + } + } + } + + private static void LogRequestCookies(CookieCollection cookieCollection, List sensitiveRequestHeadersAndCookies) + { + foreach (Cookie cookie in cookieCollection) + { + if (sensitiveRequestHeadersAndCookies.Contains(cookie.Name)) + { + Console.WriteLine($"Cookie: {cookie.Name}=*****, Domain: {cookie.Domain}, HTTP-only: {cookie.HttpOnly}, Secure: {cookie.Secure}"); + } + else + { + Console.WriteLine($"Cookie: {cookie.Name}={cookie.Value}, Domain: {cookie.Domain}, HTTP-only: {cookie.HttpOnly}, Secure: {cookie.Secure}"); + } + } + } + + private static void LogRequestBody(HttpRequestMessage request) + { + if (request.Content == null) + { + return; + } + + string requestBodyAsString = request.Content.ReadAsStringAsync().Result; + + if (requestBodyAsString.Equals(string.Empty)) + { + return; + } + + string requestMediaType = request.Content.Headers.ContentType?.MediaType ?? string.Empty; + + if (requestMediaType.Equals(string.Empty) || requestMediaType.Contains("json")) + { + object jsonPayload = JsonConvert.DeserializeObject(requestBodyAsString, typeof(object)) ?? "Could not read request payload"; + Console.WriteLine(JsonConvert.SerializeObject(jsonPayload, Formatting.Indented)); + } + else if (requestMediaType.Contains("xml")) + { + XDocument doc = XDocument.Parse(requestBodyAsString); + Console.WriteLine(doc.ToString()); + } + else + { + Console.WriteLine(requestBodyAsString); + } + } + + private static void LogResponseStatusCode(HttpResponseMessage response) + { + Console.WriteLine($"HTTP {(int)response.StatusCode} ({response.StatusCode})"); + } + + private static void LogResponseHeaders(HttpResponseMessage response, List sensitiveResponseHeadersAndCookies) + { + if (response.Content != null) + { + Console.WriteLine($"Content-Type: {response.Content.Headers.ContentType}"); + Console.WriteLine($"Content-Length: {response.Content.Headers.ContentLength}"); + } + + foreach (KeyValuePair> header in response.Headers) + { + if (sensitiveResponseHeadersAndCookies.Contains(header.Key)) + { + Console.WriteLine($"{header.Key}: *****"); + } + else + { + Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}"); + } + } + } + + private static void LogResponseCookies(CookieContainer cookieContainer, List sensitiveResponseHeadersAndCookies) + { + var cookies = cookieContainer.GetAllCookies().GetEnumerator(); + + while (cookies.MoveNext()) + { + Cookie cookie = (Cookie)cookies.Current; + + if (sensitiveResponseHeadersAndCookies.Contains(cookie.Name)) + { + Console.WriteLine($"Cookie: {cookie.Name}=*****, Domain: {cookie.Domain}, HTTP-only: {cookie.HttpOnly}, Secure: {cookie.Secure}"); + } + else + { + Console.WriteLine($"Cookie: {cookie.Name}={cookie.Value}, Domain: {cookie.Domain}, HTTP-only: {cookie.HttpOnly}, Secure: {cookie.Secure}"); + } + } + } + + private static void LogResponseBody(HttpResponseMessage response) + { + if (response.Content == null) + { + return; + } + + string responseBodyAsString = response.Content.ReadAsStringAsync().Result; + + if (responseBodyAsString.Equals(string.Empty)) + { + return; + } + + string responseMediaType = response.Content.Headers.ContentType?.MediaType ?? string.Empty; + + if (responseMediaType.Equals(string.Empty) || responseMediaType.Contains("json")) + { + object jsonPayload = JsonConvert.DeserializeObject(responseBodyAsString, typeof(object)) ?? "Could not read response payload"; + Console.WriteLine(JsonConvert.SerializeObject(jsonPayload, Formatting.Indented)); + } + else if (responseMediaType.Contains("xml")) + { + XDocument doc = XDocument.Parse(responseBodyAsString); + Console.WriteLine(doc.ToString()); + } + else + { + Console.WriteLine(responseBodyAsString); + } + } + + private static void LogResponseTime(TimeSpan elapsedTime) + { + Console.WriteLine($"Response time: {elapsedTime.TotalMilliseconds} ms"); + } + } +} diff --git a/RestAssured.Net/Logging/ResponseLogLevel.cs b/RestAssured.Net/Logging/ResponseLogLevel.cs new file mode 100644 index 0000000..c0bd356 --- /dev/null +++ b/RestAssured.Net/Logging/ResponseLogLevel.cs @@ -0,0 +1,61 @@ +// +// Copyright 2019 the original author or authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace RestAssured.Logging +{ + /// + /// Contains the different logging levels for request logging. + /// + public enum ResponseLogLevel + { + /// + /// Nothing will be logged to the console. + /// + None = 0, + + /// + /// Response headers will be logged to the console. + /// + Headers = 1, + + /// + /// Response body will be logged to the console. + /// + Body = 2, + + /// + /// Response time will be logged to the console. + /// + ResponseTime = 3, + + /// + /// All response details will be logged to the console. + /// + All = 4, + + /// + /// The entire response will be logged to the console + /// if the response status code is 4xx or 5xx. + /// + OnError = 5, + + /// + /// The entire response will be logged to the console + /// if any of the response verifications fails. + /// + OnVerificationFailure = 6, + } +} diff --git a/RestAssured.Net/Request/Builders/RequestSpecBuilder.cs b/RestAssured.Net/Request/Builders/RequestSpecBuilder.cs index 803beef..189e105 100644 --- a/RestAssured.Net/Request/Builders/RequestSpecBuilder.cs +++ b/RestAssured.Net/Request/Builders/RequestSpecBuilder.cs @@ -23,6 +23,7 @@ namespace RestAssured.Request.Builders using System.Net.Http.Headers; using System.Text; using Newtonsoft.Json; + using RestAssured.Logging; using RestAssured.Request.Exceptions; using RestAssured.Request.Logging; @@ -47,7 +48,8 @@ public class RequestSpecBuilder private readonly string? contentTypeHeader = null; private readonly Encoding? contentEncoding = null; private readonly bool disableSslCertificateValidation = false; - private readonly RequestLogLevel requestLogLevel = RequestLogLevel.None; + private readonly LogConfiguration logConfiguration = new LogConfiguration(); + private readonly Logging.RequestLogLevel requestLogLevel = Logging.RequestLogLevel.None; private readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings(); private readonly List sensitiveRequestHeadersAndCookies = new List(); private readonly HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead; @@ -57,7 +59,7 @@ public class RequestSpecBuilder /// public RequestSpecBuilder() { - this.requestSpecification = new RequestSpecification(this.scheme, this.host, this.port, this.baseUri, this.basePath, this.queryParams, this.timeout, this.userAgent, this.proxy, this.headers, this.authenticationHeader, this.contentTypeHeader, this.contentEncoding, this.disableSslCertificateValidation, this.requestLogLevel, this.jsonSerializerSettings, this.sensitiveRequestHeadersAndCookies, this.httpCompletionOption); + this.requestSpecification = new RequestSpecification(this.scheme, this.host, this.port, this.baseUri, this.basePath, this.queryParams, this.timeout, this.userAgent, this.proxy, this.headers, this.authenticationHeader, this.contentTypeHeader, this.contentEncoding, this.disableSslCertificateValidation, this.logConfiguration, this.requestLogLevel, this.jsonSerializerSettings, this.sensitiveRequestHeadersAndCookies, this.httpCompletionOption); } /// @@ -257,12 +259,24 @@ public RequestSpecBuilder WithDisabledSslCertificateValidation() return this; } + /// + /// Sets the log configuration to the specified values. + /// + /// The to apply when logging request and response details. + /// The current object. + public RequestSpecBuilder WithLogConfiguration(LogConfiguration logConfiguration) + { + this.requestSpecification.LogConfiguration = logConfiguration; + return this; + } + /// /// Sets the request log level to the specified value. /// /// The to apply to the requests. /// The current object. - public RequestSpecBuilder WithRequestLogLevel(RequestLogLevel requestLogLevel) + [Obsolete("Please use WithLogConfiguration(LogConfiguration logConfiguration) instead. This method will be removed in RestAssured.Net 5.0.0")] + public RequestSpecBuilder WithRequestLogLevel(Logging.RequestLogLevel requestLogLevel) { this.requestSpecification.RequestLogLevel = requestLogLevel; return this; diff --git a/RestAssured.Net/Request/Builders/RequestSpecification.cs b/RestAssured.Net/Request/Builders/RequestSpecification.cs index c18e4bd..71f20b3 100644 --- a/RestAssured.Net/Request/Builders/RequestSpecification.cs +++ b/RestAssured.Net/Request/Builders/RequestSpecification.cs @@ -22,6 +22,7 @@ namespace RestAssured.Request.Builders using System.Net.Http.Headers; using System.Text; using Newtonsoft.Json; + using RestAssured.Logging; using RestAssured.Request.Logging; /// @@ -102,7 +103,12 @@ public class RequestSpecification /// /// The value for the request logging level when sending the request. /// - public RequestLogLevel RequestLogLevel { get; set; } + public Logging.RequestLogLevel RequestLogLevel { get; set; } + + /// + /// The configuration to be used when logging request and response details. + /// + public LogConfiguration LogConfiguration { get; set; } /// /// Can be used to provide custom serialization settings when working with JSON request payloads. @@ -136,11 +142,12 @@ public class RequestSpecification /// The Content-Type header value to set for this request. /// The content encoding to use in this request. /// Flag indicating whether or not to disable SSL certificate validation. + /// The logging configuration to use when logging request and response details. /// The request log level to use in this request. /// The JSON serializer settings to use in this request. /// A list of sensitive header and cookie names (to be masked when logging request details. /// Indicates whether the HttpClient should wait for the request body to be sent. - public RequestSpecification(string scheme, string host, int port, string baseUri, string basePath, IEnumerable> queryParams, TimeSpan? timeout, ProductInfoHeaderValue? userAgent, IWebProxy proxy, Dictionary headers, AuthenticationHeaderValue authenticationHeader, string contentType, Encoding contentEncoding, bool disableSslCertificateValidation, RequestLogLevel requestLogLevel, JsonSerializerSettings jsonSerializerSettings, List sensitiveRequestHeadersAndCookies, HttpCompletionOption httpCompletionOption) + public RequestSpecification(string scheme, string host, int port, string baseUri, string basePath, IEnumerable> queryParams, TimeSpan? timeout, ProductInfoHeaderValue? userAgent, IWebProxy proxy, Dictionary headers, AuthenticationHeaderValue authenticationHeader, string contentType, Encoding contentEncoding, bool disableSslCertificateValidation, LogConfiguration logConfiguration, Logging.RequestLogLevel requestLogLevel, JsonSerializerSettings jsonSerializerSettings, List sensitiveRequestHeadersAndCookies, HttpCompletionOption httpCompletionOption) { this.Scheme = scheme; this.HostName = host; @@ -156,6 +163,7 @@ public RequestSpecification(string scheme, string host, int port, string baseUri this.ContentType = contentType; this.ContentEncoding = contentEncoding; this.DisableSslCertificateValidation = disableSslCertificateValidation; + this.LogConfiguration = logConfiguration; this.RequestLogLevel = requestLogLevel; this.JsonSerializerSettings = jsonSerializerSettings; this.SensitiveRequestHeadersAndCookies = sensitiveRequestHeadersAndCookies; diff --git a/RestAssured.Net/Request/ExecutableRequest.cs b/RestAssured.Net/Request/ExecutableRequest.cs index 6cf36ea..3303b22 100644 --- a/RestAssured.Net/Request/ExecutableRequest.cs +++ b/RestAssured.Net/Request/ExecutableRequest.cs @@ -29,11 +29,10 @@ namespace RestAssured.Request using Microsoft.AspNetCore.WebUtilities; using Newtonsoft.Json; using RestAssured.Configuration; + using RestAssured.Logging; using RestAssured.Request.Builders; using RestAssured.Request.Exceptions; - using RestAssured.Request.Logging; using RestAssured.Response; - using RestAssured.Response.Logging; using Stubble.Core; using Stubble.Core.Builders; using Stubble.Core.Classes; @@ -66,12 +65,17 @@ public class ExecutableRequest : IDisposable /// /// The request logging level for this request. /// - internal RequestLogLevel RequestLoggingLevel { get; set; } + internal Logging.RequestLogLevel RequestLoggingLevel { get; set; } /// /// The response logging level for this request. /// - internal ResponseLogLevel ResponseLoggingLevel { get; set; } + internal RestAssured.Response.Logging.ResponseLogLevel ResponseLoggingLevel { get; set; } + + /// + /// The configuration settings to use when logging request and response details. + /// + internal LogConfiguration LogConfiguration { get; set; } /// /// Initializes a new instance of the class. @@ -448,13 +452,25 @@ public ExecutableRequest JsonSerializerSettings(JsonSerializerSettings jsonSeria return this; } + /// + /// Sets the configuration for logging request and response details to the specified values. + /// + /// The log configuration settings to use. + /// The current object. + public ExecutableRequest Log(LogConfiguration logConfiguration) + { + this.LogConfiguration = logConfiguration; + return this; + } + /// /// Logs request details to the standard output. /// /// The desired request log level. /// The names of the request headers or cookies to be masked when logging. /// The current object. - public ExecutableRequest Log(RequestLogLevel requestLogLevel, List? sensitiveHeaderOrCookieNames = null) + [Obsolete("Use Log(LogConfiguration logConfiguration) instead. This method will be removed in RestAssured.Net 5.0.0")] + public ExecutableRequest Log(Logging.RequestLogLevel requestLogLevel, List? sensitiveHeaderOrCookieNames = null) { this.RequestLoggingLevel = requestLogLevel; @@ -691,13 +707,24 @@ private VerifiableResponse Send(HttpMethod httpMethod, string endpoint) this.sensitiveRequestHeadersAndCookies.AddRange(this.requestSpecification.SensitiveRequestHeadersAndCookies); } - RequestLogger.LogToConsole(this.request, this.RequestLoggingLevel, this.cookieCollection, this.sensitiveRequestHeadersAndCookies); + var legacyLogConfiguration = new LogConfiguration + { + RequestLogLevel = (RequestLogLevel)this.RequestLoggingLevel, + ResponseLogLevel = (ResponseLogLevel)this.ResponseLoggingLevel, + SensitiveRequestHeadersAndCookies = this.sensitiveRequestHeadersAndCookies, + SensitiveResponseHeadersAndCookies = new List(), + }; + + RequestResponseLogger logger = new RequestResponseLogger(this.LogConfiguration ?? legacyLogConfiguration); + + // RequestLogger.LogToConsole(this.request, this.RequestLoggingLevel, this.cookieCollection, this.sensitiveRequestHeadersAndCookies); + logger.LogRequest(this.request, this.cookieCollection); try { Task task = httpRequestProcessor.Send(this.request, this.cookieCollection, this.httpCompletionOption); VerifiableResponse verifiableResponse = task.Result; - verifiableResponse.Log(this.ResponseLoggingLevel); + logger.LogResponse(verifiableResponse); return verifiableResponse; } catch (AggregateException ae) diff --git a/RestAssured.Net/Request/Logging/RequestLogLevel.cs b/RestAssured.Net/Request/Logging/RequestLogLevel.cs index dd5564b..1b416ef 100644 --- a/RestAssured.Net/Request/Logging/RequestLogLevel.cs +++ b/RestAssured.Net/Request/Logging/RequestLogLevel.cs @@ -16,9 +16,12 @@ namespace RestAssured.Request.Logging { + using System; + /// /// Contains the different logging levels for request logging. /// + [Obsolete("Use the RequestLogLevel enum in the RestAssured.Logging namespace instead. This enum will be removed in RestAssured.Net 5.0.0")] public enum RequestLogLevel { /// diff --git a/RestAssured.Net/Request/Logging/RequestLogger.cs b/RestAssured.Net/Request/Logging/RequestLogger.cs index 29c2ae3..58d0787 100644 --- a/RestAssured.Net/Request/Logging/RequestLogger.cs +++ b/RestAssured.Net/Request/Logging/RequestLogger.cs @@ -18,10 +18,8 @@ namespace RestAssured.Request.Logging { using System; using System.Collections.Generic; - using System.Linq; using System.Net; using System.Net.Http; - using System.Threading.Tasks; using System.Xml.Linq; using Newtonsoft.Json; diff --git a/RestAssured.Net/Response/Logging/ResponseLogLevel.cs b/RestAssured.Net/Response/Logging/ResponseLogLevel.cs index 0fc0a74..4499a05 100644 --- a/RestAssured.Net/Response/Logging/ResponseLogLevel.cs +++ b/RestAssured.Net/Response/Logging/ResponseLogLevel.cs @@ -16,9 +16,12 @@ namespace RestAssured.Response.Logging { + using System; + /// /// Contains the different logging levels for request logging. /// + [Obsolete("Use the ResponseLogLevel enum in the RestAssured.Logging namespace instead. This enum will be removed in RestAssured.Net 5.0.0")] public enum ResponseLogLevel { /// diff --git a/RestAssured.Net/Response/VerifiableResponse.cs b/RestAssured.Net/Response/VerifiableResponse.cs index abc3dce..13c41b6 100644 --- a/RestAssured.Net/Response/VerifiableResponse.cs +++ b/RestAssured.Net/Response/VerifiableResponse.cs @@ -39,9 +39,20 @@ namespace RestAssured.Response /// public class VerifiableResponse { - private readonly HttpResponseMessage response; - private readonly CookieContainer cookieContainer; - private readonly TimeSpan elapsedTime; + /// + /// The wrapped contained in this . + /// + public HttpResponseMessage Response { internal get; init; } + + /// + /// The associated with the current . + /// + public CookieContainer CookieContainer { internal get; init; } + + /// + /// The elapsed between sending a request and receiving this . + /// + public TimeSpan ElapsedTime { internal get; init; } private bool logOnVerificationFailure = false; private JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings(); @@ -51,13 +62,13 @@ public class VerifiableResponse /// Initializes a new instance of the class. /// /// The returned by the HTTP client. - /// The used by the HTTP client. + /// The used by the HTTP client. /// The time elapsed between sending the request and receiving the response. public VerifiableResponse(HttpResponseMessage response, CookieContainer cookieContainer, TimeSpan elapsedTime) { - this.response = response; - this.cookieContainer = cookieContainer; - this.elapsedTime = elapsedTime; + this.Response = response; + this.CookieContainer = cookieContainer; + this.ElapsedTime = elapsedTime; } /// @@ -95,9 +106,9 @@ public VerifiableResponse And() /// Thrown when the actual status code does not match the expected one. public VerifiableResponse StatusCode(int expectedStatusCode) { - if (expectedStatusCode != (int)this.response.StatusCode) + if (expectedStatusCode != (int)this.Response.StatusCode) { - this.FailVerification($"Expected status code to be {expectedStatusCode}, but was {(int)this.response.StatusCode}"); + this.FailVerification($"Expected status code to be {expectedStatusCode}, but was {(int)this.Response.StatusCode}"); } return this; @@ -111,9 +122,9 @@ public VerifiableResponse StatusCode(int expectedStatusCode) /// Thrown when the actual status code does not match the expected one. public VerifiableResponse StatusCode(HttpStatusCode expectedStatusCode) { - if (!expectedStatusCode.Equals(this.response.StatusCode)) + if (!expectedStatusCode.Equals(this.Response.StatusCode)) { - this.FailVerification($"Expected status code to be {expectedStatusCode}, but was {this.response.StatusCode}"); + this.FailVerification($"Expected status code to be {expectedStatusCode}, but was {this.Response.StatusCode}"); } return this; @@ -127,9 +138,9 @@ public VerifiableResponse StatusCode(HttpStatusCode expectedStatusCode) /// Thrown when the actual status code does not match the expected one. public VerifiableResponse StatusCode(IMatcher matcher) { - if (!matcher.Matches((int)this.response.StatusCode)) + if (!matcher.Matches((int)this.Response.StatusCode)) { - this.FailVerification($"Expected response status code to match '{matcher}', but was {(int)this.response.StatusCode}"); + this.FailVerification($"Expected response status code to match '{matcher}', but was {(int)this.Response.StatusCode}"); } return this; @@ -144,7 +155,7 @@ public VerifiableResponse StatusCode(IMatcher matcher) /// Thrown when the header does not exist, or when the header value does not equal the supplied expected value. public VerifiableResponse Header(string name, string expectedValue) { - if (!this.response.Headers.TryGetValues(name, out IEnumerable? values)) + if (!this.Response.Headers.TryGetValues(name, out IEnumerable? values)) { this.FailVerification($"Expected header with name '{name}' to be in the response, but it could not be found."); } @@ -168,7 +179,7 @@ public VerifiableResponse Header(string name, string expectedValue) /// Thrown when the header does not exist, or when the header value does not equal the supplied expected value. public VerifiableResponse Header(string name, IMatcher matcher) { - if (this.response.Headers.TryGetValues(name, out IEnumerable? values)) + if (this.Response.Headers.TryGetValues(name, out IEnumerable? values)) { string firstValue = values.First(); @@ -193,7 +204,7 @@ public VerifiableResponse Header(string name, IMatcher matcher) /// Thrown when the "Content-Type" header does not exist, or when the header value does not equal the supplied expected value. public VerifiableResponse ContentType(string expectedContentType) { - MediaTypeHeaderValue? actualContentType = this.response.Content.Headers.ContentType; + MediaTypeHeaderValue? actualContentType = this.Response.Content.Headers.ContentType; if (actualContentType == null) { @@ -216,7 +227,7 @@ public VerifiableResponse ContentType(string expectedContentType) /// Thrown when the "Content-Type" header does not exist, or when the header value does not equal the supplied expected value. public VerifiableResponse ContentType(IMatcher matcher) { - MediaTypeHeaderValue? actualContentType = this.response.Content.Headers.ContentType; + MediaTypeHeaderValue? actualContentType = this.Response.Content.Headers.ContentType; if (actualContentType == null) { @@ -239,7 +250,7 @@ public VerifiableResponse ContentType(IMatcher matcher) /// The current object. public VerifiableResponse Cookie(string name, IMatcher matcher) { - var cookies = this.cookieContainer.GetAllCookies().GetEnumerator(); + var cookies = this.CookieContainer.GetAllCookies().GetEnumerator(); while (cookies.MoveNext()) { @@ -268,7 +279,7 @@ public VerifiableResponse Cookie(string name, IMatcher matcher) /// Thrown when the actual response body does not match the expected one. public VerifiableResponse Body(string expectedResponseBody) { - string actualResponseBody = this.response.Content.ReadAsStringAsync().Result; + string actualResponseBody = this.Response.Content.ReadAsStringAsync().Result; if (!actualResponseBody.Equals(expectedResponseBody)) { @@ -286,7 +297,7 @@ public VerifiableResponse Body(string expectedResponseBody) /// Thrown when the actual response body does not match the expected one. public VerifiableResponse Body(IMatcher matcher) { - string actualResponseBody = this.response.Content.ReadAsStringAsync().Result; + string actualResponseBody = this.Response.Content.ReadAsStringAsync().Result; if (!matcher.Matches(actualResponseBody)) { @@ -306,7 +317,7 @@ public VerifiableResponse Body(IMatcher matcher) /// The current object. public VerifiableResponse Body(string path, IMatcher matcher, VerifyAs verifyAs = VerifyAs.UseResponseContentTypeHeaderValue) { - string responseBodyAsString = this.response.Content.ReadAsStringAsync().Result; + string responseBodyAsString = this.Response.Content.ReadAsStringAsync().Result; string? responseMediaType = string.Empty; @@ -314,7 +325,7 @@ public VerifiableResponse Body(string path, IMatcher matcher, VerifyAs ver { case VerifyAs.UseResponseContentTypeHeaderValue: { - responseMediaType = this.response.Content.Headers.ContentType?.MediaType; + responseMediaType = this.Response.Content.Headers.ContentType?.MediaType; break; } @@ -433,7 +444,7 @@ public VerifiableResponse Body(string path, IMatcher> matcher, { List elementValues = new List(); - string responseBodyAsString = this.response.Content.ReadAsStringAsync().Result; + string responseBodyAsString = this.Response.Content.ReadAsStringAsync().Result; string? responseMediaType = string.Empty; @@ -441,7 +452,7 @@ public VerifiableResponse Body(string path, IMatcher> matcher, { case VerifyAs.UseResponseContentTypeHeaderValue: { - responseMediaType = this.response.Content.Headers.ContentType?.MediaType; + responseMediaType = this.Response.Content.Headers.ContentType?.MediaType; break; } @@ -568,14 +579,14 @@ public VerifiableResponse MatchesJsonSchema(string jsonSchema) /// Thrown when "Content-Type" doesn't contain "json" or when body doesn't match JSON schema supplied. public VerifiableResponse MatchesJsonSchema(JsonSchema jsonSchema) { - string responseMediaType = this.response.Content.Headers.ContentType?.MediaType ?? string.Empty; + string responseMediaType = this.Response.Content.Headers.ContentType?.MediaType ?? string.Empty; if (!responseMediaType.Contains("json")) { this.FailVerification($"Expected response Content-Type header to contain 'json', but was '{responseMediaType}'"); } - string responseBodyAsString = this.response.Content.ReadAsStringAsync().Result; + string responseBodyAsString = this.Response.Content.ReadAsStringAsync().Result; ICollection schemaValidationErrors = jsonSchema.Validate(responseBodyAsString); @@ -615,7 +626,7 @@ public VerifiableResponse MatchesXsd(string xsd) /// The current object. public VerifiableResponse MatchesXsd(XmlSchemaSet schemas) { - string responseMediaType = this.response.Content.Headers.ContentType?.MediaType ?? string.Empty; + string responseMediaType = this.Response.Content.Headers.ContentType?.MediaType ?? string.Empty; if (!responseMediaType.Contains("xml")) { @@ -626,7 +637,7 @@ public VerifiableResponse MatchesXsd(XmlSchemaSet schemas) settings.ValidationType = ValidationType.Schema; settings.Schemas = schemas; - string responseXmlAsString = this.response.Content.ReadAsStringAsync().Result; + string responseXmlAsString = this.Response.Content.ReadAsStringAsync().Result; XmlReader reader = XmlReader.Create(new StringReader(responseXmlAsString), settings); try @@ -649,7 +660,7 @@ public VerifiableResponse MatchesXsd(XmlSchemaSet schemas) /// The current object. public VerifiableResponse MatchesInlineDtd() { - string responseMediaType = this.response.Content.Headers.ContentType?.MediaType ?? string.Empty; + string responseMediaType = this.Response.Content.Headers.ContentType?.MediaType ?? string.Empty; if (!responseMediaType.Contains("xml")) { @@ -660,7 +671,7 @@ public VerifiableResponse MatchesInlineDtd() settings.DtdProcessing = DtdProcessing.Parse; settings.ValidationType = ValidationType.DTD; - string responseXmlAsString = this.response.Content.ReadAsStringAsync().Result; + string responseXmlAsString = this.Response.Content.ReadAsStringAsync().Result; XmlReader reader = XmlReader.Create(new StringReader(responseXmlAsString), settings); try @@ -684,9 +695,9 @@ public VerifiableResponse MatchesInlineDtd() /// The current object. public VerifiableResponse ResponseTime(IMatcher matcher) { - if (!matcher.Matches(this.elapsedTime)) + if (!matcher.Matches(this.ElapsedTime)) { - this.FailVerification($"Expected response time to match '{matcher}' but was '{this.elapsedTime}'"); + this.FailVerification($"Expected response time to match '{matcher}' but was '{this.ElapsedTime}'"); } return this; @@ -699,7 +710,7 @@ public VerifiableResponse ResponseTime(IMatcher matcher) /// The current object. public VerifiableResponse ResponseBodyLength(IMatcher matcher) { - string responseContentAsString = this.response.Content.ReadAsStringAsync().Result; + string responseContentAsString = this.Response.Content.ReadAsStringAsync().Result; if (!matcher.Matches(responseContentAsString.Length)) { @@ -728,7 +739,7 @@ public VerifiableResponse UsingJsonSerializerSettings(JsonSerializerSettings jso /// The deserialized response object. public object DeserializeTo(Type type, DeserializeAs deserializeAs = DeserializeAs.UseResponseContentTypeHeaderValue) { - return Deserializer.DeserializeResponseInto(this.response, type, deserializeAs, this.jsonSerializerSettings); + return Deserializer.DeserializeResponseInto(this.Response, type, deserializeAs, this.jsonSerializerSettings); } /// @@ -737,6 +748,7 @@ public object DeserializeTo(Type type, DeserializeAs deserializeAs = Deserialize /// The required log level. /// The names of the response headers or cookies to be masked when logging. /// The current object. + [Obsolete("Use Log(LogConfiguration logConfiguration) in ExecutableRequest instead. This method will be removed in RestAssured.Net 5.0.0")] public VerifiableResponse Log(ResponseLogLevel responseLogLevel, List? sensitiveHeaderOrCookieNames = null) { if (responseLogLevel == ResponseLogLevel.OnVerificationFailure) @@ -745,7 +757,7 @@ public VerifiableResponse Log(ResponseLogLevel responseLogLevel, List? s return this; } - ResponseLogger.Log(this.response, this.cookieContainer, responseLogLevel, sensitiveHeaderOrCookieNames ?? new List(), this.elapsedTime); + ResponseLogger.Log(this.Response, this.CookieContainer, responseLogLevel, sensitiveHeaderOrCookieNames ?? new List(), this.ElapsedTime); return this; } @@ -755,14 +767,14 @@ public VerifiableResponse Log(ResponseLogLevel responseLogLevel, List? s /// An object from which values can then be extracted. public ExtractableResponse Extract() { - return new ExtractableResponse(this.response, this.cookieContainer, this.elapsedTime); + return new ExtractableResponse(this.Response, this.CookieContainer, this.ElapsedTime); } private void FailVerification(string exceptionMessage) { if (this.logOnVerificationFailure) { - ResponseLogger.Log(this.response, this.cookieContainer, ResponseLogLevel.All, this.sensitiveResponseHeadersAndCookies, this.elapsedTime); + ResponseLogger.Log(this.Response, this.CookieContainer, ResponseLogLevel.All, this.sensitiveResponseHeadersAndCookies, this.ElapsedTime); } throw new ResponseVerificationException(exceptionMessage);