Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/Captcha.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
public static SKTypeface FallbackFontTypeface { get; } =
SKTypeface.FromStream(typeof(Constants).Assembly.GetManifestResourceStream("Captcha.Core.Resources.Fonts.Unifont.ttf"));
}
18 changes: 17 additions & 1 deletion src/Captcha.Core/Mappers/RequestToDomainMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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; }
Expand Down
10 changes: 10 additions & 0 deletions src/Captcha.Core/Models/PostCreateCaptchaRequest.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
25 changes: 22 additions & 3 deletions src/Captcha.WebApi/Controllers/CaptchaController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FileContentResult> 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<FileContentResult> CreateAsync(CaptchaRequest request)
[SwaggerRequestExample(typeof(PostCreateCaptchaRequest), typeof(CreateCaptchaExamples))]
public async Task<FileContentResult> PostCreateAsync(PostCreateCaptchaRequest request)
{
var domain = requestToDomainMapper.ToDomain(request);

using var created = captchaImageService.CreateCaptchaImage(domain);

return await SerializeToJpegFile(created);
}

private static async Task<FileContentResult> SerializeToJpegFile(SKBitmap image)
{
await using var memoryStream = new MemoryStream();
SKImage.FromBitmap(created)
SKImage.FromBitmap(image)
.Encode(SKEncodedImageFormat.Jpeg, 100)
.SaveTo(memoryStream);

Expand Down
14 changes: 7 additions & 7 deletions src/Captcha.WebApi/Controllers/Examples/CreateCaptchaExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ namespace Captcha.WebApi.Controllers.Examples;
using Captcha.Core.Models;
using Swashbuckle.AspNetCore.Filters;

public record CreateCaptchaExamples : IMultipleExamplesProvider<CaptchaRequest>
public record CreateCaptchaExamples : IMultipleExamplesProvider<PostCreateCaptchaRequest>
{
public IEnumerable<SwaggerExample<CaptchaRequest>> GetExamples()
public IEnumerable<SwaggerExample<PostCreateCaptchaRequest>> GetExamples()
{
yield return SwaggerExample.Create(
"Example 1",
"Example 1 - Create captcha with text",
new CaptchaRequest
new PostCreateCaptchaRequest
{
Text = "hello world"
});

yield return SwaggerExample.Create(
"Example 2",
"Example 2 - Create challenging captcha with text",
new CaptchaRequest
new PostCreateCaptchaRequest
{
Text = "hello world",
Difficulty = CaptchaDifficulty.Challenging
Expand All @@ -27,7 +27,7 @@ public IEnumerable<SwaggerExample<CaptchaRequest>> GetExamples()
yield return SwaggerExample.Create(
"Example 3",
"Example 3 - Create hard captcha with text",
new CaptchaRequest
new PostCreateCaptchaRequest
{
Text = "hello world",
Difficulty = CaptchaDifficulty.Hard
Expand All @@ -36,7 +36,7 @@ public IEnumerable<SwaggerExample<CaptchaRequest>> 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,
Expand All @@ -46,7 +46,7 @@ public IEnumerable<SwaggerExample<CaptchaRequest>> 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
Expand Down
32 changes: 32 additions & 0 deletions tests/Captcha.FunctionalTests/Features/CaptchaGetEndpoint.feature
Original file line number Diff line number Diff line change
@@ -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 |
| <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 |
| <ExpectedWidth> | <ExpectedHeight> |
Then I expect a captcha image to contain at least '<FirstColorPixels>' pixels of color '<FirstColorHex>' and at least '<SecondColorPixels>' pixels of color '<SecondColorHex>'

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 |
Original file line number Diff line number Diff line change
@@ -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
Expand Down
46 changes: 43 additions & 3 deletions tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
[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:")]
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])
Expand All @@ -46,11 +48,11 @@
[When(@"I send the request to the Create endpoint of the CaptchaController")]
public async Task WhenISendTheRequestToTheCreateEndpointOfTheCaptchaController()
{
var request = new RestRequest(TestConstants.CreateCaptchaEndpoint)

Check warning on line 51 in tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs

View workflow job for this annotation

GitHub Actions / build

The type 'Captcha.Core.Models.PostCreateCaptchaRequest?' cannot be used as type parameter 'T' in the generic type or method 'RestRequestExtensions.AddJsonBody<T>(RestRequest, T, ContentType?)'. Nullability of type argument 'Captcha.Core.Models.PostCreateCaptchaRequest?' doesn't match 'class' constraint.
{
RequestFormat = DataFormat.Json,
Method = Method.Post
}.AddJsonBody(_request);
}.AddJsonBody(_postRequest);

_response = await Client.ExecuteAsync(request);
}
Expand All @@ -59,7 +61,7 @@
public void ThenIExpectACaptchaImageToBeReturnedWithTheFollowingAttributes(Table table)
{
var row = table.Rows[0];
using var ms = new MemoryStream(_response.RawBytes);

Check warning on line 64 in tests/Captcha.FunctionalTests/StepDefinitions/CaptchaSteps.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var img = SKImage.FromEncodedData(ms);

var expectedWidth = int.Parse(row[TestConstants.Width], CultureInfo.InvariantCulture);
Expand Down Expand Up @@ -128,4 +130,42 @@
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<CaptchaDifficulty>(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);
}
}
5 changes: 0 additions & 5 deletions tests/Captcha.UnitTests/ConstantsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading
Loading