Skip to content

Commit

Permalink
Add localization support for the error message (#26)
Browse files Browse the repository at this point in the history
* add localization for the error message

* Use .net 6 in github actions

* Increment version to v1.4.0

* Fix unit tests
  • Loading branch information
sleeuwen authored Jan 9, 2022
1 parent a5c4dff commit fb9242a
Show file tree
Hide file tree
Showing 13 changed files with 309 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dotnetcore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
dotnet-version: 6.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand Down
144 changes: 138 additions & 6 deletions AspNetCore.ReCaptcha.Tests/ValidateReCaptchaAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
Expand All @@ -23,16 +28,16 @@ private static ActionExecutingContext CreateActionExecutingContext(Mock<HttpCont
{
httpContextMock.Setup(x => x.Request.HasFormContentType).Returns(true);
httpContextMock.Setup(x => x.Request.Form.TryGetValue(It.IsAny<string>(), out expected)).Returns(true);

return new ActionExecutingContext(actionContext, new List<IFilterMetadata>(),
new Dictionary<string, object>(), Mock.Of<Controller>());
}

private static ActionContext CreateActionContext(IMock<HttpContext> httpContextMock, ModelStateDictionary modelState)
private static ActionContext CreateActionContext(IMock<HttpContext> httpContextMock, ModelStateDictionary modelState, ActionDescriptor actionDescriptor)
{
return new(httpContextMock.Object,
Mock.Of<RouteData>(),
Mock.Of<ActionDescriptor>(),
actionDescriptor,
modelState);
}

Expand All @@ -49,11 +54,20 @@ public async Task VerifyAsyncReturnsBoolean(bool success)

var expected = new StringValues("123");

var serviceProviderMock = new Mock<IServiceProvider>();

var httpContextMock = new Mock<HttpContext>();

httpContextMock.Setup(x => x.RequestServices)
.Returns(serviceProviderMock.Object);

var modelState = new ModelStateDictionary();

var actionContext = CreateActionContext(httpContextMock, modelState);
var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(ValidateReCaptchaAttributeTests).GetTypeInfo(),
};

var actionContext = CreateActionContext(httpContextMock, modelState, actionDescriptor);

var actionExecutingContext = CreateActionExecutingContext(httpContextMock, actionContext, expected);

Expand All @@ -68,6 +82,61 @@ Task<ActionExecutedContext> Next()
if(!success)
Assert.Equal(1, modelState.ErrorCount);
}

[Fact]
public async Task VerifyAsyncLocalizesErrorMessage()
{
var reCaptchaServiceMock = new Mock<IReCaptchaService>();

reCaptchaServiceMock.Setup(x => x.VerifyAsync(It.IsAny<string>())).Returns(Task.FromResult(false));

var filter = new ValidateRecaptchaFilter(reCaptchaServiceMock.Object, "", null);

var expected = new StringValues("123");

var stringLocalizerMock = new Mock<IStringLocalizer>();
stringLocalizerMock.Setup(x => x[ValidateReCaptchaAttribute.DefaultErrorMessage])
.Returns(new LocalizedString("", "Localized error message"));

var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
stringLocalizerFactory.Setup(x => x.Create(It.IsAny<Type>()))
.Returns(stringLocalizerMock.Object);

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock.Setup(x => x.GetService(typeof(IStringLocalizerFactory)))
.Returns(stringLocalizerFactory.Object);

serviceProviderMock.Setup(x => x.GetService(typeof(IOptions<ReCaptchaSettings>)))
.Returns(new OptionsWrapper<ReCaptchaSettings>(new ReCaptchaSettings { LocalizerProvider = (type, factory) => factory.Create(type) }));

var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.RequestServices)
.Returns(serviceProviderMock.Object);

var modelState = new ModelStateDictionary();

var actionDescriptor = new ControllerActionDescriptor
{
ControllerTypeInfo = typeof(ValidateReCaptchaAttributeTests).GetTypeInfo(),
};

var actionContext = CreateActionContext(httpContextMock, modelState, actionDescriptor);

var actionExecutingContext = CreateActionExecutingContext(httpContextMock, actionContext, expected);

Task<ActionExecutedContext> Next()
{
var ctx = new ActionExecutedContext(actionContext, new List<IFilterMetadata>(), Mock.Of<Controller>());
return Task.FromResult(ctx);
}

await filter.OnActionExecutionAsync(actionExecutingContext, Next);
reCaptchaServiceMock.Verify(x => x.VerifyAsync(It.IsAny<string>()), Times.Once);

Assert.Equal(1, modelState.ErrorCount);
var errorMessage = modelState.First(x => x.Key == "Recaptcha").Value.Errors.Single().ErrorMessage;
Assert.Equal("Localized error message", errorMessage);
}
}

public class OnPageHandlerExecutionAsync : ValidateReCaptchaAttributeTests
Expand Down Expand Up @@ -98,8 +167,12 @@ public async Task VerifyAsyncReturnsBoolean(bool success)

var expected = new StringValues("123");

var serviceProviderMock = new Mock<IServiceProvider>();

var httpContextMock = new Mock<HttpContext>();

httpContextMock.Setup(x => x.RequestServices)
.Returns(serviceProviderMock.Object);

var pageContext = CreatePageContext(new ActionContext(httpContextMock.Object, new RouteData(), new ActionDescriptor()));

var model = new Mock<PageModel>();
Expand All @@ -117,6 +190,65 @@ public async Task VerifyAsyncReturnsBoolean(bool success)
await filter.OnPageHandlerExecutionAsync(actionExecutingContext, next);
reCaptchaServiceMock.Verify(x => x.VerifyAsync(It.IsAny<string>()), Times.Once);
}

[Fact]
public async Task VerifyAsyncLocalizesErrorMessage()
{
var reCaptchaServiceMock = new Mock<IReCaptchaService>();

reCaptchaServiceMock.Setup(x => x.VerifyAsync(It.IsAny<string>())).Returns(Task.FromResult(false));

var filter = new ValidateRecaptchaFilter(reCaptchaServiceMock.Object, "", "Custom Error Message");

var expected = new StringValues("123");

var stringLocalizerMock = new Mock<IStringLocalizer>();
stringLocalizerMock.Setup(x => x["Custom Error Message"])
.Returns(new LocalizedString("", "Localized error message"));

var stringLocalizerFactory = new Mock<IStringLocalizerFactory>();
stringLocalizerFactory.Setup(x => x.Create(It.IsAny<Type>()))
.Returns(stringLocalizerMock.Object);

var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock.Setup(x => x.GetService(typeof(IStringLocalizerFactory)))
.Returns(stringLocalizerFactory.Object);

serviceProviderMock.Setup(x => x.GetService(typeof(IOptions<ReCaptchaSettings>)))
.Returns(new OptionsWrapper<ReCaptchaSettings>(new ReCaptchaSettings { LocalizerProvider = (type, factory) => factory.Create(type) }));

var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(x => x.RequestServices)
.Returns(serviceProviderMock.Object);

var modelState = new ModelStateDictionary();

var actionDescriptor = new CompiledPageActionDescriptor
{
HandlerTypeInfo = typeof(ValidateReCaptchaAttributeTests).GetTypeInfo(),
};

var pageContext = CreatePageContext(new ActionContext(httpContextMock.Object, new RouteData(), actionDescriptor, modelState));

var model = new Mock<PageModel>();

var pageHandlerExecutedContext = new PageHandlerExecutedContext(
pageContext,
Array.Empty<IFilterMetadata>(),
new HandlerMethodDescriptor(),
model.Object);

var actionExecutingContext = CreatePageHandlerExecutingContext(httpContextMock, pageContext, expected, model);

PageHandlerExecutionDelegate next = () => Task.FromResult(pageHandlerExecutedContext);

await filter.OnPageHandlerExecutionAsync(actionExecutingContext, next);
reCaptchaServiceMock.Verify(x => x.VerifyAsync(It.IsAny<string>()), Times.Once);

Assert.Equal(1, modelState.ErrorCount);
var errorMessage = modelState.First(x => x.Key == "Recaptcha").Value.Errors.Single().ErrorMessage;
Assert.Equal("Localized error message", errorMessage);
}
}
}
}
6 changes: 3 additions & 3 deletions AspNetCore.ReCaptcha/AspNetCore.ReCaptcha.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<PackageId>AspNetCore.ReCaptcha</PackageId>
<Version>1.3.0</Version>
<Version>1.4.0</Version>
<Authors>Michaelvs97,sleeuwen</Authors>
<Description>Google ReCAPTCHA v2/v3 Library for .NET Core 3.1 and .NET 5.0/6.0</Description>
<PackageDescription>Google ReCAPTCHA v2/v3 Library for .NET Core 3.1 and .NET 5.0/6.0</PackageDescription>
Expand All @@ -21,8 +21,8 @@
</ItemGroup>

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\logo.png" Pack="true" PackagePath="\"/>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\logo.png" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
21 changes: 19 additions & 2 deletions AspNetCore.ReCaptcha/ReCaptchaHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,34 @@ namespace AspNetCore.ReCaptcha
[ExcludeFromCodeCoverage]
public static class ReCaptchaHelper
{
private static void AddReCaptchaServices(this IServiceCollection services)
{
services.PostConfigure<ReCaptchaSettings>(settings =>
{
settings.LocalizerProvider ??= (modelType, localizerFactory) => localizerFactory.Create(modelType);
});
services.AddHttpClient<IReCaptchaService, ReCaptchaService>();
}

public static IServiceCollection AddReCaptcha(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<ReCaptchaSettings>(configuration);
services.AddHttpClient<IReCaptchaService, ReCaptchaService>();
services.AddReCaptchaServices();
return services;
}

public static IServiceCollection AddReCaptcha(this IServiceCollection services, Action<ReCaptchaSettings> configureOptions)
{
services.Configure(configureOptions);
services.AddHttpClient<IReCaptchaService, ReCaptchaService>();
services.AddReCaptchaServices();
return services;
}

public static IServiceCollection AddReCaptcha(this IServiceCollection services, IConfiguration configuration, Action<ReCaptchaSettings> configureOptions)
{
services.Configure<ReCaptchaSettings>(configuration);
services.Configure(configureOptions);
services.AddReCaptchaServices();
return services;
}

Expand Down
5 changes: 4 additions & 1 deletion AspNetCore.ReCaptcha/ReCaptchaSettings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;

namespace AspNetCore.ReCaptcha
{
Expand All @@ -10,5 +12,6 @@ public class ReCaptchaSettings
public ReCaptchaVersion Version { get; set; }
public bool UseRecaptchaNet { get; set; }
public float ScoreThreshold { get; set; } = 0.5f;
public Func<Type, IStringLocalizerFactory, IStringLocalizer> LocalizerProvider { get; set; }
}
}
17 changes: 17 additions & 0 deletions AspNetCore.ReCaptcha/Resources/strings.nl.resx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Your request cannot be completed because you failed Recaptcha verification." xml:space="preserve">
<value>Het formulier kon niet worden verzonden omdat de ReCaptcha niet is gelukt.</value>
</data>
</root>
24 changes: 24 additions & 0 deletions AspNetCore.ReCaptcha/Resources/strings.resx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>

<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">

</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Your request cannot be completed because you failed Recaptcha verification." xml:space="preserve">
<value>Your request cannot be completed because you failed Recaptcha verification.</value>
</data>
</root>
Loading

0 comments on commit fb9242a

Please sign in to comment.