diff --git a/Ocelot.sln b/Ocelot.sln index e943d369..60369059 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -9,23 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore .gitignore = .gitignore - build-and-release-unstable.ps1 = build-and-release-unstable.ps1 - build-and-run-tests.ps1 = build-and-run-tests.ps1 build.cake = build.cake build.ps1 = build.ps1 codeanalysis.ruleset = codeanalysis.ruleset - docker-compose.yaml = docker-compose.yaml - Dockerfile = Dockerfile GitVersion.yml = GitVersion.yml - global.json = global.json LICENSE.md = LICENSE.md README.md = README.md - release.ps1 = release.ps1 ReleaseNotes.md = ReleaseNotes.md - run-acceptance-tests.ps1 = run-acceptance-tests.ps1 - run-benchmarks.ps1 = run-benchmarks.ps1 - run-unit-tests.ps1 = run-unit-tests.ps1 - version.ps1 = version.ps1 EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" diff --git a/samples/OcelotBasic/Properties/launchSettings.json b/samples/OcelotBasic/Properties/launchSettings.json new file mode 100644 index 00000000..b500ae57 --- /dev/null +++ b/samples/OcelotBasic/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55029/", + "sslPort": 44390 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "OcelotBasic": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs index eba79769..eaaa4dab 100644 --- a/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs +++ b/src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Ocelot.Configuration.Builder { @@ -7,6 +8,7 @@ namespace Ocelot.Configuration.Builder private bool _enableRateLimiting; private string _clientIdHeader; private List _clientWhitelist; + private Func> _getClientWhitelist; private bool _disableRateLimitHeaders; private string _quotaExceededMessage; private string _rateLimitCounterPrefix; @@ -19,15 +21,15 @@ namespace Ocelot.Configuration.Builder return this; } - public RateLimitOptionsBuilder WithClientIdHeader(string clientIdheader) + public RateLimitOptionsBuilder WithClientIdHeader(string clientIdHeader) { - _clientIdHeader = clientIdheader; + _clientIdHeader = clientIdHeader; return this; } - public RateLimitOptionsBuilder WithClientWhiteList(List clientWhitelist) + public RateLimitOptionsBuilder WithClientWhiteList(Func> getClientWhitelist) { - _clientWhitelist = clientWhitelist; + _getClientWhitelist = getClientWhitelist; return this; } @@ -63,9 +65,9 @@ namespace Ocelot.Configuration.Builder public RateLimitOptions Build() { - return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _clientWhitelist, - _disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix, + return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _getClientWhitelist, + _disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix, _rateLimitRule, _httpStatusCode); } } -} +} diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs index 8f300dd2..ba167bfe 100644 --- a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs @@ -11,7 +11,7 @@ namespace Ocelot.Configuration.Creator { return new RateLimitOptionsBuilder() .WithClientIdHeader(globalConfiguration.RateLimitOptions.ClientIdHeader) - .WithClientWhiteList(fileRateLimitRule.ClientWhitelist) + .WithClientWhiteList(() => fileRateLimitRule.ClientWhitelist) .WithDisableRateLimitHeaders(globalConfiguration.RateLimitOptions.DisableRateLimitHeaders) .WithEnableRateLimiting(fileRateLimitRule.EnableRateLimiting) .WithHttpStatusCode(globalConfiguration.RateLimitOptions.HttpStatusCode) diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs index db1da8eb..28472825 100644 --- a/src/Ocelot/Configuration/RateLimitOptions.cs +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Ocelot.Configuration { @@ -7,12 +8,14 @@ namespace Ocelot.Configuration /// public class RateLimitOptions { - public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List clientWhitelist, bool disableRateLimitHeaders, + private readonly Func> _getClientWhitelist; + + public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func> getClientWhitelist, bool disableRateLimitHeaders, string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) { - EnableRateLimiting = enbleRateLimiting; + EnableRateLimiting = enableRateLimiting; ClientIdHeader = clientIdHeader; - ClientWhitelist = clientWhitelist ?? new List(); + _getClientWhitelist = getClientWhitelist; DisableRateLimitHeaders = disableRateLimitHeaders; QuotaExceededMessage = quotaExceededMessage; RateLimitCounterPrefix = rateLimitCounterPrefix; @@ -22,18 +25,21 @@ namespace Ocelot.Configuration public RateLimitRule RateLimitRule { get; private set; } - public List ClientWhitelist { get; private set; } + /// + /// Gets the list of white listed clients + /// + public List ClientWhitelist { get => _getClientWhitelist(); } /// /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId /// - public string ClientIdHeader { get; private set; } - + public string ClientIdHeader { get; private set; } + /// /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) /// - public int HttpStatusCode { get; private set; } - + public int HttpStatusCode { get; private set; } + /// /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. /// If none specified the default will be: @@ -44,8 +50,8 @@ namespace Ocelot.Configuration /// /// Gets or sets the counter prefix, used to compose the rate limit counter cache key /// - public string RateLimitCounterPrefix { get; private set; } - + public string RateLimitCounterPrefix { get; private set; } + /// /// Enables endpoint rate limiting based URL path and HTTP verb /// @@ -56,4 +62,4 @@ namespace Ocelot.Configuration /// public bool DisableRateLimitHeaders { get; private set; } } -} +} diff --git a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs index 100d01f4..f2fbad2d 100644 --- a/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExeptionToErrorMapper.cs @@ -1,6 +1,6 @@ namespace Ocelot.Requester { - using Errors; + using Ocelot.Errors; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -23,7 +23,7 @@ namespace Ocelot.Requester return _mappers[type](exception); } - if (type == typeof(OperationCanceledException)) + if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException))) { return new RequestCanceledError(exception.Message); } diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index 92d3128e..a48aa84d 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -1,6 +1,9 @@ +using System.Net; +using System.Net.Http; using Ocelot.Logging; using Ocelot.Middleware; using System.Threading.Tasks; +using Ocelot.Responses; namespace Ocelot.Requester.Middleware { @@ -22,6 +25,8 @@ namespace Ocelot.Requester.Middleware { var response = await _requester.GetResponse(context); + CreateLogBasedOnResponse(response); + if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); @@ -36,5 +41,19 @@ namespace Ocelot.Requester.Middleware await _next.Invoke(context); } + + private void CreateLogBasedOnResponse(Response response) + { + if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) + { + Logger.LogInformation( + $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + } + else if (response.Data?.StatusCode >= HttpStatusCode.BadRequest) + { + Logger.LogWarning( + $"{(int) response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + } + } } } diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 4782eeb5..356561bb 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -1,74 +1,75 @@ - - - 0.0.0-dev - netcoreapp3.1 - Ocelot.AcceptanceTests - Exe - Ocelot.AcceptanceTests - true - osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 - false - false - false - ..\..\codeanalysis.ruleset - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - - - - - - - - - - - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - + + + 0.0.0-dev + netcoreapp3.1 + Ocelot.AcceptanceTests + Exe + Ocelot.AcceptanceTests + true + osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64 + false + false + false + ..\..\codeanalysis.ruleset + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs index ac2b6be8..cd5be7a7 100644 --- a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs +++ b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs @@ -34,7 +34,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 50092, + Port = 51092, } }, UpstreamPathTemplate = "/{everything}", @@ -43,7 +43,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:50092", "/inline.132.bundle.js", 304)) + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51092", "/inline.132.bundle.js", 304)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index f72efff8..fd04af61 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -20,7 +20,8 @@ [Fact] public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() - { + { + var configuration = new FileConfiguration { ReRoutes = new List @@ -49,6 +50,39 @@ .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) .BDDfy(); + } + + [Fact] + public void should_log_warning_if_downstream_service_returns_internal_server_error() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 53876, + }, + }, + DownstreamScheme = "http", + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithLogger()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenWarningShouldBeLogged()) + .BDDfy(); } private void GivenThereIsAServiceRunningOn(string url) diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 5bddcd92..9bfb2091 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -10,12 +10,14 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Moq; using Newtonsoft.Json; using Ocelot.Cache.CacheManager; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; + using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Middleware.Multiplexer; using Ocelot.Provider.Consul; @@ -1120,5 +1122,60 @@ _ocelotClient = _ocelotServer.CreateClient(); } + + public void GivenOcelotIsRunningWithLogger() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); + config.AddJsonFile("ocelot.json", false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddSingleton(); + }) + .Configure(app => + { + app.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void ThenWarningShouldBeLogged() + { + MockLoggerFactory loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); + loggerFactory.Verify(); + } + + internal class MockLoggerFactory : IOcelotLoggerFactory + { + private Mock _logger; + + public IOcelotLogger CreateLogger() + { + if (_logger == null) + { + _logger = new Mock(); + _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); + } + return _logger.Object; + } + + public void Verify() + { + _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once); + } + } } } diff --git a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs index 75e9cfca..a2bc1957 100644 --- a/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RateLimitOptionsCreatorTests.cs @@ -50,7 +50,7 @@ namespace Ocelot.UnitTests.Configuration }; var expected = new RateLimitOptionsBuilder() .WithClientIdHeader("ClientIdHeader") - .WithClientWhiteList(fileReRoute.RateLimitOptions.ClientWhitelist) + .WithClientWhiteList(() => fileReRoute.RateLimitOptions.ClientWhitelist) .WithDisableRateLimitHeaders(true) .WithEnableRateLimiting(true) .WithHttpStatusCode(200) diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 2b54bbfa..d0f69cec 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -49,15 +49,15 @@ namespace Ocelot.UnitTests.RateLimit [Fact] public void should_call_middleware_and_ratelimiting() - { - var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); + { + var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); var downstreamReRoute = new DownstreamReRouteBuilder() .WithEnableRateLimiting(true) - .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", new List(), false, "", "", new RateLimitRule("1s", 100, 3), 429)) + .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List(), false, "", "", new RateLimitRule("1s", 100, 3), 429)) .WithUpstreamHttpMethod(new List { "Get" }) .WithUpstreamPathTemplate(upstreamTemplate) - .Build(); + .Build(); var reRoute = new ReRouteBuilder() .WithDownstreamReRoute(downstreamReRoute) @@ -82,7 +82,7 @@ namespace Ocelot.UnitTests.RateLimit .WithDownstreamReRoute(new DownstreamReRouteBuilder() .WithEnableRateLimiting(true) .WithRateLimitOptions( - new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new RateLimitRule("1s", 100, 3), 429)) + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", () => new List() { "ocelotclient2" }, false, "", "", new RateLimitRule("1s", 100, 3), 429)) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) @@ -102,8 +102,8 @@ namespace Ocelot.UnitTests.RateLimit private void WhenICallTheMiddlewareMultipleTime(int times) { - var clientId = "ocelotclient1"; - + var clientId = "ocelotclient1"; + for (int i = 0; i < times; i++) { var request = new HttpRequestMessage(new HttpMethod("GET"), _url); @@ -117,8 +117,8 @@ namespace Ocelot.UnitTests.RateLimit private void WhenICallTheMiddlewareWithWhiteClient() { - var clientId = "ocelotclient2"; - + var clientId = "ocelotclient2"; + for (int i = 0; i < 10; i++) { var request = new HttpRequestMessage(new HttpMethod("GET"), _url); @@ -127,10 +127,10 @@ namespace Ocelot.UnitTests.RateLimit _downstreamContext.HttpContext.Request.Headers.TryAdd("ClientId", clientId); _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); - _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; - } - } - + _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; + } + } + private void ThenresponseStatusCodeIs429() { _responseStatusCode.ShouldBe(429); @@ -145,7 +145,7 @@ namespace Ocelot.UnitTests.RateLimit internal class FakeStream : Stream { public override void Flush() - { + { //do nothing //throw new System.NotImplementedException(); } @@ -176,4 +176,4 @@ namespace Ocelot.UnitTests.RateLimit public override long Length { get; } public override long Position { get; set; } } -} +} diff --git a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs index bad8668e..14413aad 100644 --- a/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpExeptionToErrorMapperTests.cs @@ -38,6 +38,14 @@ error.ShouldBeOfType(); } + [Fact] + public void should_return_request_canceled_for_subtype() + { + var error = _mapper.Map(new SomeException()); + + error.ShouldBeOfType(); + } + [Fact] public void should_return_error_from_mapper() { @@ -56,5 +64,8 @@ error.ShouldBeOfType(); } + + private class SomeException : OperationCanceledException + { } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index a597df6b..8a7b1fc8 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -41,9 +41,10 @@ namespace Ocelot.UnitTests.Requester public void should_call_services_correctly() { this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage()))) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.OK)))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheDownstreamResponseIsSet()) + .Then(x => InformationIsLogged()) .BDDfy(); } @@ -57,6 +58,17 @@ namespace Ocelot.UnitTests.Requester .BDDfy(); } + [Fact] + public void should_log_downstream_internal_server_error() + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns( + new OkResponse(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError)))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.WarningIsLogged()) + .BDDfy(); + } + private void ThenTheErrorIsSet() { _downstreamContext.IsError.ShouldBeTrue(); @@ -98,5 +110,23 @@ namespace Ocelot.UnitTests.Requester _downstreamContext.DownstreamResponse.Content.ShouldBe(_response.Data.Content); _downstreamContext.DownstreamResponse.StatusCode.ShouldBe(_response.Data.StatusCode); } + + private void WarningIsLogged() + { + _logger.Verify( + x => x.LogWarning( + It.IsAny() + ), + Times.Once); + } + + private void InformationIsLogged() + { + _logger.Verify( + x => x.LogInformation( + It.IsAny() + ), + Times.Once); + } } }