Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,6 @@ FodyWeavers.xsd

# JetBrains Rider
*.sln.iml
*.idea

.qodo
28 changes: 28 additions & 0 deletions Fresp.sln
Original file line number Diff line number Diff line change
@@ -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
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage?>(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.
Binary file added resources/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
83 changes: 83 additions & 0 deletions src/FakeResponseHandler.cs
Original file line number Diff line number Diff line change
@@ -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<HttpResponseMessage> 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);
}
}
34 changes: 34 additions & 0 deletions src/FakeResponseOptions.cs
Original file line number Diff line number Diff line change
@@ -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<Func<HttpRequestMessage, HttpResponseMessage?>> Fakes = [];
internal readonly List<Func<HttpRequestMessage, Task<HttpResponseMessage?>>> FakesAsync = [];

/// <summary>
/// Enabled or disable the handler to return fake responses. Default is false.
/// </summary>
public bool Enabled { get; set; }

/// <summary>
/// The name of the client that will be used to match the <see cref="HttpClient"/>. If null, the name from <see cref="Microsoft.Extensions.DependencyInjection.IHttpClientBuilder"/> will be used.
/// </summary>
public string? ClientName { get; set; } = null;

/// <summary>
/// Add a fake sync <see cref="HttpResponseMessage"/> response to the handler that match <see cref="HttpRequestMessage"/>.
/// </summary>
/// <param name="fake">A Func that takes an <see cref="HttpRequestMessage"/> and returns an <see cref="HttpResponseMessage"/> or null.</param>
public void AddFakeResponse(Func<HttpRequestMessage, HttpResponseMessage?> fake) => Fakes.Add(fake);

/// <summary>
/// Add a fake async <see cref="HttpResponseMessage"/> response to the handler that match <see cref="HttpRequestMessage"/>.
/// </summary>
/// <param name="fake">A Func that takes an <see cref="HttpRequestMessage"/> and returns an <see cref="HttpResponseMessage"/> or null.</param>
public void AddFakeResponseAsync(Func<HttpRequestMessage, Task<HttpResponseMessage?>> fake) => FakesAsync.Add(fake);
}
59 changes: 59 additions & 0 deletions src/Fresp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<LangVersion>13</LangVersion>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.0.0</Version>
<Title>Fresp</Title>
<PackageId>Fresp</PackageId>
<Description>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.</Description>
<PackageProjectUrl>https://www.nuget.org/packages/Fresp</PackageProjectUrl>
<RepositoryUrl>https://github.com/Adolfok3/Fresp</RepositoryUrl>
<PackageTags>dotnet;c#;.net;core;csharp;lib;library;api;webapi;rest;endpoint;httpclient;request;response;mock;wiremock;handler;delegatinghandler;fake;test;external;qa;helper;</PackageTags>
<Authors>Adolfok3</Authors>
<Copyright>MIT</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.1" />
</ItemGroup>

<ItemGroup>
<None Include="../README.md" Pack="true" Visible="false" PackagePath="" />
<None Include="../resources/icon.png" Pack="true" Visible="false" PackagePath="" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Fresp.Tests</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
Loading