From 556e8c286953b7e755ce6fcb509f776164ac06c5 Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 10 Feb 2025 12:02:28 -0300 Subject: [PATCH 1/4] Add initial implementation of Fresp package with fake response handling and tests --- .gitignore | 3 + Fresp.sln | 28 + README.md | 3 +- resources/icon.png | Bin 0 -> 20579 bytes src/FakeResponseHandler.cs | 83 +++ src/FakeResponseOptions.cs | 34 ++ src/Fresp.csproj | 59 +++ src/HttpClientBuilderExtensions.cs | 27 + tests/Fresp.Tests/FakeResponseHandlerTests.cs | 480 ++++++++++++++++++ tests/Fresp.Tests/Fresp.Tests.csproj | 30 ++ .../HttpClientBuilderExtensionsTests.cs | 44 ++ tests/Fresp.Tests/MockDelegatingHandler.cs | 18 + tests/Fresp.Tests/SutFakeResponseHandler.cs | 11 + 13 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 Fresp.sln create mode 100644 resources/icon.png create mode 100644 src/FakeResponseHandler.cs create mode 100644 src/FakeResponseOptions.cs create mode 100644 src/Fresp.csproj create mode 100644 src/HttpClientBuilderExtensions.cs create mode 100644 tests/Fresp.Tests/FakeResponseHandlerTests.cs create mode 100644 tests/Fresp.Tests/Fresp.Tests.csproj create mode 100644 tests/Fresp.Tests/HttpClientBuilderExtensionsTests.cs create mode 100644 tests/Fresp.Tests/MockDelegatingHandler.cs create mode 100644 tests/Fresp.Tests/SutFakeResponseHandler.cs diff --git a/.gitignore b/.gitignore index a4fe18b..ec8aa5c 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +*.idea + +.qodo \ No newline at end of file diff --git a/Fresp.sln b/Fresp.sln new file mode 100644 index 0000000..9dcd87b --- /dev/null +++ b/Fresp.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fresp", "src\Fresp.csproj", "{298F8E21-905F-4EB3-A076-81A645324D23}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Fresp.Tests", "tests\Fresp.Tests\Fresp.Tests.csproj", "{83E0CEAA-6C77-4DC4-996B-DCC1C751902A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {298F8E21-905F-4EB3-A076-81A645324D23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {298F8E21-905F-4EB3-A076-81A645324D23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {298F8E21-905F-4EB3-A076-81A645324D23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {298F8E21-905F-4EB3-A076-81A645324D23}.Release|Any CPU.Build.0 = Release|Any CPU + {83E0CEAA-6C77-4DC4-996B-DCC1C751902A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83E0CEAA-6C77-4DC4-996B-DCC1C751902A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83E0CEAA-6C77-4DC4-996B-DCC1C751902A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83E0CEAA-6C77-4DC4-996B-DCC1C751902A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 3c15bc5..c315065 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# Fresp -Fresp is a .NET NuGet package designed to provide fake responses for external APIs, aiding in testing environments such as UAT, HML, and QA. +# Fresp \ No newline at end of file diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dba46ea82daa76eca0d05839ad7a7ebb9d47f385 GIT binary patch literal 20579 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

K$Ovfw-Api2h6B`Tm4}rn>qw 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 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (!UseFakeResponse()) + return await base.SendAsync(request, cancellationToken); + + foreach (var func in options.FakesAsync) + { + try + { + var response = await func(request); + if (response is null) + continue; + + LogDebug("Async fake response found for client {ClientName}. Returning fake response...", clientName); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while trying to get a async fake response for client {ClientName}.", clientName); + } + } + + LogDebug("No async fake response found for client {ClientName}. Forwarding request to the next handler...", clientName); + return await base.SendAsync(request, cancellationToken); + } + + private bool UseFakeResponse() + { + var isEnabled = options.Enabled && !_isProduction; + if (!isEnabled) + LogDebug("Fake response is disabled for client {ClientName}. Enabled: {Enabled} | Production: {Production}. Forwarding request to the next handler...", clientName, options.Enabled, _isProduction); + + return isEnabled; + } + + private void LogDebug(string message, params object?[] args) + { + if (!_logger.IsEnabled(LogLevel.Debug)) + return; + + _logger.LogDebug(message, args); + } +} diff --git a/src/FakeResponseOptions.cs b/src/FakeResponseOptions.cs new file mode 100644 index 0000000..79aac35 --- /dev/null +++ b/src/FakeResponseOptions.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Fresp; + +public class FakeResponseOptions +{ + internal readonly List> Fakes = []; + internal readonly List>> FakesAsync = []; + + ///

+ /// Enabled or disable the handler to return fake responses. Default is false. + /// + public bool Enabled { get; set; } + + /// + /// The name of the client that will be used to match the . If null, the name from will be used. + /// + public string? ClientName { get; set; } = null; + + /// + /// Add a fake sync response to the handler that match . + /// + /// A Func that takes an and returns an or null. + public void AddFakeResponse(Func fake) => Fakes.Add(fake); + + /// + /// Add a fake async response to the handler that match . + /// + /// A Func that takes an and returns an or null. + public void AddFakeResponseAsync(Func> fake) => FakesAsync.Add(fake); +} diff --git a/src/Fresp.csproj b/src/Fresp.csproj new file mode 100644 index 0000000..d34cf1c --- /dev/null +++ b/src/Fresp.csproj @@ -0,0 +1,59 @@ + + + + net6.0;net7.0;net8.0;net9.0 + 13 + disable + enable + 1.0.0 + Fresp + Fresp + Fresp is a .NET NuGet package designed to provide fake responses for external APIs, aiding in testing environments such as DEV, UAT, HML, and QA. + https://www.nuget.org/packages/Fresp + https://github.com/Adolfok3/Fresp + dotnet;c#;.net;core;csharp;lib;library;api;webapi;rest;endpoint;httpclient;request;response;mock;wiremock;handler;delegatinghandler;fake;test;external;qa;helper; + Adolfok3 + MIT + README.md + icon.png + True + true + all + moderate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>Fresp.Tests + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/src/HttpClientBuilderExtensions.cs b/src/HttpClientBuilderExtensions.cs new file mode 100644 index 0000000..1c3a450 --- /dev/null +++ b/src/HttpClientBuilderExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; + +namespace Fresp; + +/// +/// Provides extension methods for to add a . +/// +public static class HttpClientBuilderExtensions +{ + /// + /// Adds a to the . + /// + /// The to add the handler to. + /// An optional to configure the . + /// The with the added. + public static IHttpClientBuilder AddFakeResponseHandler(this IHttpClientBuilder builder, Action? options = null) + { + var handlerOptions = new FakeResponseOptions(); + options?.Invoke(handlerOptions); + builder.AddHttpMessageHandler(services => new FakeResponseHandler(handlerOptions, handlerOptions.ClientName ?? builder.Name, services.GetRequiredService(), services.GetRequiredService())); + + return builder; + } +} diff --git a/tests/Fresp.Tests/FakeResponseHandlerTests.cs b/tests/Fresp.Tests/FakeResponseHandlerTests.cs new file mode 100644 index 0000000..f3eb3fb --- /dev/null +++ b/tests/Fresp.Tests/FakeResponseHandlerTests.cs @@ -0,0 +1,480 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace Fresp.Tests; + +public class FakeResponseHandlerTests +{ + [Fact] + public async Task Send_InProduction_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions(); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Production); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = handler.Send(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task Send_WithDisabled_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = false + }; + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = handler.Send(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task SendAsync_InProduction_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions(); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Production); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task SendAsync_WithDisabled_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = false + }; + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task Send_WithoutFakes_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions { Enabled = true }; + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = handler.Send(new HttpRequestMessage(), CancellationToken.None); + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task Send_WithFakes_ShouldReturnFromFake() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponse(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake") && request.Method == HttpMethod.Post) + { + return new HttpResponseMessage + { + Content = new StringContent("Faked!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked" + }; + } + + return null; + }); + options.AddFakeResponse(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake-2") && request.Method == HttpMethod.Get) + { + return new HttpResponseMessage + { + Content = new StringContent("Faked2!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked2" + }; + } + + return null; + }); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + var request = new HttpRequestMessage(HttpMethod.Post, "/must-fake"); + var request2 = new HttpRequestMessage(HttpMethod.Get, "/must-fake-2"); + + // Act + var response = handler.Send(request, CancellationToken.None); + var response2 = handler.Send(request2, CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.ReasonPhrase.Should().Be("Faked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Faked!"); + + response2.Should().NotBeNull(); + response2.StatusCode.Should().Be(HttpStatusCode.OK); + response2.ReasonPhrase.Should().Be("Faked2"); + content = await response2.Content.ReadAsStringAsync(); + content.Should().Be("Faked2!"); + } + + [Fact] + public async Task Send_WithFakes_ShouldReturnForward() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponse(request => + { + if (request.RequestUri?.AbsolutePath == "/must-fake" && request.Method == HttpMethod.Post) + { + return new HttpResponseMessage + { + Content = new StringContent("Faked!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked" + }; + } + + return null; + }); + options.AddFakeResponse(request => + { + if (request.RequestUri?.AbsolutePath == "/must-fake-2" && request.Method == HttpMethod.Get) + { + return new HttpResponseMessage + { + Content = new StringContent("Faked2!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked2" + }; + } + + return null; + }); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + var request = new HttpRequestMessage(HttpMethod.Post, "/must-not-fake"); + var request2 = new HttpRequestMessage(HttpMethod.Get, "/must-not-fake-2"); + + // Act + var response = handler.Send(request, CancellationToken.None); + var response2 = handler.Send(request2, CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + + response2.Should().NotBeNull(); + response2.StatusCode.Should().Be(HttpStatusCode.Accepted); + response2.ReasonPhrase.Should().Be("Mocked"); + content = await response2.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task SendAsync_WithoutFakes_ShouldForwardRequest() + { + // Arrange + var options = new FakeResponseOptions { Enabled = true }; + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task SendAsync_WithFakes_ShouldReturnFromFake() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponseAsync(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake") && request.Method == HttpMethod.Post) + { + return Task.FromResult(new HttpResponseMessage + { + Content = new StringContent("Faked!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked" + }); + } + + return Task.FromResult(null); + }); + options.AddFakeResponseAsync(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake-2") && request.Method == HttpMethod.Get) + { + return Task.FromResult(new HttpResponseMessage + { + Content = new StringContent("Faked2!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked2" + }); + } + + return Task.FromResult((HttpResponseMessage?)null); + }); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + var request = new HttpRequestMessage(HttpMethod.Post, "/must-fake"); + var request2 = new HttpRequestMessage(HttpMethod.Get, "/must-fake-2"); + + // Act + var response = await handler.SendAsync(request, CancellationToken.None); + var response2 = await handler.SendAsync(request2, CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.ReasonPhrase.Should().Be("Faked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Faked!"); + + response2.Should().NotBeNull(); + response2.StatusCode.Should().Be(HttpStatusCode.OK); + response2.ReasonPhrase.Should().Be("Faked2"); + content = await response2.Content.ReadAsStringAsync(); + content.Should().Be("Faked2!"); + } + + [Fact] + public async Task SendAsync_WithFakes_ShouldReturnForward() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponseAsync(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake") && request.Method == HttpMethod.Post) + { + return Task.FromResult(new HttpResponseMessage + { + Content = new StringContent("Faked!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked" + }); + } + + return Task.FromResult((HttpResponseMessage?)null); + }); + options.AddFakeResponseAsync(request => + { + if (request.RequestUri != null && request.RequestUri.ToString().EndsWith("/must-fake-2") && request.Method == HttpMethod.Get) + { + return Task.FromResult((HttpResponseMessage?)new HttpResponseMessage + { + Content = new StringContent("Faked2!"), + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "Faked2" + }); + } + + return Task.FromResult(null); + }); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + var request = new HttpRequestMessage(HttpMethod.Post, "/must-not-fake"); + var request2 = new HttpRequestMessage(HttpMethod.Get, "/must-not-fake-2"); + + // Act + var response = await handler.SendAsync(request, CancellationToken.None); + var response2 = await handler.SendAsync(request2, CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + + response2.Should().NotBeNull(); + response2.StatusCode.Should().Be(HttpStatusCode.Accepted); + response2.ReasonPhrase.Should().Be("Mocked"); + content = await response2.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task Send_WithFakes_ShouldThrowsAndForwardRequest() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponse(_ => throw new Exception("Fake exception")); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = handler.Send(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } + + [Fact] + public async Task SendAsync_WithFakes_ShouldThrowsAndForwardRequest() + { + // Arrange + var options = new FakeResponseOptions + { + Enabled = true + }; + options.AddFakeResponseAsync(_ => throw new Exception("Fake exception")); + var environment = Substitute.For(); + environment.EnvironmentName.Returns(Environments.Development); + var logger = Substitute.For(); + logger.CreateLogger(Arg.Any()).Returns(Substitute.For()); + var handler = new SutFakeResponseHandler(options, "clienttest", environment, logger) + { + InnerHandler = new MockDelegatingHandler() + }; + + // Act + var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.Accepted); + response.ReasonPhrase.Should().Be("Mocked"); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Be("Mocked!"); + } +} diff --git a/tests/Fresp.Tests/Fresp.Tests.csproj b/tests/Fresp.Tests/Fresp.Tests.csproj new file mode 100644 index 0000000..5ca2555 --- /dev/null +++ b/tests/Fresp.Tests/Fresp.Tests.csproj @@ -0,0 +1,30 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Fresp.Tests/HttpClientBuilderExtensionsTests.cs b/tests/Fresp.Tests/HttpClientBuilderExtensionsTests.cs new file mode 100644 index 0000000..5763f93 --- /dev/null +++ b/tests/Fresp.Tests/HttpClientBuilderExtensionsTests.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Fresp.Tests; + +public class HttpClientBuilderExtensionsTests +{ + [Fact] + public void AddFakeResponseHandler_WithoutOptions_ShouldAddSuccessfully() + { + // Arrange + var builder = Substitute.For(); + builder.Name.Returns("TestClient"); + + // Act + var act = () => builder.AddFakeResponseHandler(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void AddFakeResponseHandler_WithOptions_ShouldAddSuccessfully() + { + // Arrange + var builder = Substitute.For(); + + var optionsInvoked = false; + Action configureOptions = options => + { + optionsInvoked = true; + options.ClientName = "TestClient"; + options.Enabled = true; + options.AddFakeResponse(_ => new HttpResponseMessage()); + options.AddFakeResponseAsync(_ => Task.FromResult(new HttpResponseMessage())); + }; + + // Act + var act = () => builder.AddFakeResponseHandler(configureOptions); + + // Assert + act.Should().NotThrow(); + optionsInvoked.Should().BeTrue(); + } +} diff --git a/tests/Fresp.Tests/MockDelegatingHandler.cs b/tests/Fresp.Tests/MockDelegatingHandler.cs new file mode 100644 index 0000000..6d92439 --- /dev/null +++ b/tests/Fresp.Tests/MockDelegatingHandler.cs @@ -0,0 +1,18 @@ +namespace Fresp.Tests; + +internal class MockDelegatingHandler : DelegatingHandler +{ + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) => new HttpResponseMessage + { + Content = new StringContent("Mocked!"), + StatusCode = System.Net.HttpStatusCode.Accepted, + ReasonPhrase = "Mocked" + }; + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage + { + Content = new StringContent("Mocked!"), + StatusCode = System.Net.HttpStatusCode.Accepted, + ReasonPhrase = "Mocked" + }); +} diff --git a/tests/Fresp.Tests/SutFakeResponseHandler.cs b/tests/Fresp.Tests/SutFakeResponseHandler.cs new file mode 100644 index 0000000..945e2d9 --- /dev/null +++ b/tests/Fresp.Tests/SutFakeResponseHandler.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Fresp.Tests; + +internal class SutFakeResponseHandler(FakeResponseOptions options, string clientName, IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) : FakeResponseHandler(options, clientName, hostEnvironment, loggerFactory) +{ + public new HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) => base.Send(request, cancellationToken); + + public new Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => base.SendAsync(request, cancellationToken); +} From 0515be7c9762204f0c634c44a2911d11322141fd Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 10 Feb 2025 14:43:52 -0300 Subject: [PATCH 2/4] Update README and add GitHub Actions workflows --- .github/workflows/main.yml | 55 ++++++++++++++++++ .github/workflows/release.yml | 32 ++++++++++ README.md | 106 +++++++++++++++++++++++++++++++++- 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..779e2dd --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,55 @@ +name: Main + +on: + push: + branches: [ main, feature/*, hotfix/* ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore --property NuGetAudit=true --property NuGetAuditMode=All --property NuGetAuditLevel=Moderate --property TreatWarningsAsErrors=true + - name: Build + run: dotnet build --no-restore + + test: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore --property NuGetAudit=true --property NuGetAuditMode=All --property NuGetAuditLevel=Moderate --property TreatWarningsAsErrors=true + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal + + analisys: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x + - name: Generate coverage report + run: | + cd tests/Fresp.Tests + dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov + - name: Publish coverage report to coveralls.io + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: tests/Fresp.Tests/TestResults/coverage.info \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..612437f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: Release + +on: + push: + tags: + - '*.*.*' +jobs: + + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set current Tag + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - name: Check current Tag + run: | + echo $RELEASE_VERSION + echo ${{ env.RELEASE_VERSION }} + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x + - name: Generate Package + run: | + dotnet clean -c Release + dotnet build -c Release + dotnet test -c Release --no-build --verbosity normal + ls + dotnet pack src/Fresp.csproj -c Release --no-build /p:Version=${{ env.RELEASE_VERSION }} + - name: Push to NuGet + run: | + dotnet nuget push src/bin/Release/Fresp.${{ env.RELEASE_VERSION }}.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/README.md b/README.md index c315065..479daab 100644 --- a/README.md +++ b/README.md @@ -1 +1,105 @@ -# Fresp \ No newline at end of file +![Fresp Icon](./resources/icon.png) + +[![GithubActions](https://github.com/Adolfok3/fresp/actions/workflows/main.yml/badge.svg)](https://github.com/Adolfok3/fresp/actions) +[![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) +[![Coverage Status](https://coveralls.io/repos/github/Adolfok3/Fresp/badge.svg?branch=main)](https://coveralls.io/github/Adolfok3/Fresp?branch=main) +[![NuGet Version](https://img.shields.io/nuget/vpre/fresp)](https://www.nuget.org/packages/fresp) + +# Fresp + +Fresp is a .NET package that provides a way to mock API responses through your `HttpClient` during application execution. It allows you to configure both synchronous and asynchronous fake responses based on the incoming `HttpRequestMessage`. + +## Problem + +In many development or UAT environments, external APIs may be unreliable, slow, or even unavailable. This can cause significant delays and issues when trying to test and develop features that depend on these APIs. For example, if an external API is down, it can block the entire development process, making it difficult to proceed with testing and development. + +To address this issue, the team needs a way to bypass the call to the external API and provide a fake response instead. This allows the development and testing to continue smoothly without being dependent on the availability or reliability of the external API. + +The Fresp package helps to solve this problem by allowing developers to configure fake responses for their `HttpClient` requests, ensuring that development and testing can proceed without interruption. + +> [!NOTE] +> Fresp is not intended for unit testing; it is recommended for use in UAT, QA, and development environments during execution. + +> [!WARNING] +> By default, Fresp is disabled in the production environment, so the chance of getting a fake response in production is zero! + +## Installation + +To install Fresp, use one of the following methods: + +### NuGet Package Manager Console + +```powershell +Install-Package Fresp +``` + +### .NET CLI + +```bash +dotnet add package Fresp +``` + +## Usage + +### Adding Fake Response to your HttpClient + +To make `Fresp` mock and return fake responses from your `HttpClient`, use the `AddFakeResponseHandler` extension method: + +```csharp +services.AddHttpClient("MyClient") + .AddFakeResponseHandler(options => + { + options.Enabled = true; + }); +``` + +### Configuring Fake Responses + +Use the method `AddFakeResponse` for synchronous request calls or `AddFakeResponseAsync` for asynchronous request calls: + +- Synchronous: +```csharp +services.AddHttpClient("MyClient") + .AddFakeResponseHandler(options => + { + options.Enabled = true; + options.AddFakeResponse(request => + { + if (request.RequestUri?.AbsolutePath == "/endpoint") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Sync fake response") + }; + } + return null; + }); + }); +``` +- Asynchronous: +```csharp +services.AddHttpClient("MyClient") + .AddFakeResponseHandler(options => + { + options.Enabled = true; + options.AddFakeResponseAsync(async request => + { + var body = await request.Content.ReadAsStringAsync(); + if (body.Contains("something")) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("Async fake response") + }; + } + + return await Task.FromResult(null); + }); + }); +``` + +If the request predicate is matched, the following configured response will be returned. It's simple and lightweight! + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file From 2e8339860b35c321e549d74561969cc426a8691f Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 10 Feb 2025 14:46:31 -0300 Subject: [PATCH 3/4] Clarify Fresp description in README to include full form of the name --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 479daab..039d0d7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ # Fresp -Fresp is a .NET package that provides a way to mock API responses through your `HttpClient` during application execution. It allows you to configure both synchronous and asynchronous fake responses based on the incoming `HttpRequestMessage`. +Fresp (shorthand for `fake response`) is a .NET package that provides a way to mock API responses through your `HttpClient` during application execution. It allows you to configure both synchronous and asynchronous fake responses based on the incoming `HttpRequestMessage`. ## Problem From b2d981aa817063cc4f9304572be4069abdb6f59e Mon Sep 17 00:00:00 2001 From: Luiz Adolfo Date: Mon, 10 Feb 2025 19:26:25 -0300 Subject: [PATCH 4/4] Refactor GitHub Actions workflow for code validation and update README for clarity on production environment settings --- .github/workflows/main.yml | 30 ++---------------------------- README.md | 4 ++-- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 779e2dd..890e3a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,7 +6,7 @@ on: jobs: - build: + code-validation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -15,36 +15,10 @@ jobs: with: dotnet-version: 9.0.x - name: Restore dependencies - run: dotnet restore --property NuGetAudit=true --property NuGetAuditMode=All --property NuGetAuditLevel=Moderate --property TreatWarningsAsErrors=true - - name: Build - run: dotnet build --no-restore - - test: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 9.0.x - - name: Restore dependencies - run: dotnet restore --property NuGetAudit=true --property NuGetAuditMode=All --property NuGetAuditLevel=Moderate --property TreatWarningsAsErrors=true + run: dotnet restore -p:NuGetAudit=true -p:NuGetAuditMode=All -p:NuGetAuditLevel=Moderate -p:TreatWarningsAsErrors=true - name: Build run: dotnet build --no-restore - name: Test - run: dotnet test --no-build --verbosity normal - - analisys: - needs: test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 9.0.x - - name: Generate coverage report run: | cd tests/Fresp.Tests dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov diff --git a/README.md b/README.md index 039d0d7..3373f41 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The Fresp package helps to solve this problem by allowing developers to configur > Fresp is not intended for unit testing; it is recommended for use in UAT, QA, and development environments during execution. > [!WARNING] -> By default, Fresp is disabled in the production environment, so the chance of getting a fake response in production is zero! +> By default, Fresp is disabled in the production environment, so the chance of getting a fake response in production is zero! Unless your `ASPNETCORE_ENVIRONMENT` variable is wrong set in production server! ## Installation @@ -49,7 +49,7 @@ To make `Fresp` mock and return fake responses from your `HttpClient`, use the ` services.AddHttpClient("MyClient") .AddFakeResponseHandler(options => { - options.Enabled = true; + options.Enabled = true; // Toggle fake responses for this client. It is recommended to use this in conjunction with configuration settings from appsettings.json. }); ```