Skip to content

Commit

Permalink
Add Sentry.Tunnel to add official tunnel middleware (#1133)
Browse files Browse the repository at this point in the history
* Add Sentry.Tunnel to add official tunnel middleware

* Add Tunnel version user agent

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: Bruno Garcia <bruno@brunogarcia.com>

* Remove .NET testing stuff, cache the assembly version

* Add trailing newline

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

* Update src/Sentry.Tunnel/SentryTunnelMiddleware.cs

Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>

Co-authored-by: Bruno Garcia <bruno@brunogarcia.com>
Co-authored-by: LucasZF <lucas-zimerman1@hotmail.com>
  • Loading branch information
3 people committed Jul 25, 2021
1 parent 3f8ffa1 commit cd03fe1
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 15 deletions.
44 changes: 29 additions & 15 deletions Sentry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -101,35 +101,39 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{F267
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{E1800E08-72E6-45AE-91EC-69B4FDBA7555}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\danger.yml = .github\workflows\danger.yml
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\vulnerabilities.yml = .github\workflows\vulnerabilities.yml
EndProjectSection
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
.github\workflows\danger.yml = .github\workflows\danger.yml
.github\workflows\docs.yml = .github\workflows\docs.yml
.github\workflows\vulnerabilities.yml = .github\workflows\vulnerabilities.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.AspNetCore.Grpc", "src\Sentry.AspNetCore.Grpc\Sentry.AspNetCore.Grpc.csproj", "{720811C8-29C7-4368-86F2-D61DF415AC2A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.AspNetCore.Grpc.Tests", "test\Sentry.AspNetCore.Grpc.Tests\Sentry.AspNetCore.Grpc.Tests.csproj", "{3818FA5C-8649-427D-8E68-0C44558CA0DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.AspNetCore.Grpc", "samples\Sentry.Samples.AspNetCore.Grpc\Sentry.Samples.AspNetCore.Grpc.csproj", "{21599C29-C3D4-4DAC-A2D6-6C194600478F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.AspNetCore.Blazor.Wasm", "samples\Sentry.Samples.AspNetCore.Blazor.Wasm\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "{73FDCE53-75D1-4DCC-933A-8AB93A0E86EA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.AspNetCore.Blazor.Wasm", "samples\Sentry.Samples.AspNetCore.Blazor.Wasm\Sentry.Samples.AspNetCore.Blazor.Wasm.csproj", "{73FDCE53-75D1-4DCC-933A-8AB93A0E86EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.Aws.Lambda.AspNetCoreServer", "samples\Sentry.Samples.Aws.Lambda.AspNetCoreServer\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", "{274CEDC2-2129-469D-B269-649EFA2EF5E0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.EntityFramework", "src\Sentry.EntityFramework\Sentry.EntityFramework.csproj", "{8B38F62E-0DD5-486F-96F5-2025AFB9B491}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Aws.Lambda.AspNetCoreServer", "samples\Sentry.Samples.Aws.Lambda.AspNetCoreServer\Sentry.Samples.Aws.Lambda.AspNetCoreServer.csproj", "{274CEDC2-2129-469D-B269-649EFA2EF5E0}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.EntityFramework.Tests", "test\Sentry.EntityFramework.Tests\Sentry.EntityFramework.Tests.csproj", "{840B220E-68EC-4ECB-AEA1-67B0151F17FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.EntityFramework", "src\Sentry.EntityFramework\Sentry.EntityFramework.csproj", "{8B38F62E-0DD5-486F-96F5-2025AFB9B491}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.EntityFramework", "samples\Sentry.Samples.EntityFramework\Sentry.Samples.EntityFramework.csproj", "{8E4BA4C7-413C-4668-8F41-32F484FFB7AA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.EntityFramework.Tests", "test\Sentry.EntityFramework.Tests\Sentry.EntityFramework.Tests.csproj", "{840B220E-68EC-4ECB-AEA1-67B0151F17FC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Samples.Google.Cloud.Functions", "samples\Sentry.Samples.Google.Cloud.Functions\Sentry.Samples.Google.Cloud.Functions.csproj", "{88269A52-A0BA-41B2-8DF3-505B66B17243}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.EntityFramework", "samples\Sentry.Samples.EntityFramework\Sentry.Samples.EntityFramework.csproj", "{8E4BA4C7-413C-4668-8F41-32F484FFB7AA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Google.Cloud.Functions", "src\Sentry.Google.Cloud.Functions\Sentry.Google.Cloud.Functions.csproj", "{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Google.Cloud.Functions", "samples\Sentry.Samples.Google.Cloud.Functions\Sentry.Samples.Google.Cloud.Functions.csproj", "{88269A52-A0BA-41B2-8DF3-505B66B17243}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Google.Cloud.Functions.Tests", "test\Sentry.Google.Cloud.Functions.Tests\Sentry.Google.Cloud.Functions.Tests.csproj", "{066522A4-8380-4D29-8DD0-973B1EDF0B39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions", "src\Sentry.Google.Cloud.Functions\Sentry.Google.Cloud.Functions.csproj", "{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Tunnel", "src\Sentry.Tunnel\Sentry.Tunnel.csproj", "{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Google.Cloud.Functions.Tests", "test\Sentry.Google.Cloud.Functions.Tests\Sentry.Google.Cloud.Functions.Tests.csproj", "{066522A4-8380-4D29-8DD0-973B1EDF0B39}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sentry.Tunnel.Tests", "test\Sentry.Tunnel.Tests\Sentry.Tunnel.Tests.csproj", "{BB54EF08-2FA1-498B-827B-32D905FB0F9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -289,6 +293,14 @@ Global
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{066522A4-8380-4D29-8DD0-973B1EDF0B39}.Release|Any CPU.Build.0 = Release|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458}.Release|Any CPU.Build.0 = Release|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB54EF08-2FA1-498B-827B-32D905FB0F9F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -335,6 +347,8 @@ Global
{88269A52-A0BA-41B2-8DF3-505B66B17243} = {77454495-55EE-4B40-A089-71B9E8F82E89}
{D1DB7B31-EC6B-430B-B6B0-2849BAE41AC1} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB}
{066522A4-8380-4D29-8DD0-973B1EDF0B39} = {83263231-1A2A-4733-B759-EEFF14E8C5D5}
{D2F8BF0E-7749-4C92-A4F1-7B96A9878458} = {AF6AF4C7-8AA2-4D59-8064-2D79560904EB}
{BB54EF08-2FA1-498B-827B-32D905FB0F9F} = {83263231-1A2A-4733-B759-EEFF14E8C5D5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0C652B1A-DF72-4EE5-A98B-194FE2C054F6}
Expand Down
27 changes: 27 additions & 0 deletions src/Sentry.Tunnel/Sentry.Tunnel.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net5.0;netcoreapp3.0;netstandard2.0</TargetFrameworks>
<PackageTags>$(PackageTags);AspNetCore;MVC</PackageTags>
<PackageId>Sentry.Tunnel</PackageId>
<AssemblyName>Sentry.Tunnel</AssemblyName>
<RootNamespace>Sentry.Tunnel</RootNamespace>
<Description>Official Tunnel middleware for Sentry.</Description>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Routing.Abstractions" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.0" />
<PackageReference Include="System.Text.Json" Version="4.6.0" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.5.0" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
96 changes: 96 additions & 0 deletions src/Sentry.Tunnel/SentryTunnelMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers;

namespace Sentry.Tunnel
{
/// <summary>
/// Middleware that can forward Sentry envelopes.
/// </summary>
/// <seealso href="https://docs.sentry.io/platforms/javascript/troubleshooting/#dealing-with-ad-blockers"/>
public class SentryTunnelMiddleware : IMiddleware
{
private readonly string[] _allowedHosts;
private string? _version;
private string Version => _version ??= (GetType().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? string.Empty);

/// <summary>
/// Middleware that can forward Sentry envelopes.
/// </summary>
/// <seealso href="https://docs.sentry.io/platforms/javascript/troubleshooting/#dealing-with-ad-blockers"/>
public SentryTunnelMiddleware(string[] allowedHosts)
{
_allowedHosts = new[] {"sentry.io"}.Concat(allowedHosts).ToArray();
}

/// <inheritdoc />
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Method == "OPTIONS")
{
context.Response.Headers.Add("Access-Control-Allow-Origin", new[] {(string) context.Request.Headers["Origin"]});
context.Response.Headers.Add("Access-Control-Allow-Headers", new[] {"Origin, X-Requested-With, Content-Type, Accept"});
context.Response.Headers.Add("Access-Control-Allow-Methods", new[] {"POST, OPTIONS"});
context.Response.Headers.Add("Access-Control-Allow-Credentials", new[] {"true"});
context.Response.StatusCode = 200;
return;
}

var httpClientFactory = context.RequestServices.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient("SentryTunnel");
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Sentry.NET_Tunnel", Version));
var ms = new MemoryStream();
await context.Request.Body.CopyToAsync(ms);
ms.Position = 0;
using (var reader = new StreamReader(ms))
{
var header = await reader.ReadLineAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(header))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}

try
{
var headerJson = JsonSerializer.Deserialize<Dictionary<string, object>>(header);
if (headerJson == null)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Invalid DSN JSON supplied").ConfigureAwait(false);
return;
}
if (headerJson.TryGetValue("dsn", out var dsnString) && Uri.TryCreate(dsnString.ToString(), UriKind.Absolute, out var dsn) && _allowedHosts.Contains(dsn.Host))
{
var projectId = dsn.AbsolutePath.Trim('/');
ms.Position = 0;
var responseMessage = await client.PostAsync($"https://{dsn.Host}/api/{projectId}/envelope/",
new StreamContent(ms)).ConfigureAwait(false);
context.Response.Headers["content-type"] = "application/json";
context.Response.StatusCode = StatusCodes.Status200OK;
await responseMessage.Content.CopyToAsync(context.Response.Body).ConfigureAwait(false);
}
}
catch(JsonException)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Invalid DSN JSON supplied").ConfigureAwait(false);
}
catch(ArgumentNullException)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
await context.Response.WriteAsync("Received empty body").ConfigureAwait(false);
}
}
}
}
}
26 changes: 26 additions & 0 deletions src/Sentry.Tunnel/SentryTunnelingApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace Sentry.Tunnel
{
public static class SentryTunnelingApplicationBuilderExtensions
{
/// <summary>
/// Adds and configures the Sentry tunneling middleware
/// </summary>
/// <param name="services"></param>
/// <param name="hostnames">The extra hostnames to be allowed for the tunneling. sentry.io is allowed by default; add your own Sentry domain if you use a self-hosted Sentry or Relay.</param>
public static void AddSentryTunneling(this IServiceCollection services, params string[] hostnames)
{
services.AddScoped<SentryTunnelMiddleware>((s) => new SentryTunnelMiddleware(hostnames));
}

public static void UseSentryTunneling(this IApplicationBuilder builder, string path = "/tunnel")
{
builder.Map(path, applicationBuilder =>
{
applicationBuilder.UseMiddleware<SentryTunnelMiddleware>();
});
}
}
}
95 changes: 95 additions & 0 deletions test/Sentry.Tunnel.Tests/IntegrationsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Xunit;

namespace Sentry.Tunnel.Tests
{
[Collection("SentryTunnelCollection")]
public partial class IntegrationsTests
{
private readonly TestServer _server;
private HttpClient _httpClient;
private MockHttpMessageHandler _httpMessageHander;

public IntegrationsTests()
{
var builder = new WebHostBuilder()
.ConfigureServices(s =>
{
s.AddSentryTunneling("sentry.mywebsite.com");
_httpMessageHander = new MockHttpMessageHandler("{}", HttpStatusCode.OK);
_httpClient = new HttpClient(_httpMessageHander);
var factory = Substitute.For<IHttpClientFactory>();
factory.CreateClient(Arg.Any<string>()).Returns(_httpClient);
s.AddSingleton<IHttpClientFactory>(factory);
})
.Configure(app => { app.UseSentryTunneling(); });
_server = new TestServer(builder);
}

[Fact]
public async Task TunnelMiddleware_CanForwardValidEnvelope()
{
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel");
requestMessage.Content = new StringContent(
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@sentry.io/1""}
{""type"":""session""}
{""sid"":""fda00e933162466c849962eaea0cfaff""}");
var responseMessage = await _server.CreateClient().SendAsync(requestMessage);

Assert.Equal(1, _httpMessageHander.NumberOfCalls);
}

[Fact]
public async Task TunnelMiddleware_DoesNotForwardEnvelopeWithoutDsn()
{
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel");
requestMessage.Content = new StringContent(@"{}
{""type"":""session""}
{""sid"":""fda00e933162466c849962eaea0cfaff""}");
var responseMessage = await _server.CreateClient().SendAsync(requestMessage);

Assert.Equal(0, _httpMessageHander.NumberOfCalls);
}

[Fact]
public async Task TunnelMiddleware_DoesNotForwardEnvelopeToArbitraryHost()
{
{
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel");
requestMessage.Content = new StringContent(
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@evil.com/1""}
{""type"":""session""}
{""sid"":""fda00e933162466c849962eaea0cfaff""}");
var responseMessage = await _server.CreateClient().SendAsync(requestMessage);

Assert.Equal(0, _httpMessageHander.NumberOfCalls);
}
}

[Fact]
public async Task TunnelMiddleware_CanForwardEnvelopeToWhiteListedHost()
{
{
var requestMessage = new HttpRequestMessage(new HttpMethod("POST"), "/tunnel");
requestMessage.Content = new StringContent(
@"{""sent_at"":""2021-01-01T00:00:00.000Z"",""sdk"":{""name"":""sentry.javascript.browser"",""version"":""6.8.0""},""dsn"":""https://dns@sentry.mywebsite.com/1""}
{""type"":""session""}
{""sid"":""fda00e933162466c849962eaea0cfaff""}");
var responseMessage = await _server.CreateClient().SendAsync(requestMessage);

Assert.Equal(1, _httpMessageHander.NumberOfCalls);
}
}
}
}
41 changes: 41 additions & 0 deletions test/Sentry.Tunnel.Tests/MockHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Sentry.Tunnel.Tests
{
public class MockHttpMessageHandler : HttpMessageHandler
{
private readonly string _response;
private readonly HttpStatusCode _statusCode;

public string Input { get; private set; }
public int NumberOfCalls { get; private set; }

public MockHttpMessageHandler(string response, HttpStatusCode statusCode)
{
_response = response;
_statusCode = statusCode;
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
NumberOfCalls++;
if (request.Content != null) // Could be a GET-request without a body
{
Input = await request.Content.ReadAsStringAsync();
}
return new HttpResponseMessage
{
StatusCode = _statusCode,
Content = new StringContent(_response)
};
}
}
}
Loading

0 comments on commit cd03fe1

Please sign in to comment.