diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..890e3a1 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: Main + +on: + push: + branches: [ main, feature/*, hotfix/* ] + +jobs: + + code-validation: + 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 -p:NuGetAudit=true -p:NuGetAuditMode=All -p:NuGetAuditLevel=Moderate -p:TreatWarningsAsErrors=true + - name: Build + run: dotnet build --no-restore + - name: Test + 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/.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..3373f41 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ +![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 NuGet package designed to provide fake responses for external APIs, aiding in testing environments such as UAT, HML, and QA. + +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 + +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! Unless your `ASPNETCORE_ENVIRONMENT` variable is wrong set in production server! + +## 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; // Toggle fake responses for this client. It is recommended to use this in conjunction with configuration settings from appsettings.json. + }); +``` + +### 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 diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..dba46ea Binary files /dev/null and b/resources/icon.png differ diff --git a/src/FakeResponseHandler.cs b/src/FakeResponseHandler.cs new file mode 100644 index 0000000..1451f67 --- /dev/null +++ b/src/FakeResponseHandler.cs @@ -0,0 +1,83 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Fresp; + +internal class FakeResponseHandler(FakeResponseOptions options, string clientName, IHostEnvironment hostEnvironment, ILoggerFactory loggerFactory) : DelegatingHandler +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(nameof(FakeResponseHandler)); + private readonly bool _isProduction = hostEnvironment.IsProduction(); + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (!UseFakeResponse()) + return base.Send(request, cancellationToken); + + foreach (var func in options.Fakes) + { + try + { + var response = func(request); + if (response is null) + continue; + + LogDebug("Sync 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 sync fake response for client {ClientName}.", clientName); + } + } + + LogDebug("No sync fake response found for client {ClientName}. Forwarding request to the next handler...", clientName); + return base.Send(request, cancellationToken); + } + + protected override async Task 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); +}