diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyBehavior.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyBehavior.cs new file mode 100644 index 0000000..2eaaff4 --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyBehavior.cs @@ -0,0 +1,17 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace Mockaco.Chaos.Strategies; + +internal class ChaosStrategyBehavior : IChaosStrategy +{ + public Task Response(HttpResponse httpResponse) + { + httpResponse.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + + var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.ServiceUnavailable}"); + + return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length, default); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyException.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyException.cs new file mode 100644 index 0000000..4f6b6b4 --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyException.cs @@ -0,0 +1,17 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace Mockaco.Chaos.Strategies; + +internal class ChaosStrategyException : IChaosStrategy +{ + public Task Response(HttpResponse httpResponse) + { + httpResponse.StatusCode = (int)HttpStatusCode.InternalServerError; + + var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.InternalServerError}"); + + return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyLatency.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyLatency.cs new file mode 100644 index 0000000..5a2d616 --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyLatency.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Mockaco.Chaos.Strategies; + +internal class ChaosStrategyLatency : IChaosStrategy +{ + private readonly ILogger _logger; + private readonly IOptions _options; + + public ChaosStrategyLatency(ILogger logger, IOptions options) + { + _logger = logger; + _options = options; + } + public Task Response(HttpResponse httpResponse) + { + var responseDelay = new Random().Next(_options.Value.MinimumLatencyTime, _options.Value.MaximumLatencyTime); + _logger.LogInformation($"Chaos Latency (ms): {responseDelay}"); + return Task.Delay(responseDelay); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyResult.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyResult.cs new file mode 100644 index 0000000..18e23fb --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyResult.cs @@ -0,0 +1,17 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; + +namespace Mockaco.Chaos.Strategies; + +internal class ChaosStrategyResult : IChaosStrategy +{ + public Task Response(HttpResponse httpResponse) + { + httpResponse.StatusCode = (int)HttpStatusCode.BadRequest; + + var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.BadRequest}"); + + return httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyTimeout.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyTimeout.cs new file mode 100644 index 0000000..fe56f52 --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/ChaosStrategyTimeout.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Mockaco.Chaos.Strategies; + +internal class ChaosStrategyTimeout : IChaosStrategy +{ + private readonly IOptions _options; + + public ChaosStrategyTimeout(IOptions options) + { + _options = options; + } + + public async Task Response(HttpResponse httpResponse) + { + await Task.Delay(_options.Value.TimeBeforeTimeout); + + httpResponse.StatusCode = (int)HttpStatusCode.RequestTimeout; + + var bodyBytes = Encoding.UTF8.GetBytes($"Error {httpResponse.StatusCode}: {HttpStatusCode.RequestTimeout}"); + + await httpResponse.Body.WriteAsync(bodyBytes, 0, bodyBytes.Length, default); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Chaos/Strategies/IChaosStrategy.cs b/src/Mockaco.AspNetCore/Chaos/Strategies/IChaosStrategy.cs new file mode 100644 index 0000000..de13519 --- /dev/null +++ b/src/Mockaco.AspNetCore/Chaos/Strategies/IChaosStrategy.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Http; + +namespace Mockaco.Chaos.Strategies; + +internal interface IChaosStrategy +{ + Task Response(HttpResponse httpResponse); +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/DependencyInjection/MockacoApplicationBuilder.cs b/src/Mockaco.AspNetCore/DependencyInjection/MockacoApplicationBuilder.cs index ce0d3bc..e77b8fe 100644 --- a/src/Mockaco.AspNetCore/DependencyInjection/MockacoApplicationBuilder.cs +++ b/src/Mockaco.AspNetCore/DependencyInjection/MockacoApplicationBuilder.cs @@ -16,6 +16,8 @@ public static IApplicationBuilder UseMockaco(this IApplicationBuilder app, Actio var options = app.ApplicationServices.GetRequiredService>().Value; + var optionsChaos = app.ApplicationServices.GetRequiredService>().Value; + app.UseEndpoints(endpoints => { endpoints.Map($"/{options.VerificationEndpointPrefix ?? options.MockacoEndpoint}/{options.VerificationEndpointName}", VerifyerExtensions.Verify); @@ -36,6 +38,7 @@ public static IApplicationBuilder UseMockaco(this IApplicationBuilder app, Actio app .UseMiddleware() .UseMiddleware() + .UseMiddleware() .UseMiddleware() .UseMiddleware(); diff --git a/src/Mockaco.AspNetCore/DependencyInjection/MockacoServiceCollection.cs b/src/Mockaco.AspNetCore/DependencyInjection/MockacoServiceCollection.cs index 9094cb1..51cf15d 100644 --- a/src/Mockaco.AspNetCore/DependencyInjection/MockacoServiceCollection.cs +++ b/src/Mockaco.AspNetCore/DependencyInjection/MockacoServiceCollection.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Mockaco; +using Mockaco.Chaos.Strategies; using Mockaco.HealthChecks; using Mockaco.Settings; @@ -15,6 +16,7 @@ public static IServiceCollection AddMockaco(this IServiceCollection services) => public static IServiceCollection AddMockaco(this IServiceCollection services, Action config) => services .AddOptions().Configure(config).Services + .AddOptions().Configure>((options, parent) => options = parent.Value.Chaos).Services .AddOptions() .Configure>((options, parent) => options = parent.Value.TemplateFileProvider) .Services @@ -30,6 +32,7 @@ private static IServiceCollection AddConfiguration(this IServiceCollection servi services .AddOptions() .Configure(config) + .Configure(config.GetSection("Chaos")) .Configure(config.GetSection("TemplateFileProvider")); private static IServiceCollection AddCommonServices(this IServiceCollection services) @@ -38,6 +41,7 @@ private static IServiceCollection AddCommonServices(this IServiceCollection serv .AddMemoryCache() .AddHttpClient() .AddInternalServices() + .AddChaosServices() .AddHostedService(); services @@ -81,5 +85,13 @@ private static IServiceCollection AddInternalServices(this IServiceCollection se .AddTransient() .AddTemplatesGenerating(); + + private static IServiceCollection AddChaosServices(this IServiceCollection services) => + services + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient(); } } diff --git a/src/Mockaco.AspNetCore/Extensions/EnumerableExtensions.cs b/src/Mockaco.AspNetCore/Extensions/EnumerableExtensions.cs index 9b18510..de3c3d4 100644 --- a/src/Mockaco.AspNetCore/Extensions/EnumerableExtensions.cs +++ b/src/Mockaco.AspNetCore/Extensions/EnumerableExtensions.cs @@ -39,5 +39,10 @@ public static async Task AllAsync(this IEnumerable> return true; } + + public static T Random(this IEnumerable enumerable) { + int index = new Random().Next(0, enumerable.Count()); + return enumerable.ElementAt(index); + } } } diff --git a/src/Mockaco.AspNetCore/Middlewares/ChaosMiddleware.cs b/src/Mockaco.AspNetCore/Middlewares/ChaosMiddleware.cs new file mode 100644 index 0000000..4cb4056 --- /dev/null +++ b/src/Mockaco.AspNetCore/Middlewares/ChaosMiddleware.cs @@ -0,0 +1,73 @@ +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Mockaco.Chaos.Strategies; + +namespace Mockaco; + +internal class ChaosMiddleware +{ + private readonly RequestDelegate _next; + private readonly IEnumerable _strategies; + private readonly ILogger _logger; + private readonly IOptions _options; + + private List ErrorList { get; set; } + private int Counter { get; set; } + + public ChaosMiddleware( + RequestDelegate next, + IEnumerable strategies, + ILogger logger, + IOptions options) + { + _next = next; + _strategies = strategies; + _logger = logger; + _options = options; + } + + public async Task Invoke(HttpContext httpContext) + { + if (!_options.Value.Enabled) + { + return; + } + + Counter++; + if (Counter > 100) + Counter = 1; + + if (Counter == 1) + ErrorList = GenerateErrorList(_options.Value.ChaosRate); + + if (ErrorList.Contains(Counter)) + { + var selected = _strategies.Random(); + _logger.LogInformation($"Chaos: {selected?.ToString()}"); + if (selected != null) await selected.Response(httpContext.Response); + } + + if (httpContext.Response.StatusCode != (int)HttpStatusCode.OK) + return; + + + await _next(httpContext); + } + + private List GenerateErrorList(int rate) + { + var list = new List(); + while (list.Count < rate) + { + var item = new Random().Next(1, 100); + if (!list.Contains(item)) + { + list.Add(item); + } + } + + return list.OrderBy(x => x).ToList(); + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Options/ChaosOptions.cs b/src/Mockaco.AspNetCore/Options/ChaosOptions.cs new file mode 100644 index 0000000..e83c7f1 --- /dev/null +++ b/src/Mockaco.AspNetCore/Options/ChaosOptions.cs @@ -0,0 +1,19 @@ +namespace Mockaco; + +public class ChaosOptions +{ + public bool Enabled { get; set; } + public int ChaosRate { get; set; } + public int MinimumLatencyTime { get; set; } + public int MaximumLatencyTime { get; set; } + public int TimeBeforeTimeout { get; set; } + + public ChaosOptions() + { + Enabled = true; + ChaosRate = 10; + MinimumLatencyTime = 500; + MaximumLatencyTime = 3000; + TimeBeforeTimeout = 10000; + } +} \ No newline at end of file diff --git a/src/Mockaco.AspNetCore/Options/MockacoOptions.cs b/src/Mockaco.AspNetCore/Options/MockacoOptions.cs index 5a3257b..842789e 100644 --- a/src/Mockaco.AspNetCore/Options/MockacoOptions.cs +++ b/src/Mockaco.AspNetCore/Options/MockacoOptions.cs @@ -22,6 +22,8 @@ public class MockacoOptions // Deprecated (use MockacoEndpoint instead) public string VerificationEndpointPrefix { get; set; } + + public ChaosOptions Chaos { get; set; } public string VerificationEndpointName { get; set; } @@ -38,6 +40,7 @@ public MockacoOptions() MockacoEndpoint = "_mockaco"; VerificationEndpointName = "verification"; TemplateFileProvider = new(); + Chaos = new ChaosOptions(); } } } diff --git a/src/Mockaco.AspNetCore/PublicAPI.Unshipped.txt b/src/Mockaco.AspNetCore/PublicAPI.Unshipped.txt index 481a7e2..307a3fe 100644 --- a/src/Mockaco.AspNetCore/PublicAPI.Unshipped.txt +++ b/src/Mockaco.AspNetCore/PublicAPI.Unshipped.txt @@ -1,6 +1,18 @@ Bogus.PhoneNumberExtensions Microsoft.AspNetCore.Builder.MockacoApplicationBuilder Microsoft.Extensions.DependencyInjection.MockacoServiceCollection +Mockaco.ChaosOptions +Mockaco.ChaosOptions.ChaosOptions() -> void +Mockaco.ChaosOptions.ChaosRate.get -> int +Mockaco.ChaosOptions.ChaosRate.set -> void +Mockaco.ChaosOptions.Enabled.get -> bool +Mockaco.ChaosOptions.Enabled.set -> void +Mockaco.ChaosOptions.MaximumLatencyTime.get -> int +Mockaco.ChaosOptions.MaximumLatencyTime.set -> void +Mockaco.ChaosOptions.MinimumLatencyTime.get -> int +Mockaco.ChaosOptions.MinimumLatencyTime.set -> void +Mockaco.ChaosOptions.TimeBeforeTimeout.get -> int +Mockaco.ChaosOptions.TimeBeforeTimeout.set -> void Mockaco.HealthChecks.StartupHealthCheck Mockaco.HealthChecks.StartupHealthCheck.CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task Mockaco.HealthChecks.StartupHealthCheck.StartupCompleted.get -> bool @@ -42,6 +54,8 @@ Mockaco.Mock.RawTemplate.set -> void Mockaco.Mock.Route.get -> string Mockaco.Mock.Route.set -> void Mockaco.MockacoOptions +Mockaco.MockacoOptions.Chaos.get -> Mockaco.ChaosOptions +Mockaco.MockacoOptions.Chaos.set -> void Mockaco.MockacoOptions.DefaultHttpContentType.get -> string Mockaco.MockacoOptions.DefaultHttpContentType.set -> void Mockaco.MockacoOptions.DefaultHttpStatusCode.get -> System.Net.HttpStatusCode diff --git a/src/Mockaco/Settings/appsettings.json b/src/Mockaco/Settings/appsettings.json index 8675e75..1994e4c 100644 --- a/src/Mockaco/Settings/appsettings.json +++ b/src/Mockaco/Settings/appsettings.json @@ -17,7 +17,14 @@ "Imports": [], "MatchedRoutesCacheDuration": 60, "MockacoEndpoint": "_mockaco", - "VerificationEndpointName": "verification" + "VerificationEndpointName": "verification", + "Chaos": { + "Enabled": false, + "ChaosRate": 20, + "MinimumLatencyTime": 500, + "MaximumLatencyTime": 3000, + "TimeBeforeTimeout": 10000 + } }, "AllowedHosts": "*", "Serilog": { diff --git a/test/Mockaco.AspNetCore.Tests/Extensions/EnumerableExtensionTests.cs b/test/Mockaco.AspNetCore.Tests/Extensions/EnumerableExtensionTests.cs new file mode 100644 index 0000000..94e0847 --- /dev/null +++ b/test/Mockaco.AspNetCore.Tests/Extensions/EnumerableExtensionTests.cs @@ -0,0 +1,22 @@ +namespace Mockaco.Tests.Extensions; + +public class EnumerableExtensionTests +{ + [Theory] + [MemberData(nameof(Data))] + public void Select_Random_IEnumerables(List source) + { + IEnumerable enummerables = source; + + object selected = enummerables.Random(); + + Assert.Contains(selected, source); + } + + public static IEnumerable Data() + { + yield return new object[] { new List { 1, 2, 3 } }; + yield return new object[] { new List { "a", "b", "c" } }; + yield return new object[] { new List { "a1", "c2" } }; + } +} \ No newline at end of file diff --git a/website/docs/chaos/index.md b/website/docs/chaos/index.md new file mode 100644 index 0000000..1aa0bff --- /dev/null +++ b/website/docs/chaos/index.md @@ -0,0 +1,54 @@ +# Chaos Engineering + +Enabling chaos engineering, behavior different from what is expected will be randomly inserted into the calls, such as errors and delays, with this it is possible to verify how the client behaves in unforeseen situations. + +## How to enable Chaos Engineering + +To enable chaos it is necessary to set the 'Enabled' variable to 'true' as shown in the example below (see [Configuration](../configuration/index.md) for more details): + +``` +"Mockaco": { + ... + "Chaos": { + "Enabled": true, + }, + ... + }, +``` +in ```appsettings.json```. + +## Types of answers with chaos + +- Behavior: Return HTTP Error 503 (Service Unavailable) +- Exception: Return HTTP Error 500 (Internal Server Erro) +- Latency: Randomly add delay time to a call +- Result: Return HTTP Error 400 (Bad Request) +- Timeout: Waits a while and returns error 408 (Request Timeout) + +## How to define parameters + +Parameters are defined inside the Chaos key + +``` +"Mockaco": { + ... + "Chaos": { + "Enabled": true, + "ChaosRate": 20, + "MinimumLatencyTime": 500, + "MaximumLatencyTime": 3000, + "TimeBeforeTimeout": 10000 + }, + ... + }, +``` +in ```appsettings.json```. + + +|Parameter |Description |Default| +|--------------------|-------------------------------------------------------------|-------| +|Enabled |Option to enable and disable chaos (true / false) |true | +|ChaosRate |Percentage of calls affected by chaos (0 - 100) |20 | +|MinimumLatencyTime |Minimum latency in milliseconds when the latency strategy is drawn|500 | +|MaximumLatencyTime |Maximum latency in milliseconds when the latency strategy is drawn|3000 | +|TimeBeforeTimeout |Time in milliseconds before timeout error |10000 | \ No newline at end of file