Compare commits

...

10 Commits

Author SHA1 Message Date
ddb2439b29 add mapping profile to convert CreateEnvelopeCommand to Envelope 2025-09-01 17:30:38 +02:00
d48514bbad refactor(Extensions): update CreateEnvelopeCommand to make UseSQLExecutor false 2025-09-01 17:27:14 +02:00
00077a647a feat(envelopes): add support for SQLExecutor or repository when creating envelopes
- Refactored `CreateEnvelopeCommandHandler` to resolve dependencies via `IServiceProvider`
- Added `IRepository<Envelope>` to allow repository-based envelope creation
- Updated handler logic to choose between `IEnvelopeExecutor` and repository based on `UseSQLExecutor` flag
2025-09-01 17:23:40 +02:00
ee7eb08e75 refactor(CreateEnvelopeCommand): add UseSQLExecutor-property 2025-09-01 17:07:16 +02:00
6a34b65825 rename as ToEnvelopeKey 2025-09-01 15:43:13 +02:00
20d312a84e update to create key by EncodeEnvelopeReceiverId 2025-09-01 15:42:33 +02:00
87c5e7e4de feat(HistoryTests): add create receiver logic 2025-09-01 15:27:32 +02:00
bb93b980b4 refactor(HistoryTests): create and implement TestBase 2025-09-01 15:19:52 +02:00
950ae5a418 Update HistoryTests with new commands and setup changes
- Added new using directives for additional namespaces.
- Replaced `_host.AddSampleReceivers()` with `_host.AddSamples()` in the Setup method.
- Introduced `createEnvelopeCmd` in the test method to create an envelope before executing the history command.
2025-09-01 15:06:10 +02:00
582cc1eb13 Refactor CreateHistoryCommand and update tests
- Simplified `CreateHistoryCommand` method by removing generic type parameters.
- Updated `using` directives in `HistoryTests` to include necessary constants.
- Revised test method to utilize `Fake.Provider.CreateHistoryCommand` for improved maintainability.
2025-09-01 14:52:49 +02:00
11 changed files with 141 additions and 70 deletions

View File

@@ -1,6 +1,10 @@
using EnvelopeGenerator.Application.Dto;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Dto;
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
using MediatR;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
@@ -30,4 +34,14 @@ public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
/// ID des Absenders
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Determines which component is used for envelope processing.
/// When <c>true</c>, processing is delegated to <see cref="IEnvelopeExecutor"/>;
/// when <c>false</c>, <see cref="IRepository{Envelope}"/> is used instead.
/// Note: <see cref="IRepository{Envelope}"/> should only be used in testing scenarios.
/// </summary>
[JsonIgnore]
[NotMapped]
public bool UseSQLExecutor { get; set; } = true;
}

View File

@@ -2,6 +2,9 @@
using EnvelopeGenerator.Application.Interfaces.SQLExecutor;
using EnvelopeGenerator.Application.Dto;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
@@ -10,18 +13,22 @@ namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// </summary>
public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeCommand, EnvelopeDto?>
{
private readonly IEnvelopeExecutor _envelopeExecutor;
private readonly IServiceProvider _provider;
private IEnvelopeExecutor Executor => _provider.GetRequiredService<IEnvelopeExecutor>();
private IRepository<Envelope> Repository => _provider.GetRequiredService<IRepository<Envelope>>();
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="envelopeExecutor"></param>
/// <param name="provider"></param>
/// <param name="mapper"></param>
public CreateEnvelopeCommandHandler(IEnvelopeExecutor envelopeExecutor, IMapper mapper)
public CreateEnvelopeCommandHandler(IServiceProvider provider, IMapper mapper)
{
_envelopeExecutor = envelopeExecutor;
_provider = provider;
_mapper = mapper;
}
@@ -29,12 +36,14 @@ public class CreateEnvelopeCommandHandler : IRequestHandler<CreateEnvelopeComman
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancellationToken)
public async Task<EnvelopeDto?> Handle(CreateEnvelopeCommand request, CancellationToken cancel)
{
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancellationToken);
var envelope = request.UseSQLExecutor
? await Executor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel)
: await Repository.CreateAsync(request, cancel);
return _mapper.Map<EnvelopeDto>(envelope);
}
}
}

View File

@@ -1,5 +1,6 @@
using AutoMapper;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Envelopes;
@@ -15,5 +16,6 @@ public class MappingProfile : Profile
public MappingProfile()
{
CreateMap<Envelope, CreateEnvelopeReceiverResponse>();
CreateMap<CreateEnvelopeCommand, Envelope>();
}
}

View File

@@ -1,30 +1,38 @@
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text;
namespace EnvelopeGenerator.Application.Extensions
namespace EnvelopeGenerator.Application.Extensions;
/// <summary>
/// Provides extension methods for decoding and extracting information from an envelope receiver ID.
/// </summary>
public static class EncodingExtensions
{
/// <summary>
/// Provides extension methods for decoding and extracting information from an envelope receiver ID.
///
/// </summary>
public static class EncodingExtensions
/// <param name="readOnlyId"></param>
/// <returns></returns>
public static string ToEnvelopeKey(this long readOnlyId)
{
public static string EncodeEnvelopeReceiverId(this long readOnlyId)
{
//The random number is used as a salt to increase security but it is not saved in the database.
string combinedString = $"{Random.Shared.Next()}::{readOnlyId}::{Random.Shared.Next()}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
//The random number is used as a salt to increase security but it is not saved in the database.
string combinedString = $"{Random.Shared.Next()}::{readOnlyId}::{Random.Shared.Next()}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
return base64String;
}
return base64String;
}
public static string EncodeEnvelopeReceiverId(this (string envelopeUuid, string receiverSignature) input)
{
string combinedString = $"{input.envelopeUuid}::{input.receiverSignature}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
/// <summary>
///
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string ToEnvelopeKey(this (string envelopeUuid, string receiverSignature) input)
{
string combinedString = $"{input.envelopeUuid}::{input.receiverSignature}";
byte[] bytes = Encoding.UTF8.GetBytes(combinedString);
string base64String = Convert.ToBase64String(bytes);
return base64String;
}
return base64String;
}
}

View File

@@ -1,4 +1,6 @@
using DigitalData.Core.Abstraction.Application.Repository;
using AutoMapper;
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Application.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -13,7 +15,7 @@ namespace EnvelopeGenerator.Application.Receivers.Commands;
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record CreateReceiverCommand : IRequest<(int Id, bool AlreadyExists)>
public record CreateReceiverCommand : IRequest<(ReceiverReadDto Receiver, bool AlreadyExists)>
{
/// <summary>
///
@@ -52,20 +54,23 @@ public record CreateReceiverCommand : IRequest<(int Id, bool AlreadyExists)>
/// <summary>
///
/// </summary>
public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverCommand, (int Id, bool AlreadyExists)>
public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverCommand, (ReceiverReadDto Receiver, bool AlreadyExists)>
{
/// <summary>
///
/// </summary>
private readonly IRepository<Receiver> _repo;
private readonly IMapper _mapper;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
public CreateReceiverCommandHandler(IRepository<Receiver> repo)
public CreateReceiverCommandHandler(IRepository<Receiver> repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
/// <summary>
@@ -74,7 +79,7 @@ public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverComman
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<(int Id, bool AlreadyExists)> Handle(CreateReceiverCommand request, CancellationToken cancel)
public async Task<(ReceiverReadDto Receiver, bool AlreadyExists)> Handle(CreateReceiverCommand request, CancellationToken cancel)
{
var receiver = await _repo.ReadOnly()
.Where(r => r.EmailAddress == request.EmailAddress)
@@ -85,6 +90,7 @@ public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverComman
if (!alreadyExists)
receiver = await _repo.CreateAsync(request, cancel);
return (receiver!.Id, alreadyExists);
var receiverDto = _mapper.Map<ReceiverReadDto>(receiver);
return (receiverDto, alreadyExists);
}
}

View File

@@ -56,7 +56,7 @@ private async Task<Dictionary<string, string>> CreatePlaceholders(string? access
if(envelopeReceiverDto?.Envelope is not null && envelopeReceiverDto.Receiver is not null)
{
var erId = (envelopeReceiverDto.Envelope.Uuid, envelopeReceiverDto.Receiver.Signature).EncodeEnvelopeReceiverId();
var erId = (envelopeReceiverDto.Envelope.Uuid, envelopeReceiverDto.Receiver.Signature).ToEnvelopeKey();
var sigHost = await _configService.ReadDefaultSignatureHost();
var linkToDoc = $"{sigHost}/EnvelopeKey/{erId}";
_placeholders["[LINK_TO_DOCUMENT]"] = linkToDoc;
@@ -71,7 +71,7 @@ private async Task<Dictionary<string, string>> CreatePlaceholders(string? access
if (readOnlyDto?.Envelope is not null && readOnlyDto.Receiver is not null)
{
_placeholders["[NAME_RECEIVER]"] = await _envRcvService.ReadLastUsedReceiverNameByMailAsync(readOnlyDto.AddedWho).ThenAsync(res => res, (msg, ntc) => string.Empty) ?? string.Empty;
var erReadOnlyId = (readOnlyDto.Id).EncodeEnvelopeReceiverId();
var erReadOnlyId = (readOnlyDto.Id).ToEnvelopeKey();
var sigHost = await _configService.ReadDefaultSignatureHost();
var linkToDoc = $"{sigHost}/EnvelopeKey/{erReadOnlyId}";
_placeholders["[LINK_TO_DOCUMENT]"] = linkToDoc;

View File

@@ -104,8 +104,8 @@ public class Fake
var mediator = Mediator;
foreach (var cmd in Provider.CreateReceiverCommands())
{
var (Id, _) = await mediator.Send(cmd);
_sampleReceivers.Add((Id, cmd.EmailAddress));
var (receiver, _) = await mediator.Send(cmd);
_sampleReceivers.Add((receiver.Id, cmd.EmailAddress));
}
return this;
}
@@ -156,7 +156,8 @@ public static class Extensions
{
Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)),
Title = fake.Lorem.Paragraph(fake.Random.Number(1, 2)),
UserId = userId
UserId = userId,
UseSQLExecutor = false
};
public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs)
@@ -205,9 +206,7 @@ public static class Extensions
#endregion
#region History
public static CreateHistoryCommand CreateHistoryCommand<TEnvelopeQuery, TReceiverQuery>(this Faker fake, string key, EnvelopeStatus? status = null)
where TEnvelopeQuery : EnvelopeQueryBase
where TReceiverQuery : ReceiverQueryBase
public static CreateHistoryCommand CreateHistoryCommand(this Faker fake, string key, EnvelopeStatus? status = null)
{
return new()
{

View File

@@ -1,48 +1,50 @@
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Domain.Constants;
namespace EnvelopeGenerator.Tests.Application;
[TestFixture]
public class HistoryTests
public class HistoryTests : TestBase
{
private Fake.Host _host;
[SetUp]
public async Task Setup()
public override Task Setup()
{
_host = Fake.CreateHost();
await _host.AddSampleReceivers();
return base.Setup();
}
[TearDown]
public void TearDown()
public override void TearDown()
{
_host.Dispose();
base.TearDown();
}
[Test]
public async Task CreateHistory_And_ReadHistory_Should_Work()
{
// Arrange
var createCmd = new CreateHistoryCommand
{
EnvelopeId = 1,
UserReference = "UserA",
Status = EnvelopeStatus.EnvelopeCreated,
Comment = "First create"
};
/// Arrange
// Create envelope
var createEnvelopeCmd = FakeCreateEnvelopeCommand;
var envelope = await Mediator.Send(createEnvelopeCmd).ThrowIfNull(Exceptions.NotFound);
// Create receiver
var createReceiverCmd = this.CreateReceiverCommand();
(var receiver, _) = await Mediator.Send(createReceiverCmd);
var key = (envelope.Uuid, receiver.Signature).ToEnvelopeKey();
var createCmd = Fake.Provider.CreateHistoryCommand(key);
// Act
var id = await _host.Mediator.Send(createCmd);
var id = await Mediator.Send(createCmd);
// Assert
Assert.That(id, Is.Not.Null);
// ReadHistory query
var query = new ReadHistoryQuery(1);
var result = await _host.Mediator.Send(query);
var result = await Mediator.Send(query);
Assert.That(result, Is.Not.Empty);
}
@@ -65,11 +67,11 @@ public class HistoryTests
Status = EnvelopeStatus.EnvelopePartlySigned
};
await _host.Mediator.Send(createCmd1);
await _host.Mediator.Send(createCmd2);
await Mediator.Send(createCmd1);
await Mediator.Send(createCmd2);
// Act
var result = await _host.Mediator.Send(new ReadHistoryQuery(2, EnvelopeStatus.EnvelopePartlySigned));
var result = await Mediator.Send(new ReadHistoryQuery(2, EnvelopeStatus.EnvelopePartlySigned));
// Assert
Assert.That(result, Has.Exactly(1).Items);
@@ -81,7 +83,7 @@ public class HistoryTests
public async Task ReadHistory_Should_Return_Empty_When_No_Record()
{
// Act
var result = await _host.Mediator.Send(new ReadHistoryQuery(999));
var result = await Mediator.Send(new ReadHistoryQuery(999));
// Assert
Assert.That(result, Is.Empty);

View File

@@ -0,0 +1,31 @@
using Bogus;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Receivers.Commands;
using MediatR;
namespace EnvelopeGenerator.Tests.Application;
public class TestBase : Faker
{
private Fake.Host Host;
public User User => Host.User;
public IMediator Mediator => Host.Mediator;
public CreateEnvelopeCommand FakeCreateEnvelopeCommand => this.CreateEnvelopeCommand(Host.User.Id);
[SetUp]
public virtual async Task Setup()
{
Host = Fake.CreateHost();
await Host.AddSamples();
}
[TearDown]
public virtual void TearDown()
{
Host.Dispose();
}
}

View File

@@ -458,7 +458,7 @@ public class HomeController : ViewControllerBase
return await _envRcvService.ReadByUuidSignatureAsync(uuid: erro.Envelope!.Uuid, erro.Receiver!.Signature).ThenAsync(
SuccessAsync: async er =>
{
var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).EncodeEnvelopeReceiverId();
var envelopeKey = (er.Envelope!.Uuid, er.Receiver!.Signature).ToEnvelopeKey();
//TODO: implement multi-threading to history process (Task)
var hist_res = await _historyService.RecordAsync((int)erro.EnvelopeId, erro.AddedWho, EnvelopeStatus.EnvelopeViewed);

View File

@@ -75,8 +75,8 @@ public class TestEnvelopeReceiverController : ControllerBase
public IActionResult EncodeEnvelopeReceiverId(string? uuid = null, string? signature = null, long? readOnlyId = null)
{
if(readOnlyId is long readOnlyId_long)
return Ok(readOnlyId_long.EncodeEnvelopeReceiverId());
return Ok(readOnlyId_long.ToEnvelopeKey());
else
return Ok((uuid ?? string.Empty, signature ?? string.Empty).EncodeEnvelopeReceiverId());
return Ok((uuid ?? string.Empty, signature ?? string.Empty).ToEnvelopeKey());
}
}