Skip to content

Commit

Permalink
🖋️ feat: Transform signature to value object (#162)
Browse files Browse the repository at this point in the history
feat: implement signature value object
  • Loading branch information
kamilbaczek authored Oct 27, 2024
1 parent c797a62 commit 2db6e2d
Show file tree
Hide file tree
Showing 30 changed files with 435 additions and 118 deletions.
8 changes: 4 additions & 4 deletions .github/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ runs:
run: |
cd ${{ github.workspace }}/${{ inputs.path }}
if dotnet nuget list source | grep -q $NugetSourceName; then
echo "Removing existing nuget source: $NugetSourceName"
dotnet nuget remove source $NugetSourceName
if dotnet nuget list source | grep -q ${{ inputs.nuget-source-name }}; then
echo "Removing existing nuget source: '${{ inputs.nuget-source-name }}'"
dotnet nuget remove source '${{ inputs.nuget-source-name }}'
else
echo "Nuget source $NugetSourceName does not exist. Skipping removal."
fi
dotnet nuget add source --username ${{ inputs.owner }} --password ${{ inputs.github-token }} --store-password-in-clear-text --name ${{ inputs.nuget-source-name }} "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json"
dotnet nuget list source
shell: bash
shell: bash
4 changes: 2 additions & 2 deletions .github/workflows/chapter-4-contracts-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Add Evolutionary Architecture Nuget Source
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@main
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@feature/add_signature_value_object
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
owner: ${{ github.repository_owner }}
Expand All @@ -54,7 +54,7 @@ jobs:
with:
dotnet-version: 8.0.x
- name: Add Evolutionary Architecture Nuget Source
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@main
uses: evolutionary-architecture/evolutionary-architecture-by-example/.github@feature/add_signature_value_object
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
owner: ${{ github.repository_owner }}
Expand Down
2 changes: 1 addition & 1 deletion Chapter-2-modules-separation/Src/Fitnet/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<ItemGroup>
<PackageReference Include="Bogus" Version="35.5.1" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.analyzers" Version="1.11.0">
<PrivateAssets>all</PrivateAssets>
Expand All @@ -25,6 +26,7 @@

<ItemGroup>
<ProjectReference Include="..\Fitnet.Contracts.Api\Fitnet.Contracts.Api.csproj" />
<ProjectReference Include="..\Fitnet.Contracts.Core\Fitnet.Contracts.Core.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.UnitTests.SignContract;

public sealed class SignContractRequestValidatorTests
{
private const string ValidSignature = "John Doe";
private const int SignatureCharacterLimit = 100;
private readonly SignContractRequestValidator _validator = new();
private readonly DateTimeOffset _fakeNow = new Faker().Date.RecentOffset();

[Fact]
internal void Given_sign_contract_request_validation_When_request_is_valid_Then_result_should_have_no_errors()
{
// Arrange
var request = new SignContractRequest(_fakeNow);
var request = new SignContractRequest(_fakeNow, ValidSignature);

// Act
var result = _validator.TestValidate(request);
Expand All @@ -25,12 +27,29 @@ internal void Given_sign_contract_request_validation_When_request_is_valid_Then_
internal void Given_sign_contract_request_validation_When_signed_at_not_provided_Then_result_should_have_error()
{
// Arrange
var request = new SignContractRequest(default);
var request = new SignContractRequest(default, ValidSignature);

// Act
var result = _validator.TestValidate(request);

// Assert
result.ShouldHaveValidationErrorFor(signContractRequest => signContractRequest.SignedAt);
}


[Fact]
internal void Given_sign_contract_request_validation_When_signature_is_to_long_Then_result_should_have_error()
{
// Arrange
var tooLongSignature = GenerateTooLongSignature();
var request = new SignContractRequest(default, tooLongSignature);

// Act
var result = _validator.TestValidate(request);

// Assert
result.ShouldHaveValidationErrorFor(signContractRequest => signContractRequest.SignedAt);
}

private static string GenerateTooLongSignature() => new('a', SignatureCharacterLimit + 1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.UnitTests.SignContract.Signatures;
using EvolutionaryArchitecture.Fitnet.Contracts.Core.SignContract.Signatures;

using Core.SignContract.Signatures.Exceptions;
using FluentAssertions;

public sealed class SignatureTests
{
[Theory]
[InlineData("John Doe")]
[InlineData("Kamil Baczek")]
[InlineData("Maciej Jedrzejewski")]
[InlineData("John David Smith")]
internal void Given_create_signature_When_signature_is_valid_Then_should_not_throw(string value)
{
// Arrange
var now = DateTimeOffset.Now;

// Act
var signature = Signature.From(now, value);

// Assert
signature.Value.Should().Be(value);
signature.Date.Should().Be(now);
}

[Theory]
[InlineData("invalidSignature!")]
[InlineData("invalid@Signature")]
internal void Given_create_signature_When_signature_has_forbidden_characters_Then_should_throw_exception(string value)
{
// Arrange
var now = DateTimeOffset.Now;

// Act
Action act = () => Signature.From(now, value);

// Assert
act.Should().Throw<SignatureNotValidException>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ internal static class PrepareContractEndpoint
{
internal static void MapPrepareContract(this IEndpointRouteBuilder app) =>
app.MapPost(ContractsApiPaths.Prepare,
async Task (PrepareContractRequest request, IContractsModule contractsModule, CancellationToken cancellationToken) =>
await contractsModule.ExecuteCommandAsync(request.ToCommand(), cancellationToken).Match(
async Task<IResult> (PrepareContractRequest request, IContractsModule contractsModule, CancellationToken cancellationToken) =>
await contractsModule.ExecuteCommandAsync(request.ToCommand(), cancellationToken)
.Match(
contractId => Results.Created(ContractsApiPaths.GetPreparedContractPath(contractId), (object?)contractId),
errors => errors.ToProblem()))
.ValidateRequest<PrepareContractRequest>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

internal static class SignContractEndpoint
{
internal static void MapSignContract(this IEndpointRouteBuilder app) => app.MapPatch(ContractsApiPaths.Sign, async (
internal static void MapSignContract(this IEndpointRouteBuilder app) => app.MapPatch(ContractsApiPaths.Sign,
async Task<IResult> (
Guid id,
SignContractRequest request,
IContractsModule contractsModule, CancellationToken cancellationToken) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

using EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

internal sealed record SignContractRequest(DateTimeOffset SignedAt)
internal sealed record SignContractRequest(DateTimeOffset SignedAt, string Signature)
{
internal SignContractCommand ToCommand(Guid id) =>
new(id, SignedAt);
new(id, Signature, SignedAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Api.SignContract;

internal sealed class SignContractRequestValidator : AbstractValidator<SignContractRequest>
{
public SignContractRequestValidator() => RuleFor(signContractRequest => signContractRequest.SignedAt)
.NotEmpty();
private const int SignatureMaximumLength = 100;

public SignContractRequestValidator()
{
RuleFor(signContractRequest => signContractRequest.Signature)
.NotEmpty()
.MaximumLength(SignatureMaximumLength);

RuleFor(signContractRequest => signContractRequest.SignedAt)
.NotEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

public sealed record SignContractCommand(Guid Id, DateTimeOffset SignedAt) : ICommand<ErrorOr<Guid>>;
public sealed record SignContractCommand(Guid Id, string Signature, DateTimeOffset SignedAt) : ICommand<ErrorOr<Guid>>;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Application.SignContract;

using Core.SignContract.Signatures;
using IntegrationEvents;
using MassTransit;

Expand All @@ -12,14 +13,14 @@ internal sealed class SignContractCommandHandler(
{
public async Task<ErrorOr<Guid>> Handle(SignContractCommand command, CancellationToken cancellationToken) =>
await contractsRepository.GetByIdAsync(command.Id, cancellationToken)
.ThenAsync(async contract => await contract.Sign(command.SignedAt, timeProvider.GetUtcNow())
.ThenAsync(async contract => await contract.Sign(Signature.From(command.SignedAt, command.Signature), timeProvider.GetUtcNow())
.ThenAsync(async bindingContract =>
{
await bindingContractsRepository.AddAsync(bindingContract, cancellationToken);
await contractsRepository.CommitAsync(cancellationToken);
var @event = ContractSignedEvent.Create(contract.Id.Value,
contract.CustomerId,
contract.SignedAt!.Value,
contract.Signature!.Date,
contract.ExpiringAt!.Value);
await publishEndpoint.Publish(@event, cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.UnitTests.Common.Builders;

using Core.SignContract.Signatures;

internal sealed class SignContractBuilder(Contract parentBuilder)
{
private DateTimeOffset _signDay;
Expand All @@ -15,7 +17,8 @@ public SignContractBuilder SignedOn(DateTimeOffset signDay, DateTimeOffset fakeT

private BindingContract Build()
{
var bindingContract = parentBuilder.Sign(_signDay, _fakeToday);
var signature = Signature.From(_signDay, "John Doe");
var bindingContract = parentBuilder.Sign(signature, _fakeToday);

return bindingContract.Value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.UnitTests.SignContract;
using Common;
using Common.Builders;
using Core.SignContract;
using Core.SignContract.Signatures;

public sealed class SignContractTests
{
private const string SignatureValue = "John Doe";

[Theory]
[ClassData(typeof(SignContractTestData))]
internal void Given_sign_contract_Then_expiration_date_is_set_to_contract_duration_from_now(
Expand All @@ -18,9 +21,10 @@ internal void Given_sign_contract_Then_expiration_date_is_set_to_contract_durati
Contract contract = ContractBuilder
.Prepared()
.PreparedAt(preparedAt);
var signature = Signature.From(signedAt, SignatureValue);

// Act
var signResult = contract.Sign(signedAt, fakeNow);
var signResult = contract.Sign(signature, fakeNow);

// Assert
var @event = signResult.Value.GetPublishedEvent<BindingContractStartedEvent>();
Expand All @@ -36,9 +40,10 @@ internal void Given_sign_contract_Then_contracts_becomes_binding_contract()
// Arrange
Contract contract = ContractBuilder
.Prepared();
var signature = Signature.From(SignedAt, SignatureValue);

// Act
var signResult = contract.Sign(SignedAt, FakeNow);
var signResult = contract.Sign(signature, FakeNow);

// Assert
var @event = signResult.Value.GetPublishedEvent<BindingContractStartedEvent>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.UnitTests.SignContract;

using Common;
using Core.SignContract.Signatures;

internal sealed class SignedContractBuilder(Contract parentBuilder)
{
private const string SignatureValue = "John Doe";
private DateTimeOffset? _signDay;
private DateTimeOffset? _fakeToday;

Expand All @@ -19,7 +21,8 @@ private BindingContract Build()
{
var signDay = _signDay ?? FakeContractDates.SignDay;
var fakeToday = _fakeToday ?? FakeContractDates.SignDay;
var bindingContract = parentBuilder.Sign(signDay, fakeToday).Value;
var signature = Signature.From(signDay, SignatureValue);
var bindingContract = parentBuilder.Sign(signature, fakeToday).Value;

return bindingContract;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace EvolutionaryArchitecture.Fitnet.Contracts.Core;
using PrepareContract;
using PrepareContract.BusinessRules;
using SignContract.BusinessRules;
using SignContract.Signatures;

public sealed class Contract : Entity
{
Expand All @@ -17,10 +18,10 @@ public sealed class Contract : Entity
public DateTimeOffset PreparedAt { get; init; }
public TimeSpan Duration { get; init; }

public DateTimeOffset? SignedAt { get; private set; }
public Signature? Signature { get; private set; }
public DateTimeOffset? ExpiringAt { get; private set; }

public bool IsSigned => SignedAt.HasValue;
public bool IsSigned => Signature is not null;

// EF needs this constructor to create non-primitive types
private Contract() { }
Expand Down Expand Up @@ -51,13 +52,13 @@ public static ErrorOr<Contract> Prepare(
new PreviousContractHasToBeSignedRule(isPreviousContractSigned))
.Then<Contract>(_ => new Contract(customerId, preparedAt, StandardDuration));

public ErrorOr<BindingContract> Sign(DateTimeOffset signedAt, DateTimeOffset now) =>
public ErrorOr<BindingContract> Sign(Signature signature, DateTimeOffset now) =>
BusinessRuleValidator.Validate(
new ContractMustNotBeAlreadySignedRule(IsSigned),
new ContractCanOnlyBeSignedWithin30DaysFromPreparationRule(PreparedAt, signedAt))
new ContractCanOnlyBeSignedWithin30DaysFromPreparationRule(PreparedAt, signature.Date))
.Then(_ =>
{
SignedAt = signedAt;
Signature = signature;
ExpiringAt = now.Add(Duration);
var bindingContract = BindingContract.Start(Id, CustomerId, Duration, now, ExpiringAt.Value);

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
global using ErrorOr;
global using System.Text.RegularExpressions;
global using ErrorOr;
global using EvolutionaryArchitecture.Fitnet.Contracts.Core.SignContract.Signatures.Exceptions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.SignContract.Signatures.Exceptions;

public sealed class SignatureNotValidException(string signature) : InvalidOperationException($"Signature: '{signature}' contains invalid characters.");
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace EvolutionaryArchitecture.Fitnet.Contracts.Core.SignContract.Signatures;

public sealed partial class Signature
{
private static readonly Regex SignaturePattern = SignatureAllowedCharacters();
public DateTimeOffset Date { get; }
public string Value { get; }

private Signature(DateTimeOffset date, string value)
{
Date = date;
if (!SignaturePattern.IsMatch(value))
{
throw new SignatureNotValidException(value);
}

Value = value;
}

public static Signature From(DateTimeOffset signedAt, string signature) =>
new(signedAt, signature);

[GeneratedRegex(@"^[A-Za-z\s]+$")]
private static partial Regex SignatureAllowedCharacters();
}
Loading

0 comments on commit 2db6e2d

Please sign in to comment.