diff --git a/src/Captcha.Core/Constants.cs b/src/Captcha.Core/Constants.cs
index 019d738..354a78f 100644
--- a/src/Captcha.Core/Constants.cs
+++ b/src/Captcha.Core/Constants.cs
@@ -4,8 +4,6 @@ namespace Captcha.Core;
public static class Constants
{
- public const int MinCaptchaSize = 10;
- public const int MaxCaptchaSize = 1024;
public const int DefaultCaptchaWidth = 400;
public const int DefaultCaptchaHeight = 100;
public const float DefaultFrequency = 100F;
@@ -16,11 +14,15 @@ public static class Constants
public const float WarpCaptchaTextFrequency = 4F;
public const int CaptchaNoise = 50;
- // According to https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sktypeface, this should be thread-safe
+ ///
+ /// This object is shared across threads, however according to https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sktypeface it is fine to share it across threads.
+ ///
public static SKTypeface MainFontTypeface { get; } =
SKTypeface.FromStream(typeof(Constants).Assembly.GetManifestResourceStream("Captcha.Core.Resources.Fonts.Caveat-SemiBold.ttf"));
- // According to https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sktypeface, this should be thread-safe
+ ///
+ /// This object is shared across threads, however according to https://learn.microsoft.com/en-us/dotnet/api/skiasharp.sktypeface it is fine to share it across threads.
+ ///
public static SKTypeface FallbackFontTypeface { get; } =
SKTypeface.FromStream(typeof(Constants).Assembly.GetManifestResourceStream("Captcha.Core.Resources.Fonts.Unifont.ttf"));
}
diff --git a/src/Captcha.Core/Mappers/RequestToDomainMapper.cs b/src/Captcha.Core/Mappers/RequestToDomainMapper.cs
index 805c939..01c537d 100644
--- a/src/Captcha.Core/Mappers/RequestToDomainMapper.cs
+++ b/src/Captcha.Core/Mappers/RequestToDomainMapper.cs
@@ -6,7 +6,23 @@ namespace Captcha.Core.Mappers;
public class RequestToDomainMapper
{
- public CaptchaConfigurationData ToDomain(CaptchaRequest request)
+ public CaptchaConfigurationData ToDomain(GetCreateCaptchaRequest request)
+ {
+ var width = request.Width ?? Constants.DefaultCaptchaWidth;
+ var height = request.Height ?? Constants.DefaultCaptchaHeight;
+
+ return new CaptchaConfigurationData
+ {
+ Text = request.Text,
+ Width = width,
+ Height = height,
+ Frequency = GetFrequency(request.Difficulty, width, height),
+ PrimaryColor = GetColor(request.Theme?.PrimaryColor) ?? Constants.DefaultPrimaryColor,
+ SecondaryColor = GetColor(request.Theme?.SecondaryColor) ?? Constants.DefaultSecondaryColor,
+ };
+ }
+
+ public CaptchaConfigurationData ToDomain(PostCreateCaptchaRequest request)
{
var width = request.Width ?? Constants.DefaultCaptchaWidth;
var height = request.Height ?? Constants.DefaultCaptchaHeight;
diff --git a/src/Captcha.Core/Models/CaptchaRequest.cs b/src/Captcha.Core/Models/GetCreateCaptchaRequest.cs
similarity index 87%
rename from src/Captcha.Core/Models/CaptchaRequest.cs
rename to src/Captcha.Core/Models/GetCreateCaptchaRequest.cs
index 98a88dc..3391101 100644
--- a/src/Captcha.Core/Models/CaptchaRequest.cs
+++ b/src/Captcha.Core/Models/GetCreateCaptchaRequest.cs
@@ -1,6 +1,6 @@
namespace Captcha.Core.Models;
-public record CaptchaRequest
+public record GetCreateCaptchaRequest
{
public required string Text { get; init; }
public int? Width { get; init; }
diff --git a/src/Captcha.Core/Models/PostCreateCaptchaRequest.cs b/src/Captcha.Core/Models/PostCreateCaptchaRequest.cs
new file mode 100644
index 0000000..e1b1ae5
--- /dev/null
+++ b/src/Captcha.Core/Models/PostCreateCaptchaRequest.cs
@@ -0,0 +1,10 @@
+namespace Captcha.Core.Models;
+
+public record PostCreateCaptchaRequest
+{
+ public required string Text { get; init; }
+ public int? Width { get; init; }
+ public int? Height { get; init; }
+ public CaptchaDifficulty? Difficulty { get; init; }
+ public ThemeConfiguration? Theme { get; set; }
+}
diff --git a/src/Captcha.WebApi/Controllers/CaptchaController.cs b/src/Captcha.WebApi/Controllers/CaptchaController.cs
index 33afac3..f85941f 100644
--- a/src/Captcha.WebApi/Controllers/CaptchaController.cs
+++ b/src/Captcha.WebApi/Controllers/CaptchaController.cs
@@ -13,18 +13,37 @@ namespace Captcha.WebApi.Controllers;
[Route("[controller]")]
public class CaptchaController(ICaptchaImageService captchaImageService, RequestToDomainMapper requestToDomainMapper) : ControllerBase
{
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task GetCreateAsync([FromQuery] GetCreateCaptchaRequest request)
+ {
+ var domain = requestToDomainMapper.ToDomain(request);
+
+ using var created = captchaImageService.CreateCaptchaImage(domain);
+
+ return await SerializeToJpegFile(created);
+ }
+
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
- [SwaggerRequestExample(typeof(CaptchaRequest), typeof(CreateCaptchaExamples))]
- public async Task CreateAsync(CaptchaRequest request)
+ [SwaggerRequestExample(typeof(PostCreateCaptchaRequest), typeof(CreateCaptchaExamples))]
+ public async Task PostCreateAsync(PostCreateCaptchaRequest request)
{
var domain = requestToDomainMapper.ToDomain(request);
+
using var created = captchaImageService.CreateCaptchaImage(domain);
+ return await SerializeToJpegFile(created);
+ }
+
+ private static async Task SerializeToJpegFile(SKBitmap image)
+ {
await using var memoryStream = new MemoryStream();
- SKImage.FromBitmap(created)
+ SKImage.FromBitmap(image)
.Encode(SKEncodedImageFormat.Jpeg, 100)
.SaveTo(memoryStream);
diff --git a/src/Captcha.WebApi/Controllers/Examples/CreateCaptchaExamples.cs b/src/Captcha.WebApi/Controllers/Examples/CreateCaptchaExamples.cs
index c4c3315..1e9497e 100644
--- a/src/Captcha.WebApi/Controllers/Examples/CreateCaptchaExamples.cs
+++ b/src/Captcha.WebApi/Controllers/Examples/CreateCaptchaExamples.cs
@@ -3,14 +3,14 @@ namespace Captcha.WebApi.Controllers.Examples;
using Captcha.Core.Models;
using Swashbuckle.AspNetCore.Filters;
-public record CreateCaptchaExamples : IMultipleExamplesProvider
+public record CreateCaptchaExamples : IMultipleExamplesProvider
{
- public IEnumerable> GetExamples()
+ public IEnumerable> GetExamples()
{
yield return SwaggerExample.Create(
"Example 1",
"Example 1 - Create captcha with text",
- new CaptchaRequest
+ new PostCreateCaptchaRequest
{
Text = "hello world"
});
@@ -18,7 +18,7 @@ public IEnumerable> GetExamples()
yield return SwaggerExample.Create(
"Example 2",
"Example 2 - Create challenging captcha with text",
- new CaptchaRequest
+ new PostCreateCaptchaRequest
{
Text = "hello world",
Difficulty = CaptchaDifficulty.Challenging
@@ -27,7 +27,7 @@ public IEnumerable> GetExamples()
yield return SwaggerExample.Create(
"Example 3",
"Example 3 - Create hard captcha with text",
- new CaptchaRequest
+ new PostCreateCaptchaRequest
{
Text = "hello world",
Difficulty = CaptchaDifficulty.Hard
@@ -36,7 +36,7 @@ public IEnumerable> GetExamples()
yield return SwaggerExample.Create(
"Example 4",
"Example 4 - Create captcha with text and height and width",
- new CaptchaRequest
+ new PostCreateCaptchaRequest
{
Text = "world",
Height = 300,
@@ -46,7 +46,7 @@ public IEnumerable> GetExamples()
yield return SwaggerExample.Create(
"Example 5",
"Example 5 - Create captcha with a color theme",
- new CaptchaRequest
+ new PostCreateCaptchaRequest
{
Text = "hello world",
Theme = new ThemeConfiguration
diff --git a/tests/Captcha.FunctionalTests/Features/CaptchaGetEndpoint.feature b/tests/Captcha.FunctionalTests/Features/CaptchaGetEndpoint.feature
new file mode 100644
index 0000000..13352b5
--- /dev/null
+++ b/tests/Captcha.FunctionalTests/Features/CaptchaGetEndpoint.feature
@@ -0,0 +1,32 @@
+Feature: CaptchaGet
+I want to send different captcha requests and assure the image is generated
+
+ Scenario Outline: Send captcha requests
+ Given I have a captcha request using get with following parameters:
+ | Text | Width | Height | Difficulty |
+ | | | | |
+ When I send the get request to the Create endpoint of the CaptchaController
+ Then I expect a captcha image to be returned with the following attributes:
+ | Width | Height |
+ | | |
+ Then I expect a captcha image to contain at least '' pixels of color '' and at least '' pixels of color ''
+
+ Examples:
+ | Text | Width | Height | Difficulty | ExpectedWidth | ExpectedHeight | FirstColorPixels | FirstColorHex | SecondColorPixels | SecondColorHex |
+ | مرحبًا | | | | 400 | 100 | 2300 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | 你好 | | | | 400 | 100 | 2500 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | こんにちは | | | | 400 | 100 | 2500 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | 안녕하세요 | | | | 400 | 100 | 3000 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Здравствуйте | | | | 400 | 100 | 3000 | #FFD3D3D3 | 28000 | #FFFFFFFF |
+ | Bonjour | | | | 400 | 100 | 3000 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Guten Tag | | | | 400 | 100 | 3000 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Selam | | | | 400 | 100 | 2500 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Γεια σας | | | | 400 | 100 | 3000 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Lorem | | | | 400 | 100 | 2500 | #FFD3D3D3 | 30000 | #FFFFFFFF |
+ | Ipsum | | 200 | | 400 | 200 | 6000 | #FFD3D3D3 | 60000 | #FFFFFFFF |
+ | helloworld | 200 | | Easy | 200 | 100 | 550 | #FFD3D3D3 | 10000 | #FFFFFFFF |
+ | bar | 300 | 300 | Medium | 300 | 300 | 4500 | #FFD3D3D3 | 70000 | #FFFFFFFF |
+ | foo | 400 | 400 | Hard | 400 | 400 | 6000 | #FFD3D3D3 | 110000 | #FFFFFFFF |
+ | Ciao | 200 | | Easy | 200 | 100 | 850 | #FFD3D3D3 | 10000 | #FFFFFFFF |
+ | Olá | 300 | 300 | Challenging | 300 | 300 | 6000 | #FFD3D3D3 | 64000 | #FFFFFFFF |
+ | สวัสดี | 400 | 400 | Hard | 400 | 400 | 12000 | #FFD3D3D3 | 120000 | #FFFFFFFF |
diff --git a/tests/Captcha.FunctionalTests/Features/Captcha.feature b/tests/Captcha.FunctionalTests/Features/CaptchaPostEndpoint.feature
similarity index 99%
rename from tests/Captcha.FunctionalTests/Features/Captcha.feature
rename to tests/Captcha.FunctionalTests/Features/CaptchaPostEndpoint.feature
index 7a60723..4220a1b 100644
--- a/tests/Captcha.FunctionalTests/Features/Captcha.feature
+++ b/tests/Captcha.FunctionalTests/Features/CaptchaPostEndpoint.feature
@@ -1,4 +1,4 @@
-Feature: Captcha
+Feature: CaptchaPost
I want to send different captcha requests and assure the image is generated
Scenario Outline: Send captcha requests
diff --git a/tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs b/tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs
index fcf515d..89de591 100644
--- a/tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs
+++ b/tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs
@@ -11,7 +11,9 @@ namespace Captcha.FunctionalTests.StepDefinitions;
[Binding]
public class CaptchaSteps(ScenarioContext context) : TestBase(context)
{
- private CaptchaRequest? _request;
+ private GetCreateCaptchaRequest? _getRequest;
+ private PostCreateCaptchaRequest? _postRequest;
+
private RestResponse? _response;
[Given(@"I have a captcha request with following parameters:")]
@@ -19,7 +21,7 @@ public void GivenIHaveACaptchaRequestWithFollowingParameters(Table table)
{
var row = table.Rows[0];
- _request = new CaptchaRequest
+ _postRequest = new PostCreateCaptchaRequest
{
Text = row[TestConstants.Text],
Width = string.IsNullOrEmpty(row[TestConstants.Width])
@@ -50,7 +52,7 @@ public async Task WhenISendTheRequestToTheCreateEndpointOfTheCaptchaController()
{
RequestFormat = DataFormat.Json,
Method = Method.Post
- }.AddJsonBody(_request);
+ }.AddJsonBody(_postRequest);
_response = await Client.ExecuteAsync(request);
}
@@ -128,4 +130,42 @@ public void ThenIExpectACaptchaImageToContainPixelsOfColorAndPixelsOfColor
Assert.That(firstColorActualAmount, Is.AtLeast(firstColorExpectedAmount));
Assert.That(secondColorActualAmount, Is.AtLeast(secondColorExpectedAmount));
}
+
+ [Given("I have a captcha request using get with following parameters:")]
+ public void GivenIHaveACaptchaRequestUsingGetWithFollowingParameters(Table table)
+ {
+ var row = table.Rows[0];
+
+ _getRequest = new GetCreateCaptchaRequest
+ {
+ Text = row[TestConstants.Text],
+ Width = string.IsNullOrEmpty(row[TestConstants.Width])
+ ? null
+ : int.Parse(row[TestConstants.Width], CultureInfo.InvariantCulture),
+ Height = string.IsNullOrEmpty(row[TestConstants.Height])
+ ? null
+ : int.Parse(row[TestConstants.Height], CultureInfo.InvariantCulture),
+ Difficulty = string.IsNullOrEmpty(row[TestConstants.Difficulty])
+ ? null
+ : Enum.Parse(row[TestConstants.Difficulty], true),
+ Theme = new ThemeConfiguration()
+ {
+ PrimaryColor = !row.ContainsKey(TestConstants.PrimaryColor)
+ ? null
+ : row[TestConstants.PrimaryColor],
+ SecondaryColor = !row.ContainsKey(TestConstants.SecondaryColor)
+ ? null
+ : row[TestConstants.SecondaryColor]
+ }
+ };
+ }
+
+ [When("I send the get request to the Create endpoint of the CaptchaController")]
+ public async Task WhenISendTheGetRequestToTheCreateEndpointOfTheCaptchaController()
+ {
+ var request = new RestRequest(TestConstants.CreateCaptchaEndpoint)
+ .AddObject(_getRequest);
+
+ _response = await Client.ExecuteAsync(request);
+ }
}
diff --git a/tests/Captcha.UnitTests/ConstantsTests.cs b/tests/Captcha.UnitTests/ConstantsTests.cs
index 3091888..8a23faa 100644
--- a/tests/Captcha.UnitTests/ConstantsTests.cs
+++ b/tests/Captcha.UnitTests/ConstantsTests.cs
@@ -7,11 +7,6 @@ namespace Captcha.UnitTests;
[TestFixture]
public class ConstantsTests
{
- [Test]
- public void MaxCaptchaSizeShouldBe1024() => Assert.That(Constants.MaxCaptchaSize, Is.EqualTo(1024));
-
- [Test]
- public void MinCaptchaSizeShouldBe10() => Assert.That(Constants.MinCaptchaSize, Is.EqualTo(10));
[Test]
public void DefaultCaptchaWidthShouldBe400() => Assert.That(Constants.DefaultCaptchaWidth, Is.EqualTo(400));
diff --git a/tests/Captcha.UnitTests/Mappers/RequestToDomainMapperTests.cs b/tests/Captcha.UnitTests/Mappers/RequestToDomainMapperTests.cs
index 1f3bf5a..8342239 100644
--- a/tests/Captcha.UnitTests/Mappers/RequestToDomainMapperTests.cs
+++ b/tests/Captcha.UnitTests/Mappers/RequestToDomainMapperTests.cs
@@ -17,7 +17,7 @@ public class RequestToDomainMapperTests
public void ToDomainMapsTextCorrectly()
{
// Arrange
- var request = new CaptchaRequest { Text = "test text" };
+ var request = new PostCreateCaptchaRequest { Text = "test text" };
// Act
var result = _requestToDomainMapper.ToDomain(request);
@@ -30,7 +30,7 @@ public void ToDomainMapsTextCorrectly()
public void ToDomainUsesDefaultWidthAndHeightWhenNotProvided()
{
// Arrange
- var request = new CaptchaRequest { Text = "some text" };
+ var request = new PostCreateCaptchaRequest { Text = "some text" };
// Act
var result = _requestToDomainMapper.ToDomain(request);
@@ -47,7 +47,7 @@ public void ToDomainUsesDefaultWidthAndHeightWhenNotProvided()
public void ToDomainUsesProvidedWidthAndHeight()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Width = 500,
Height = 300,
@@ -69,7 +69,7 @@ public void ToDomainUsesProvidedWidthAndHeight()
public void ToDomainUsesDefaultFrequencyWhenDifficultyNotProvided()
{
// Arrange
- var request = new CaptchaRequest { Text = "default freq" };
+ var request = new PostCreateCaptchaRequest { Text = "default freq" };
// Act
var result = _requestToDomainMapper.ToDomain(request);
@@ -82,25 +82,25 @@ public void ToDomainUsesDefaultFrequencyWhenDifficultyNotProvided()
public void ToDomainUsesCorrectFrequencyForDifficulty()
{
// Arrange
- var easyRequest = new CaptchaRequest
+ var easyRequest = new PostCreateCaptchaRequest
{
Difficulty = CaptchaDifficulty.Easy,
Text = "easy"
};
- var mediumRequest = new CaptchaRequest
+ var mediumRequest = new PostCreateCaptchaRequest
{
Difficulty = CaptchaDifficulty.Medium,
Text = "medium"
};
- var challengingRequest = new CaptchaRequest
+ var challengingRequest = new PostCreateCaptchaRequest
{
Difficulty = CaptchaDifficulty.Challenging,
Text = "challenging"
};
- var hardRequest = new CaptchaRequest
+ var hardRequest = new PostCreateCaptchaRequest
{
Difficulty = CaptchaDifficulty.Hard,
Text = "hard"
@@ -126,7 +126,7 @@ public void ToDomainUsesCorrectFrequencyForDifficulty()
public void ToDomainUsesDefaultFrequencyForUnrecognizedDifficulty()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Difficulty = (CaptchaDifficulty)999,
Text = "unknown difficulty"
@@ -143,7 +143,7 @@ public void ToDomainUsesDefaultFrequencyForUnrecognizedDifficulty()
public void ToDomainMapsMultipleRequestsCorrectly()
{
// Arrange
- var requests = new List
+ var requests = new List
{
new()
{
@@ -183,7 +183,7 @@ public void ToDomainMapsMultipleRequestsCorrectly()
public void ToDomainThrowsWhenRequestIsNull()
{
// Arrange
- CaptchaRequest request = null;
+ PostCreateCaptchaRequest request = null;
// Act & Assert
Assert.Throws(() => _requestToDomainMapper.ToDomain(request));
@@ -193,7 +193,7 @@ public void ToDomainThrowsWhenRequestIsNull()
public void ToDomainUsesDefaultPrimaryColorWhenNotProvided()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "text"
};
@@ -213,7 +213,7 @@ public void ToDomainUsesDefaultPrimaryColorWhenNotProvided()
public void ToDomainHandlesNullText()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = null,
Width = 100,
@@ -236,7 +236,7 @@ public void ToDomainHandlesNullText()
public void ToDomainHandlesExcessivelyLargeWidthAndHeight()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "Large dimensions",
Width = int.MaxValue,
@@ -259,7 +259,7 @@ public void ToDomainPreservesWhitespaceInText()
{
// Arrange
var originalText = " Leading, internal and trailing whitespace ";
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = originalText
};
@@ -277,7 +277,7 @@ public void ToDomainPreservesUnicodeText()
{
// Arrange
var unicodeString = "Captcha 🚀 Test – 你好";
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = unicodeString
};
@@ -293,7 +293,7 @@ public void ToDomainPreservesUnicodeText()
public void ToDomainAllowsEmptyText()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = string.Empty
};
@@ -310,7 +310,7 @@ public void ToDomainCanHandleExtremelyLongText()
{
// Arrange
var longText = new string('x', 10_000);
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = longText
};
@@ -327,7 +327,7 @@ public void ToDomainCanHandleExtremelyLongText()
public void ToDomainUsesDefaultHeightWhenOnlyWidthProvided()
{
// Arrange
- var request = new CaptchaRequest { Text = "w-only", Width = 321 };
+ var request = new PostCreateCaptchaRequest { Text = "w-only", Width = 321 };
// Act
var result = _requestToDomainMapper.ToDomain(request);
@@ -344,7 +344,7 @@ public void ToDomainUsesDefaultHeightWhenOnlyWidthProvided()
public void ToDomainUsesDefaultWidthWhenOnlyHeightProvided()
{
// Arrange
- var request = new CaptchaRequest { Text = "h-only", Height = 654 };
+ var request = new PostCreateCaptchaRequest { Text = "h-only", Height = 654 };
// Act
var result = _requestToDomainMapper.ToDomain(request);
@@ -361,7 +361,7 @@ public void ToDomainUsesDefaultWidthWhenOnlyHeightProvided()
public void ToDomainUsesProvidedThemeColorsWhenValidHex()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "colors",
Theme = new()
@@ -386,7 +386,7 @@ public void ToDomainUsesProvidedThemeColorsWhenValidHex()
public void ToDomainParsesHexCaseInsensitively()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "test",
Theme = new()
@@ -418,7 +418,7 @@ public void ToDomainParsesHexCaseInsensitively()
public void ToDomainUsesDefaultColorsWhenThemeProvidedButEmptyOrWhitespace()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "empty theme",
Theme = new()
@@ -443,7 +443,7 @@ public void ToDomainUsesDefaultColorsWhenThemeProvidedButEmptyOrWhitespace()
public void ToDomainUsesDefaultSecondaryColorWhenOnlyPrimaryProvided()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "only primary",
Theme = new()
@@ -468,7 +468,7 @@ public void ToDomainUsesDefaultSecondaryColorWhenOnlyPrimaryProvided()
public void ToDomainUsesDefaultPrimaryColorWhenOnlySecondaryProvided()
{
// Arrange
- var request = new CaptchaRequest
+ var request = new PostCreateCaptchaRequest
{
Text = "only secondary",
Theme = new()
@@ -489,4 +489,479 @@ public void ToDomainUsesDefaultPrimaryColorWhenOnlySecondaryProvided()
}
}
+ [Test]
+ public void GetRequestDomainMapsTextCorrectly()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest { Text = "test text" };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Text, Is.EqualTo("test text"));
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultWidthAndHeightWhenNotProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest { Text = "some text" };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Width, Is.EqualTo(Constants.DefaultCaptchaWidth));
+ Assert.That(result.Height, Is.EqualTo(Constants.DefaultCaptchaHeight));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesProvidedWidthAndHeight()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Width = 500,
+ Height = 300,
+ Text = "another text"
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Width, Is.EqualTo(500));
+ Assert.That(result.Height, Is.EqualTo(300));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultFrequencyWhenDifficultyNotProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest { Text = "default freq" };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Frequency, Is.EqualTo(Constants.DefaultFrequency));
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesCorrectFrequencyForDifficulty()
+ {
+ // Arrange
+ var easyRequest = new GetCreateCaptchaRequest
+ {
+ Difficulty = CaptchaDifficulty.Easy,
+ Text = "easy"
+ };
+
+ var mediumRequest = new GetCreateCaptchaRequest
+ {
+ Difficulty = CaptchaDifficulty.Medium,
+ Text = "medium"
+ };
+
+ var challengingRequest = new GetCreateCaptchaRequest
+ {
+ Difficulty = CaptchaDifficulty.Challenging,
+ Text = "challenging"
+ };
+
+ var hardRequest = new GetCreateCaptchaRequest
+ {
+ Difficulty = CaptchaDifficulty.Hard,
+ Text = "hard"
+ };
+
+ // Act
+ var easyResult = _requestToDomainMapper.ToDomain(easyRequest);
+ var mediumResult = _requestToDomainMapper.ToDomain(mediumRequest);
+ var challengingResult = _requestToDomainMapper.ToDomain(challengingRequest);
+ var hardResult = _requestToDomainMapper.ToDomain(hardRequest);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(hardResult.Frequency, Is.EqualTo(20F), "Hard frequency");
+ Assert.That(easyResult.Frequency, Is.EqualTo(300F), "Easy frequency");
+ Assert.That(challengingResult.Frequency, Is.EqualTo(30F), "Challenging frequency");
+ Assert.That(mediumResult.Frequency, Is.EqualTo(Constants.DefaultFrequency), "Medium frequency");
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultFrequencyForUnrecognizedDifficulty()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Difficulty = (CaptchaDifficulty)999,
+ Text = "unknown difficulty"
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Frequency, Is.EqualTo(Constants.DefaultFrequency));
+ }
+
+ [Test]
+ public void GetRequestToDomainMapsMultipleRequestsCorrectly()
+ {
+ // Arrange
+ var requests = new List
+ {
+ new()
+ {
+ Text = "test1",
+ Width = 500,
+ Height = 200,
+ Difficulty = CaptchaDifficulty.Easy
+ },
+ new()
+ {
+ Text = "test2",
+ Width = 600,
+ Height = 300,
+ Difficulty = CaptchaDifficulty.Hard
+ },
+ };
+
+ // Act
+ var results = requests.Select(_requestToDomainMapper.ToDomain).ToList();
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(results[0].Text, Is.EqualTo(requests[0].Text));
+ Assert.That(results[0].Width, Is.EqualTo(requests[0].Width));
+ Assert.That(results[0].Height, Is.EqualTo(requests[0].Height));
+ Assert.That(results[0].Frequency, Is.EqualTo(600F));
+
+ Assert.That(results[1].Text, Is.EqualTo(requests[1].Text));
+ Assert.That(results[1].Width, Is.EqualTo(requests[1].Width));
+ Assert.That(results[1].Height, Is.EqualTo(requests[1].Height));
+ Assert.That(results[1].Frequency, Is.EqualTo(80F));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainThrowsWhenRequestIsNull()
+ {
+ // Arrange
+ GetCreateCaptchaRequest request = null;
+
+ // Act & Assert
+ Assert.Throws(() => _requestToDomainMapper.ToDomain(request));
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultPrimaryColorWhenNotProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "text"
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(Constants.DefaultPrimaryColor));
+ Assert.That(result.SecondaryColor, Is.EqualTo(Constants.DefaultSecondaryColor));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainHandlesNullText()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = null,
+ Width = 100,
+ Height = 50
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ using (Assert.EnterMultipleScope())
+ {
+ // Assert
+ Assert.That(result.Text, Is.Null);
+ Assert.That(result.Width, Is.EqualTo(100));
+ Assert.That(result.Height, Is.EqualTo(50));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainHandlesExcessivelyLargeWidthAndHeight()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "Large dimensions",
+ Width = int.MaxValue,
+ Height = int.MaxValue
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Width, Is.EqualTo(int.MaxValue));
+ Assert.That(result.Height, Is.EqualTo(int.MaxValue));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainPreservesWhitespaceInText()
+ {
+ // Arrange
+ var originalText = " Leading, internal and trailing whitespace ";
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = originalText
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Text, Is.EqualTo(originalText));
+ }
+
+
+ [Test]
+ public void GetRequestToDomainPreservesUnicodeText()
+ {
+ // Arrange
+ var unicodeString = "Captcha 🚀 Test – 你好";
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = unicodeString
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Text, Is.EqualTo(unicodeString));
+ }
+
+ [Test]
+ public void GetRequestToDomainAllowsEmptyText()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = string.Empty
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Text, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void GetRequestToDomainCanHandleExtremelyLongText()
+ {
+ // Arrange
+ var longText = new string('x', 10_000);
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = longText
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ Assert.That(result.Text, Has.Length.EqualTo(10_000));
+ Assert.That(result.Text, Is.EqualTo(longText));
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultHeightWhenOnlyWidthProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest { Text = "w-only", Width = 321 };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Width, Is.EqualTo(321));
+ Assert.That(result.Height, Is.EqualTo(Constants.DefaultCaptchaHeight));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultWidthWhenOnlyHeightProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest { Text = "h-only", Height = 654 };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.Width, Is.EqualTo(Constants.DefaultCaptchaWidth));
+ Assert.That(result.Height, Is.EqualTo(654));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesProvidedThemeColorsWhenValidHex()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "colors",
+ Theme = new()
+ {
+ PrimaryColor = "#112233",
+ SecondaryColor = "#AABBCC"
+ }
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(SkiaSharp.SKColor.Parse("#112233")));
+ Assert.That(result.SecondaryColor, Is.EqualTo(SkiaSharp.SKColor.Parse("#AABBCC")));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainParsesHexCaseInsensitively()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "test",
+ Theme = new()
+ {
+ PrimaryColor = "#a1b2c3",
+ SecondaryColor = "#80FF0000"
+ }
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ var expectedPrimary = SkiaSharp.SKColor.Parse("#A1B2C3");
+ var expectedSecondary = SkiaSharp.SKColor.Parse("#80FF0000");
+
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(expectedPrimary));
+ Assert.That(result.SecondaryColor, Is.EqualTo(expectedSecondary));
+ Assert.That(result.SecondaryColor.Alpha, Is.EqualTo(expectedSecondary.Alpha));
+ Assert.That(result.SecondaryColor.Red, Is.EqualTo(expectedSecondary.Red));
+ Assert.That(result.SecondaryColor.Green, Is.EqualTo(expectedSecondary.Green));
+ Assert.That(result.SecondaryColor.Blue, Is.EqualTo(expectedSecondary.Blue));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultColorsWhenThemeProvidedButEmptyOrWhitespace()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "empty theme",
+ Theme = new()
+ {
+ PrimaryColor = " ",
+ SecondaryColor = ""
+ }
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(Constants.DefaultPrimaryColor));
+ Assert.That(result.SecondaryColor, Is.EqualTo(Constants.DefaultSecondaryColor));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultSecondaryColorWhenOnlyPrimaryProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "only primary",
+ Theme = new()
+ {
+ PrimaryColor = "#010203",
+ SecondaryColor = null
+ }
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(SkiaSharp.SKColor.Parse("#010203")));
+ Assert.That(result.SecondaryColor, Is.EqualTo(Constants.DefaultSecondaryColor));
+ }
+ }
+
+ [Test]
+ public void GetRequestToDomainUsesDefaultPrimaryColorWhenOnlySecondaryProvided()
+ {
+ // Arrange
+ var request = new GetCreateCaptchaRequest
+ {
+ Text = "only secondary",
+ Theme = new()
+ {
+ PrimaryColor = null,
+ SecondaryColor = "#0A0B0C"
+ }
+ };
+
+ // Act
+ var result = _requestToDomainMapper.ToDomain(request);
+
+ // Assert
+ using (Assert.EnterMultipleScope())
+ {
+ Assert.That(result.PrimaryColor, Is.EqualTo(Constants.DefaultPrimaryColor));
+ Assert.That(result.SecondaryColor, Is.EqualTo(SkiaSharp.SKColor.Parse("#0A0B0C")));
+ }
+ }
}