();
+ }
+}
+
diff --git a/src/OrchardCoreContrib.CloudflareTurnstile/TurnstilePermissions.cs b/src/OrchardCoreContrib.CloudflareTurnstile/TurnstilePermissions.cs
new file mode 100644
index 00000000..b0d293f1
--- /dev/null
+++ b/src/OrchardCoreContrib.CloudflareTurnstile/TurnstilePermissions.cs
@@ -0,0 +1,8 @@
+using OrchardCore.Security.Permissions;
+
+namespace OrchardCoreContrib.CloudflareTurnstile;
+
+public static class TurnstilePermissions
+{
+ public static readonly Permission ManageTurnstileSettings = new("ManageTurnstileSettings", "Manage Turnstile Settings");
+}
diff --git a/src/OrchardCoreContrib.CloudflareTurnstile/ViewModels/TurnstileSettingsViewModel.cs b/src/OrchardCoreContrib.CloudflareTurnstile/ViewModels/TurnstileSettingsViewModel.cs
new file mode 100644
index 00000000..b670e131
--- /dev/null
+++ b/src/OrchardCoreContrib.CloudflareTurnstile/ViewModels/TurnstileSettingsViewModel.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace OrchardCoreContrib.CloudflareTurnstile.ViewModels;
+
+public class TurnstileSettingsViewModel
+{
+ [Required]
+ public string SiteKey { get; set; }
+
+ [Required]
+ public string SecretKey { get; set; }
+
+ public string Theme { get; set; }
+
+ public string Size { get; set; }
+}
+
diff --git a/src/OrchardCoreContrib.CloudflareTurnstile/Views/NavigationItemText-turnstile.Id.cshtml b/src/OrchardCoreContrib.CloudflareTurnstile/Views/NavigationItemText-turnstile.Id.cshtml
new file mode 100644
index 00000000..d1557514
--- /dev/null
+++ b/src/OrchardCoreContrib.CloudflareTurnstile/Views/NavigationItemText-turnstile.Id.cshtml
@@ -0,0 +1,4 @@
+
+
+
+@T["Cloudflare Turnstile"]
diff --git a/src/OrchardCoreContrib.CloudflareTurnstile/Views/TurnstileSettings.Edit.cshtml b/src/OrchardCoreContrib.CloudflareTurnstile/Views/TurnstileSettings.Edit.cshtml
new file mode 100644
index 00000000..9d8c6d3f
--- /dev/null
+++ b/src/OrchardCoreContrib.CloudflareTurnstile/Views/TurnstileSettings.Edit.cshtml
@@ -0,0 +1,38 @@
+@model TurnstileSettingsViewModel
+@using OrchardCore.DisplayManagement.Views
+@using OrchardCoreContrib.CloudflareTurnstile.ViewModels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @T["Choose the Turnstile widget theme."]
+
+
+
+
+
+
+
+ @T["Choose the Turnstile widget size."]
+
+
diff --git a/src/OrchardCoreContrib.CloudflareTurnstile/Views/_ViewImports.cshtml b/src/OrchardCoreContrib.CloudflareTurnstile/Views/_ViewImports.cshtml
new file mode 100644
index 00000000..61b0660c
--- /dev/null
+++ b/src/OrchardCoreContrib.CloudflareTurnstile/Views/_ViewImports.cshtml
@@ -0,0 +1,4 @@
+@inherits OrchardCore.DisplayManagement.Razor.RazorPage
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@addTagHelper *, OrchardCore.DisplayManagement
+@addTagHelper *, OrchardCore.ResourceManagement
diff --git a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj
index 4e2f9f0c..119fc20b 100644
--- a/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj
+++ b/src/OrchardCoreContrib.Modules.Web/OrchardCoreContrib.Modules.Web.csproj
@@ -8,6 +8,7 @@
+
diff --git a/test/OrchardCoreContrib.CloudflareTurnstile.Tests/OrchardCoreContrib.CloudflareTurnstile.Tests.csproj b/test/OrchardCoreContrib.CloudflareTurnstile.Tests/OrchardCoreContrib.CloudflareTurnstile.Tests.csproj
new file mode 100644
index 00000000..0ad711ec
--- /dev/null
+++ b/test/OrchardCoreContrib.CloudflareTurnstile.Tests/OrchardCoreContrib.CloudflareTurnstile.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OrchardCoreContrib.CloudflareTurnstile.Tests/Services/TurnstileServiceTests.cs b/test/OrchardCoreContrib.CloudflareTurnstile.Tests/Services/TurnstileServiceTests.cs
new file mode 100644
index 00000000..c7ea0f3c
--- /dev/null
+++ b/test/OrchardCoreContrib.CloudflareTurnstile.Tests/Services/TurnstileServiceTests.cs
@@ -0,0 +1,85 @@
+using Microsoft.Extensions.Options;
+using Moq;
+using Moq.Protected;
+using OrchardCoreContrib.CloudflareTurnstile.Configuration;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+
+namespace OrchardCoreContrib.CloudflareTurnstile.Services.Tests;
+
+public class TurnstileServiceTests
+{
+ private static readonly IOptions _turnstileOptions = Options.Create(new TurnstileOptions
+ {
+ SecretKey = "secret"
+ });
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ValidateAsync_ReturnsBasedOnResponseSuccessResult(bool success)
+ {
+ // Arrange
+ var content = JsonSerializer.Serialize(new TurnstileResponse { Success = success });
+
+ var responseMessage = new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(content , Encoding.UTF8, "application/json")
+ };
+
+ var service = new TurnstileService(CreateHttpClientFactory(responseMessage), _turnstileOptions);
+
+ // Act
+ var result = await service.ValidateAsync("token");
+
+ // Assert
+ Assert.Equal(success, result);
+ }
+
+ [Fact]
+ public async Task ValidateAsync_ReturnsFalse_WhenApiReturnsNonSuccessStatusCode()
+ {
+ // Arrange
+ var service = new TurnstileService(CreateHttpClientFactory(HttpStatusCode.BadRequest), _turnstileOptions);
+
+ // Act
+ var result = await service.ValidateAsync("token");
+
+ // Assert
+ Assert.False(result);
+ }
+
+ private sealed class CaptureHandler(HttpResponseMessage response) : HttpMessageHandler
+ {
+ public HttpRequestMessage Request { get; private set; }
+
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ Request = request;
+
+ return Task.FromResult(response);
+ }
+ }
+
+ private static IHttpClientFactory CreateHttpClientFactory(HttpResponseMessage httpReponseMessage)
+ {
+ var httpMessageHandlerMock = new Mock();
+
+ httpMessageHandlerMock.Protected()
+ .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny())
+ .ReturnsAsync(httpReponseMessage);
+
+ var client = new HttpClient(httpMessageHandlerMock.Object);
+
+ var httpClientFactoryMock = new Mock();
+
+ httpClientFactoryMock.Setup(x => x.CreateClient(It.IsAny()))
+ .Returns(client);
+
+ return httpClientFactoryMock.Object;
+ }
+
+ private static IHttpClientFactory CreateHttpClientFactory(HttpStatusCode httpStatusCode)
+ => CreateHttpClientFactory(new HttpResponseMessage(httpStatusCode));
+}