Skip to content

Commit

Permalink
Merge pull request #116 from twilio-labs/ReloadOnChangeOptionsAndVali…
Browse files Browse the repository at this point in the history
…dation

Support reloadOnChange and add better validation
  • Loading branch information
Swimburger authored Mar 2, 2023
2 parents 0870c80 + dea6545 commit 08f8b94
Show file tree
Hide file tree
Showing 19 changed files with 1,355 additions and 801 deletions.
59 changes: 59 additions & 0 deletions src/Twilio.AspNet.Core.UnitTests/ContextMocks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Net;
using Microsoft.AspNetCore.Http;

namespace Twilio.AspNet.Core.UnitTests;

public class ContextMocks
{
public Moq.Mock<HttpContext> HttpContext { get; set; }
public Moq.Mock<HttpRequest> Request { get; set; }

public ContextMocks(bool isLocal, FormCollection form = null, bool isProxied = false) : this("", isLocal, form,
isProxied)
{
}

public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null, bool isProxied = false)
{
var headers = new HeaderDictionary();
headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form));
if (isProxied)
{
headers.Add("X-Forwarded-For", "1.1.1.1");
}

var connectionInfo = new Moq.Mock<ConnectionInfo>();
connectionInfo.Setup(x => x.RemoteIpAddress).Returns(isLocal ? IPAddress.Loopback : IPAddress.Parse("1.1.1.1"));

HttpContext = new Moq.Mock<HttpContext>();
Request = new Moq.Mock<HttpRequest>();
HttpContext.Setup(x => x.Request).Returns(Request.Object);
HttpContext.Setup(x => x.Connection).Returns(connectionInfo.Object);
Request.Setup(x => x.Headers).Returns(headers);
Request.Setup(x => x.HttpContext).Returns(HttpContext.Object);

var uri = new Uri(ContextMocks.fakeUrl);
Request.Setup(x => x.QueryString).Returns(new QueryString(uri.Query));
Request.Setup(x => x.Scheme).Returns(uri.Scheme);
Request.Setup(x => x.Host).Returns(new HostString(uri.Host));
Request.Setup(x => x.Path).Returns(new PathString(uri.AbsolutePath));

if (form != null)
{
Request.Setup(x => x.Method).Returns("POST");
Request.Setup(x => x.Form).Returns(form);
Request.Setup(x => x.HasFormContentType).Returns(true);
}
}

public static string fakeUrl = "https://api.example.com/webhook";
public static string fakeAuthToken = "thisisafakeauthtoken";

private static string CalculateSignature(string urlOverride, FormCollection form)
=> ValidationHelper.CalculateSignature(
string.IsNullOrEmpty(urlOverride) ? fakeUrl : urlOverride,
fakeAuthToken,
form
);
}
31 changes: 31 additions & 0 deletions src/Twilio.AspNet.Core.UnitTests/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

namespace Twilio.AspNet.Core.UnitTests;

public static class Extensions
{
public static Task<T> WaitForOptionChange<T>(this IOptionsMonitor<T> monitor)
{
var tcs = new TaskCompletionSource<T>();
var cts = new CancellationTokenSource();
IDisposable disposable = null;
disposable = monitor.OnChange(options =>
{
disposable.Dispose();
cts.Cancel();
tcs.SetResult(options);
});

Task.Delay(TimeSpan.FromSeconds(1), cts.Token)
.ContinueWith(_ =>
{
disposable.Dispose();
tcs.SetException(new Exception("WaitForOptionChange timed out."));
}, cts.Token);

return tcs.Task;
}
}
102 changes: 18 additions & 84 deletions src/Twilio.AspNet.Core.UnitTests/RequestValidationHelperTests.cs
Original file line number Diff line number Diff line change
@@ -1,88 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Xunit;

namespace Twilio.AspNet.Core.UnitTests;

public class ContextMocks
{
public Moq.Mock<HttpContext> HttpContext { get; set; }
public Moq.Mock<HttpRequest> Request { get; set; }

public ContextMocks(bool isLocal, FormCollection form = null, bool isProxied = false) : this("", isLocal, form, isProxied)
{
}

public ContextMocks(string urlOverride, bool isLocal, FormCollection form = null, bool isProxied = false)
{
var headers = new HeaderDictionary();
headers.Add("X-Twilio-Signature", CalculateSignature(urlOverride, form));
if (isProxied)
{
headers.Add("X-Forwarded-For", "1.1.1.1");
}

var connectionInfo = new Moq.Mock<ConnectionInfo>();
connectionInfo.Setup(x => x.RemoteIpAddress).Returns(isLocal ? IPAddress.Loopback : IPAddress.Parse("1.1.1.1"));

HttpContext = new Moq.Mock<HttpContext>();
Request = new Moq.Mock<HttpRequest>();
HttpContext.Setup(x => x.Request).Returns(Request.Object);
HttpContext.Setup(x => x.Connection).Returns(connectionInfo.Object);
Request.Setup(x => x.Headers).Returns(headers);
Request.Setup(x => x.HttpContext).Returns(HttpContext.Object);

var uri = new Uri(ContextMocks.fakeUrl);
Request.Setup(x => x.QueryString).Returns(new QueryString(uri.Query));
Request.Setup(x => x.Scheme).Returns(uri.Scheme);
Request.Setup(x => x.Host).Returns(new HostString(uri.Host));
Request.Setup(x => x.Path).Returns(new PathString(uri.AbsolutePath));

if (form != null)
{
Request.Setup(x => x.Method).Returns("POST");
Request.Setup(x => x.Form).Returns(form);
Request.Setup(x => x.HasFormContentType).Returns(true);
}
}

public static string fakeUrl = "https://api.example.com/webhook";
public static string fakeAuthToken = "thisisafakeauthtoken";

private string CalculateSignature(string urlOverride, FormCollection form)
{
var value = new StringBuilder();
value.Append(string.IsNullOrEmpty(urlOverride) ? ContextMocks.fakeUrl : urlOverride);

if (form != null)
{
var sortedKeys = form.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
foreach (var key in sortedKeys)
{
value.Append(key);
value.Append(form[key]);
}
}

var sha1 = new HMACSHA1(Encoding.UTF8.GetBytes(ContextMocks.fakeAuthToken));
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(value.ToString()));

return Convert.ToBase64String(hash);
}
}

public class RequestValidationHelperTests
{
[Fact]
public void TestLocal()
{
var fakeContext = (new ContextMocks(true)).HttpContext.Object;
var fakeContext = new ContextMocks(true).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", true);

Assert.True(result);
Expand All @@ -91,7 +20,7 @@ public void TestLocal()
[Fact]
public void TestNoLocalDueToProxy()
{
var fakeContext = (new ContextMocks(true, isProxied: true)).HttpContext.Object;
var fakeContext = new ContextMocks(true, isProxied: true).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", true);

Assert.False(result);
Expand All @@ -100,7 +29,7 @@ public void TestNoLocalDueToProxy()
[Fact]
public void TestNoLocal()
{
var fakeContext = (new ContextMocks(true)).HttpContext.Object;
var fakeContext = new ContextMocks(true).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, "bad-token", false);

Assert.False(result);
Expand All @@ -109,7 +38,7 @@ public void TestNoLocal()
[Fact]
public void TestNoForm()
{
var fakeContext = (new ContextMocks(true)).HttpContext.Object;
var fakeContext = new ContextMocks(true).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false);

Assert.True(result);
Expand All @@ -131,29 +60,34 @@ public void TestBadForm()
[Fact]
public void TestUrlOverrideFail()
{
var fakeContext = (new ContextMocks(true)).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", false);
var fakeContext = new ContextMocks(true).HttpContext.Object;
var result =
RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/",
false);

Assert.False(result);
}

[Fact]
public void TestUrlOverride()
{
var fakeContext = (new ContextMocks("https://example.com/", true)).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/", false);
var fakeContext = new ContextMocks("https://example.com/", true).HttpContext.Object;
var result =
RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, "https://example.com/",
false);

Assert.True(result);
}

[Fact]
public void TestForm()
{
var form = new FormCollection(new Dictionary<string, StringValues>() {
{"key1", "value1"},
{"key2", "value2"}
});
var fakeContext = (new ContextMocks(true, form)).HttpContext.Object;
var form = new FormCollection(new Dictionary<string, StringValues>
{
{ "key1", "value1" },
{ "key2", "value2" }
});
var fakeContext = new ContextMocks(true, form).HttpContext.Object;
var result = RequestValidationHelper.IsValidRequest(fakeContext, ContextMocks.fakeAuthToken, false);

Assert.True(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down
Loading

0 comments on commit 08f8b94

Please sign in to comment.