Skip to content

Commit

Permalink
update test project to net47 for FluentAssertions.
Browse files Browse the repository at this point in the history
write more tests for remoteip format.
improve tests with VerifyNoOutstandingExpectations with MockHttp.
support CData response field.
  • Loading branch information
bchavez committed Sep 4, 2023
1 parent e29158d commit 923c063
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 22 deletions.
4 changes: 2 additions & 2 deletions BitArmory.Turnstile.Tests/BitArmory.Turnstile.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net45;net6.0;net7.0</TargetFrameworks>
<TargetFrameworks>net47;net6.0;net7.0</TargetFrameworks>
<OutputType>Library</OutputType>
<AssemblyOriginatorKeyFile />
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="FluentAssertions" Version="5.6.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Flurl" Version="2.8.1" />
<PackageReference Include="Flurl.Http" Version="2.4.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
Expand Down
38 changes: 35 additions & 3 deletions BitArmory.Turnstile.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using NUnit.Framework;
Expand All @@ -15,7 +16,7 @@ public async Task can_make_process_bad_request()
var service = new TurnstileService();
service.EnableFiddlerDebugProxy("http://localhost.:8888");

var response = await service.VerifyAsync("ffff", "127.0.0.1", "bbbb");
var response = await service.VerifyAsync("ffff", "bbbb");

response.IsSuccess.Should().BeFalse();
response.ErrorCodes[0].Should().Be("invalid-input-secret");
Expand All @@ -28,12 +29,43 @@ public async Task can_make_good_request()
var service = new TurnstileService();
service.EnableFiddlerDebugProxy("http://localhost.:8888");

var response = await service.VerifyAsync("ffff", "127.0.0.1", "1x0000000000000000000000000000000AA");
var response = await service.VerifyAsync("ffff", "1x0000000000000000000000000000000AA");

response.IsSuccess.Should().Be(true);
response.ErrorCodes.Length.Should().Be(0);
response.ChallengeTs.Should().NotBeNullOrEmpty();
response.HostName.Should().Be("example.com");
}

[Test]
public async Task can_make_good_request_with_idempotency()
{
var service = new TurnstileService();
service.EnableFiddlerDebugProxy("http://localhost.:8888");

var idempotencyKey = "E446D876-990F-43F6-ACAC-B3BEF8CF6803";

var response = await service.VerifyAsync("ffff", "1x0000000000000000000000000000000AA", idempotencyKey: idempotencyKey);

response.IsSuccess.Should().Be(true);
response.ErrorCodes.Length.Should().Be(0);
response.ChallengeTs.Should().NotBeNullOrEmpty();
response.HostName.Should().Be("example.com");
}

[Test]
public async Task can_make_good_request_with_remoteip()
{
var service = new TurnstileService();
service.EnableFiddlerDebugProxy("http://localhost.:8888");

var response = await service.VerifyAsync("ffff", "1x0000000000000000000000000000000AA", remoteIp: "127.0.0.1");

response.IsSuccess.Should().Be(true);
response.ErrorCodes.Length.Should().Be(0);
response.ChallengeTs.Should().NotBeNullOrEmpty();
response.HostName.Should().Be("example.com");
}

}
}
70 changes: 61 additions & 9 deletions BitArmory.Turnstile.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Net.Http;
using System;
using System.Net.Http;
using System.Net.Mime;
using System.Threading.Tasks;
using FluentAssertions;
using NUnit.Framework;
using RichardSzalay.MockHttp;
using static FluentAssertions.FluentActions;

namespace BitArmory.Turnstile.Tests
{
Expand All @@ -27,17 +29,19 @@ public class UnitTests
public async Task can_parse_errors()
{
var mockHttp = new MockHttpMessageHandler();

mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.Respond("application/json", ErrorJson)
.WithExactFormData("response=aaa&remoteip=bbb&secret=ccc");

mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.WithExactFormData("response=aaa&secret=sss")
.Respond("application/json", ErrorJson);

var captcha = new TurnstileService(client: mockHttp.ToHttpClient());

var response = await captcha.VerifyAsync("aaa", "bbb", "ccc");
var response = await captcha.VerifyAsync("aaa", "sss");

response.IsSuccess.Should().BeFalse();
response.ErrorCodes.Should().BeEquivalentTo("invalid-input-secret");

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
Expand All @@ -46,18 +50,66 @@ public async Task can_parse_good_response()
var mockHttp = new MockHttpMessageHandler();

mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.Respond("application/json", GoodJson)
.WithExactFormData("response=aaa&remoteip=bbb&secret=ccc");
.WithExactFormData("response=aaa&secret=sss")
.Respond("application/json", GoodJson);

var captcha = new TurnstileService(client: mockHttp.ToHttpClient());

var response = await captcha.VerifyAsync("aaa", "bbb", "ccc");
var response = await captcha.VerifyAsync("aaa", "sss");

response.IsSuccess.Should().BeTrue();
response.ErrorCodes.Should().BeEmpty();
response.ChallengeTs.Should().Be("2023-09-01T03:09:21.688Z");
response.HostName.Should().Be("example.com");

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
public async Task invalid_ip_format_throws_argumentexception()
{
var mockHttp = new MockHttpMessageHandler();

var captcha = new TurnstileService(client: mockHttp.ToHttpClient());

await Awaiting(() => captcha.VerifyAsync("aaa", "sss", "rrr"))
.Should().ThrowExactlyAsync<ArgumentException>().WithParameterName("remoteIp");

mockHttp.GetMatchCount(mockHttp.Fallback).Should().Be(0);
}

[Test]
public async Task ipv4_format_is_okay()
{
var mockHttp = new MockHttpMessageHandler();

mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.WithExactFormData("response=aaa&secret=ccc&remoteip=127.0.0.1")
.Respond("application/json", GoodJson);

var captcha = new TurnstileService(client: mockHttp.ToHttpClient());

await Awaiting(() => captcha.VerifyAsync("aaa", "ccc", "127.0.0.1"))
.Should().NotThrowAsync();

mockHttp.VerifyNoOutstandingExpectation();
}

[Test]
public async Task ipv6_format_is_okay()
{
var mockHttp = new MockHttpMessageHandler();

mockHttp.Expect(HttpMethod.Post, Constants.VerifyUrl)
.WithExactFormData("response=aaa&secret=ccc&remoteip=127:0:0:1")
.Respond("application/json", GoodJson);

var captcha = new TurnstileService(client: mockHttp.ToHttpClient());

await Awaiting(() => captcha.VerifyAsync("aaa", "ccc", "127:0:0:1"))
.Should().NotThrowAsync();

mockHttp.VerifyNoOutstandingExpectation();
}
}
}
6 changes: 6 additions & 0 deletions BitArmory.Turnstile/TurnstileResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public class TurnstileResponse : JsonResponse
/// The hostname of the site where the Turnstile was solved
/// </summary>
public string HostName { get; set; }

/// <summary>
/// The customer data passed to the widget on the client side. This can be used by the customer
/// to convey state. It is integrity protected by modifications from an attacker.
/// </summary>
public string CData { get; set; }
}

}
36 changes: 28 additions & 8 deletions BitArmory.Turnstile/TurnstileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,26 @@ public TurnstileService(string verifyUrl = null, HttpClient client = null)
/// Validate Turnstile <paramref name="clientToken"/> using your secret.
/// </summary>
/// <param name="clientToken">Required. The user response token provided by the Turnstile client-side integration on your site. The <seealso cref="Constants.ClientResponseFormKey"/> value pulled from the hidden HTML Form field.</param>
/// <param name="remoteIp">Optional. The remote IP of the client</param>
/// <param name="siteSecret">Required. The server-side secret: v2 secret, invisible secret, or android secret. The shared key between your site and Turnstile.</param>
/// <param name="cancellationToken">Async cancellation token.</param>
public virtual async Task<TurnstileResponse> VerifyAsync(string clientToken, string remoteIp, string siteSecret, CancellationToken cancellationToken = default)
/// <param name="remoteIp">Optional. The remote IP of the client</param>
/// <param name="idempotencyKey">Optional. The optional UUID to be associated with the response.
/// Normally, a response may only be validated once. That is, if the same response is presented twice, the second and each subsequent
/// request will generate an error stating that the response has already been consumed. If an application requires
/// to retry failed requests, it must utilize the idempotency functionality. You can do so by providing a UUID as
/// the idempotencyKey parameter when initially validating the response and the same UUID
/// with any subsequent request for that response.</param>
/// <param name="cancellationToken">Optional. Async cancellation token.</param>
public virtual async Task<TurnstileResponse> VerifyAsync(string clientToken, string siteSecret, string remoteIp = null, string idempotencyKey = null, CancellationToken cancellationToken = default)
{
if( string.IsNullOrWhiteSpace(siteSecret) ) throw new ArgumentException("The secret must not be null or empty", nameof(siteSecret));
if( string.IsNullOrWhiteSpace(clientToken) ) throw new ArgumentException("The client response must not be null or empty", nameof(clientToken));
if( string.IsNullOrWhiteSpace(remoteIp) ) throw new ArgumentException("The remote ip of the client must be spec specified.", nameof(remoteIp));
if( remoteIp is not null && !remoteIp.Contains(".") && !remoteIp.Contains(":") )
{
throw new ArgumentException("The remote ip parameter must be formatted in IPv4 '.' or IPv6 ':' syntax.", nameof(remoteIp));
}


var form = PrepareRequestBody(clientToken, siteSecret, remoteIp);
var form = PrepareRequestBody(clientToken, siteSecret, remoteIp, idempotencyKey);

var response = await this.HttpClient.PostAsync(verifyUrl, form, cancellationToken)
.ConfigureAwait(false);
Expand All @@ -85,6 +94,9 @@ public virtual async Task<TurnstileResponse> VerifyAsync(string clientToken, str
case "hostname":
result.HostName = kv.Value;
break;
case "cdata":
result.CData = kv.Value;
break;
case "error-codes" when kv.Value is JsonArray errors:
{
result.ErrorCodes = errors.Children
Expand All @@ -109,15 +121,23 @@ public virtual async Task<TurnstileResponse> VerifyAsync(string clientToken, str
/// <param name="secret"></param>
/// <param name="remoteIp"></param>
/// <returns></returns>
protected FormUrlEncodedContent PrepareRequestBody(string clientResponse, string secret, string remoteIp)
protected FormUrlEncodedContent PrepareRequestBody(string clientResponse, string secret, string remoteIp, string idempotencyKey)
{
var form = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("response", clientResponse),
new KeyValuePair<string, string>("secret", secret),
new KeyValuePair<string, string>("remoteip", remoteIp)
new KeyValuePair<string, string>("secret", secret)
};

if( !string.IsNullOrWhiteSpace(remoteIp) )
{
form.Add(new KeyValuePair<string, string>("remoteip", remoteIp));
}
if( !string.IsNullOrWhiteSpace(idempotencyKey) )
{
form.Add(new KeyValuePair<string, string>("idempotency_key", idempotencyKey));
}

return new FormUrlEncodedContent(form);
}

Expand Down
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## v1.0.1
* Added Cloudflare Turnstile `TestingSiteKeys` and `TestingSecretKeys`.
* Changed `ClientResponseKey` -> `ClientResponseFormKey`.
* VerifyAsync method signature changed; made remoteIp param optional per documnetation.
* Support `CData` field in `TurnstileResponse`.

## v1.0.0
* Initial release.

0 comments on commit 923c063

Please sign in to comment.