Compare commits

...

28 Commits

Author SHA1 Message Date
838d7e3ab8 refactor(ReceiverGetOrCreateCommand): update to use getter inittier 2025-08-29 16:33:01 +02:00
542d80c439 fix(CreateEnvelopeReceiverCommandHandler): remove request.UserId null-check 2025-08-29 16:23:44 +02:00
145a2ebe8f feat(Extensions): add PDF generation support for EnvelopeGenerator tests
- Introduced QuestPDF dependency to generate PDF documents in tests.
- Added `CreatePdfAsBase64` extension method for generating random PDF content.
- Added `CreateDocumentCommand` and `CreateDocumentCommands` helpers for creating document test data.
- Refactored using statements and added `EnvelopeGenerator.Application.EnvelopeReceivers.Commands`.
- Maintains existing sample user and receiver setup for integration testing.
2025-08-29 16:11:14 +02:00
9cf776fa98 fix(CreateEnvelopeReceiverCommand): update to use getter-setter 2025-08-29 15:33:03 +02:00
68878c0fc8 feat(Fake): Erweiterung von Fake.Host um Repository-Zugriff und Unterstützung für Beispielbenutzer
- IRepository<TEntity>-Verknüpfung zum Auflösen von Repositorys hinzugefügt
 - AddSamples()-Hilfsprogramm zum gemeinsamen Initialisieren von Empfängern und Benutzern eingeführt
 - SampleReceivers mit Validierungsüberwachung verbessert
 - Beispiel für Benutzerinitialisierung über Repository mit AddSampleUser() hinzugefügt
 - using-Anweisungen aktualisiert, um DigitalData.Core.Abstraction.Application.Repository und DigitalData.UserManager.Domain.Entities einzubeziehen
2025-08-29 14:57:51 +02:00
d3e5d3d791 refactor(Extensions): add CreateUserCommand and CreateUserCommands 2025-08-29 14:37:24 +02:00
a7f6b94d20 create CreateUserCommand with handler and mapping profile 2025-08-29 14:31:36 +02:00
777f20eddb create faker-extension method for Envelope Commands 2025-08-29 14:00:00 +02:00
c14ffceee4 refactor(CreateEnvelopeCommand): update to generate with getter-setter 2025-08-29 13:41:10 +02:00
e9202ad23e refactor(Fake): Ersetzen der Roh-E-Mail-Generierung durch CreateReceiverCommand-Helfer
- Direkte Verwendung von `Internet.EMailList()` für Beispielempfänger entfernt.
- Erweiterungsmethoden `CreateReceiverCommand` und `CreateReceiverCommands` in `Faker` eingeführt.
- `AddSampleReceivers` aktualisiert, um `CreateReceiverCommands` für eine sauberere, konsistentere Testdatenerstellung zu verwenden.
2025-08-29 13:25:36 +02:00
954eff7101 refactor(tests): replace static sample emails with Bogus-generated random emails 2025-08-29 13:12:33 +02:00
ac501dffb1 fix(Mock): rename Fake 2025-08-29 12:53:03 +02:00
baf2207d03 refactor(Mock): Host-Klasse für csetup erstellen und MediatR integrieren
- Migration zu Microsoft.Extensions.Hosting.CreateDefaultBuilder
- Optionale Parameter und echte DB-Unterstützung in Tests entfernt
- InMemoryDatabase zum Testen hinzugefügt
- MediatR für die Befehlsverarbeitung integriert
- Beispielmethode zum Initialisieren von Empfängern im Test-Host hinzugefügt
2025-08-29 12:46:38 +02:00
6863ada4be feat(HistoryTests): add Receiver to provide random receiver 2025-08-29 11:11:15 +02:00
8a22075abe update to use deconstructed 2025-08-29 11:05:52 +02:00
bcb2e79fa1 feat: Duplikatsprüfung beim Erstellen eines Empfängers hinzufügen
- `CreateReceiverCommand` wurde aktualisiert, sodass nun `(Id, AlreadyExists)` anstelle von nur `Id` zurückgegeben wird.
- Der Handler wurde geändert, um zu überprüfen, ob bereits ein Empfänger mit derselben E-Mail-Adresse vorhanden ist.
- Es wird nur dann ein neuer Empfänger erstellt, wenn dieser noch nicht vorhanden ist.
- `Microsoft.EntityFrameworkCore` wurde für die Abfrageunterstützung hinzugefügt.
2025-08-29 10:58:27 +02:00
c8dae1d8ff test: extend HistoryTests by adding receiver initialization with CreateReceiverCommand 2025-08-29 10:45:00 +02:00
cc2db8716e refactor(CreateReceiverCommand): add handler 2025-08-29 10:25:33 +02:00
b939e19334 move mapping profile 2025-08-29 10:07:45 +02:00
16e769d916 rename UpdateReceiverCommand 2025-08-29 10:04:25 +02:00
befbacad7c move ReceiverUpdateDto 2025-08-29 10:04:04 +02:00
aa1e218b37 remove lazy loading of signature 2025-08-29 10:03:19 +02:00
ab9a6cd595 rename CreateReceiverCommand 2025-08-29 10:02:01 +02:00
8783cb9cd8 move ReceiverCreateDto to commands 2025-08-29 10:01:17 +02:00
e49be2b7c3 make set up Setup 2025-08-29 09:57:57 +02:00
14a565d202 refactor(HistoryTests): Vereinfachung von HistoryTests durch direkte Einbindung von IMediator
- Entfernen der benutzerdefinierten Hilfsmethode Send<T>
 - Einführung der Mediator-Eigenschaft, die aus dem DI-Container aufgelöst wird
 - Ersetzen aller Send(request)-Aufrufe durch Mediator.Send(request)
 - Reduzierung unnötiger Indirektionen, wodurch Tests übersichtlicher und leichter lesbar werden
2025-08-29 09:52:34 +02:00
dc42a76f31 Merge branch 'master' of http://git.dd:3000/AppStd/EnvelopeGenerator 2025-08-29 09:39:08 +02:00
79dc4ba599 refactor: remove HasPrincipalKey-statement 2025-08-28 11:05:40 +02:00
22 changed files with 536 additions and 218 deletions

View File

@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Dto.EnvelopeReceiverReadOnly;
using EnvelopeGenerator.Application.Dto.Messaging;
using EnvelopeGenerator.Application.Dto.Receiver;
using EnvelopeGenerator.Application.Extensions;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Dto;
@@ -34,8 +35,6 @@ public class MappingProfile : Profile
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverSecretDto>();
CreateMap<EnvelopeType, EnvelopeTypeDto>();
CreateMap<Domain.Entities.Receiver, ReceiverReadDto>();
CreateMap<Domain.Entities.Receiver, ReceiverCreateDto>();
CreateMap<Domain.Entities.Receiver, ReceiverUpdateDto>();
CreateMap<Domain.Entities.EnvelopeReceiverReadOnly, EnvelopeReceiverReadOnlyDto>();
// DTO to Entity mappings
@@ -50,8 +49,6 @@ public class MappingProfile : Profile
CreateMap<EnvelopeReceiverDto, Domain.Entities.EnvelopeReceiver>();
CreateMap<EnvelopeTypeDto, EnvelopeType>();
CreateMap<ReceiverReadDto, Domain.Entities.Receiver>().ForMember(rcv => rcv.EnvelopeReceivers, rcvReadDto => rcvReadDto.Ignore());
CreateMap<ReceiverCreateDto, Domain.Entities.Receiver>();
CreateMap<ReceiverUpdateDto, Domain.Entities.Receiver>();
CreateMap<Domain.Entities.EnvelopeReceiver, EnvelopeReceiverBasicDto>();
CreateMap<EnvelopeReceiverReadOnlyCreateDto, Domain.Entities.EnvelopeReceiverReadOnly>();
CreateMap<EnvelopeReceiverReadOnlyUpdateDto, Domain.Entities.EnvelopeReceiverReadOnly>();

View File

@@ -1,53 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text;
namespace EnvelopeGenerator.Application.Dto.Receiver;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record ReceiverCreateDto
{
/// <summary>
///
/// </summary>
public ReceiverCreateDto()
{
_sha256HexOfMail = new(() =>
{
var bytes_arr = Encoding.UTF8.GetBytes(EmailAddress!.ToUpper());
var hash_arr = SHA256.HashData(bytes_arr);
var hexa_str = BitConverter.ToString(hash_arr);
return hexa_str.Replace("-", string.Empty);
});
}
/// <summary>
///
/// </summary>
[EmailAddress]
public required string EmailAddress { get; init; }
/// <summary>
///
/// </summary>
public string? TotpSecretkey { get; init; }
/// <summary>
/// var bytes_arr = Encoding.UTF8.GetBytes(EmailAddress.ToUpper());<br/>
/// var hash_arr = SHA256.HashData(bytes_arr);
/// var hexa_str = BitConverter.ToString(hash_arr);
/// return hexa_str.Replace("-", string.Empty);
/// </summary>
public string Signature => _sha256HexOfMail.Value;
private readonly Lazy<string> _sha256HexOfMail;
/// <summary>
/// Default value is DateTime.Now
/// </summary>
public DateTime AddedWhen { get; } = DateTime.Now;
};

View File

@@ -7,18 +7,20 @@ namespace EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
/// <summary>
/// Befehl zur Erstellung eines Umschlags.
/// </summary>
/// <param name="Title">Der Titel des Umschlags. Dies ist ein Pflichtfeld.</param>
/// <param name="Message">Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.</param>
/// <param name="Document">Das mit dem Umschlag verknüpfte Dokument. Dies ist ein Pflichtfeld.</param>
/// <param name="Receivers">Eine Sammlung von Empfängern, die den Umschlag erhalten. Dies ist ein Pflichtfeld.</param>
/// <param name="TFAEnabled">Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.</param>
public record CreateEnvelopeReceiverCommand(
[Required] string Title,
[Required] string Message,
[Required] DocumentCreateCommand Document,
[Required] IEnumerable<ReceiverGetOrCreateCommand> Receivers,
bool TFAEnabled = false
) : CreateEnvelopeCommand(Title, Message, TFAEnabled), IRequest<CreateEnvelopeReceiverResponse>;
public record CreateEnvelopeReceiverCommand : CreateEnvelopeCommand, IRequest<CreateEnvelopeReceiverResponse>
{
/// <summary>
/// Das mit dem Umschlag verknüpfte Dokument. Dies ist ein Pflichtfeld.
/// </summary>
[Required]
public required DocumentCreateCommand Document { get; set; }
/// <summary>
/// Eine Sammlung von Empfängern, die den Umschlag erhalten. Dies ist ein Pflichtfeld.
/// </summary>
[Required]
public List<ReceiverGetOrCreateCommand> Receivers { get; set; } = new();
}
#region Subcommands
/// <summary>
@@ -33,19 +35,37 @@ public record Signature([Required] double X, [Required] double Y, [Required] int
/// DTO für Empfänger, die erstellt oder abgerufen werden sollen.
/// Wenn nicht, wird sie erstellt und mit einer Signatur versehen.
/// </summary>
/// <param name="Signatures">Unterschriften auf Dokumenten.</param>
/// <param name="Salution">Der Name, mit dem der Empfänger angesprochen werden soll. Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet.</param>
/// <param name="PhoneNumber">Sollte mit Vorwahl geschrieben werden</param>
public record ReceiverGetOrCreateCommand([Required] IEnumerable<Signature> Signatures, string? Salution = null, string? PhoneNumber = null)
public class ReceiverGetOrCreateCommand
{
/// <summary>
/// Unterschriften auf Dokumenten.
/// </summary>
[Required]
public List<Signature> Signatures { get; init; } = new();
/// <summary>
/// Der Name, mit dem der Empfänger angesprochen werden soll.
/// Bei Null oder keinem Wert wird der zuletzt verwendete Name verwendet.
/// </summary>
public string? Salution { get; init; }
/// <summary>
/// Sollte mit Vorwahl geschrieben werden
/// </summary>
public string? PhoneNumber { get; init; }
private string _emailAddress = string.Empty;
/// <summary>
/// E-Mail-Adresse des Empfängers.
/// </summary>
[Required]
public string EmailAddress { get => _emailAddress.ToLower(); init => _emailAddress = value.ToLower(); }
};
public string EmailAddress
{
get => _emailAddress.ToLower();
init => _emailAddress = value.ToLower();
}
}
/// <summary>
/// DTO zum Erstellen eines Dokuments.

View File

@@ -42,9 +42,7 @@ public class CreateEnvelopeReceiverCommandHandler : IRequestHandler<CreateEnvelo
/// <returns>A task representing the asynchronous operation.</returns>
public async Task<CreateEnvelopeReceiverResponse> Handle(CreateEnvelopeReceiverCommand request, CancellationToken cancel)
{
int userId = request.UserId ?? throw new InvalidOperationException("UserId cannot be null when creating an envelope.");
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(userId, request.Title, request.Message, request.TFAEnabled, cancel);
var envelope = await _envelopeExecutor.CreateEnvelopeAsync(request.UserId, request.Title, request.Message, request.TFAEnabled, cancel);
List<EnvelopeReceiver> sentRecipients = new();
List<ReceiverGetOrCreateCommand> unsentRecipients = new();

View File

@@ -1,28 +1,33 @@
using EnvelopeGenerator.Application.Dto;
using EnvelopeGenerator.Application.Envelopes.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace EnvelopeGenerator.Application.Envelopes.Commands;
/// <summary>
/// Befehl zur Erstellung eines Umschlags.
/// </summary>
/// <param name="Title">Der Titel des Umschlags. Dies ist ein Pflichtfeld.</param>
/// <param name="Message">Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.</param>
/// <param name="TFAEnabled">Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.</param>
public record CreateEnvelopeCommand(
[Required] string Title,
[Required] string Message,
bool TFAEnabled = false
) : IRequest<EnvelopeDto?>
public record CreateEnvelopeCommand : IRequest<EnvelopeDto?>
{
/// <summary>
/// Id of receiver
/// Der Titel des Umschlags. Dies ist ein Pflichtfeld.
/// </summary>
[JsonIgnore]
[BindNever]
public int? UserId { get; set; }
};
[Required]
public required string Title { get; set; }
/// <summary>
/// Die Nachricht, die im Umschlag enthalten sein soll. Dies ist ein Pflichtfeld.
/// </summary>
[Required]
public required string Message { get; set; }
/// <summary>
/// Gibt an, ob die Zwei-Faktor-Authentifizierung für den Umschlag aktiviert ist. Standardmäßig false.
/// </summary>
public bool TFAEnabled { get; set; } = false;
/// <summary>
/// ID des Absenders
/// </summary>
public int UserId { get; set; }
}

View File

@@ -1,6 +1,7 @@
using DigitalData.Core.Abstraction.Application;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Dto.Receiver;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Interfaces.Services;
@@ -9,7 +10,7 @@ namespace EnvelopeGenerator.Application.Interfaces.Services;
///
/// </summary>
[Obsolete("Use MediatR")]
public interface IReceiverService : ICRUDService<ReceiverCreateDto, ReceiverReadDto, Receiver, int>
public interface IReceiverService : ICRUDService<CreateReceiverCommand, ReceiverReadDto, Receiver, int>
{
/// <summary>
///

View File

@@ -0,0 +1,90 @@
using DigitalData.Core.Abstraction.Application.Repository;
using EnvelopeGenerator.Domain.Entities;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
using System.Text;
namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary>
///
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public record CreateReceiverCommand : IRequest<(int Id, bool AlreadyExists)>
{
/// <summary>
///
/// </summary>
[EmailAddress]
public required string EmailAddress { get; init; }
/// <summary>
///
/// </summary>
public string? TotpSecretkey { get; init; }
/// <summary>
/// var bytes_arr = Encoding.UTF8.GetBytes(EmailAddress.ToUpper());<br/>
/// var hash_arr = SHA256.HashData(bytes_arr);
/// var hexa_str = BitConverter.ToString(hash_arr);
/// return hexa_str.Replace("-", string.Empty);
/// </summary>
public string Signature
{
get
{
var bytes_arr = Encoding.UTF8.GetBytes(EmailAddress!.ToUpper());
var hash_arr = SHA256.HashData(bytes_arr);
var hexa_str = BitConverter.ToString(hash_arr);
return hexa_str.Replace("-", string.Empty);
}
}
/// <summary>
/// Default value is DateTime.Now
/// </summary>
public DateTime AddedWhen { get; } = DateTime.Now;
};
/// <summary>
///
/// </summary>
public class CreateReceiverCommandHandler : IRequestHandler<CreateReceiverCommand, (int Id, bool AlreadyExists)>
{
/// <summary>
///
/// </summary>
private readonly IRepository<Receiver> _repo;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
public CreateReceiverCommandHandler(IRepository<Receiver> repo)
{
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<(int Id, bool AlreadyExists)> Handle(CreateReceiverCommand request, CancellationToken cancel)
{
var receiver = await _repo.ReadOnly()
.Where(r => r.EmailAddress == request.EmailAddress)
.SingleOrDefaultAsync(cancel);
var alreadyExists = receiver is not null;
if (!alreadyExists)
receiver = await _repo.CreateAsync(request, cancel);
return (receiver!.Id, alreadyExists);
}
}

View File

@@ -1,12 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace EnvelopeGenerator.Application.Dto.Receiver;
namespace EnvelopeGenerator.Application.Receivers.Commands;
/// <summary>
/// Data Transfer Object for updating a receiver's information.
/// </summary>
[ApiExplorerSettings(IgnoreApi = true)]
public class ReceiverUpdateDto
public class UpdateReceiverCommand
{
/// <summary>
/// Gets or sets the unique identifier of the receiver.

View File

@@ -0,0 +1,23 @@
using AutoMapper;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Domain.Entities;
namespace EnvelopeGenerator.Application.Receivers;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<Receiver, UpdateReceiverCommand>();
CreateMap<UpdateReceiverCommand, Receiver>();
CreateMap<Receiver, CreateReceiverCommand>();
CreateMap<CreateReceiverCommand, Receiver>();
}
}

View File

@@ -5,6 +5,7 @@ using EnvelopeGenerator.Application.Interfaces.Repositories;
using EnvelopeGenerator.Application.Dto.Receiver;
using DigitalData.Core.Abstraction.Application.DTO;
using EnvelopeGenerator.Application.Interfaces.Services;
using EnvelopeGenerator.Application.Receivers.Commands;
namespace EnvelopeGenerator.Application.Services;
@@ -12,7 +13,7 @@ namespace EnvelopeGenerator.Application.Services;
///
/// </summary>
[Obsolete("Use MediatR")]
public class ReceiverService : CRUDService<IReceiverRepository, ReceiverCreateDto, ReceiverReadDto, Receiver, int>, IReceiverService
public class ReceiverService : CRUDService<IReceiverRepository, CreateReceiverCommand, ReceiverReadDto, Receiver, int>, IReceiverService
{
/// <summary>
///

View File

@@ -0,0 +1,105 @@
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using MediatR;
namespace EnvelopeGenerator.Application.Users.Commands;
/// <summary>
///
/// </summary>
public record CreateUserCommand : IRequest<int>
{
/// <summary>
///
/// </summary>
public string? Prename { get; init; }
/// <summary>
///
/// </summary>
public string? Name { get; init; }
/// <summary>
///
/// </summary>
public required string Username { get; init; }
/// <summary>
///
/// </summary>
public string? Shortname { get; init; }
/// <summary>
///
/// </summary>
public string? Email { get; init; }
/// <summary>
///
/// </summary>
public string Language { get; init; } = "de-DE";
/// <summary>
///
/// </summary>
public string? Comment { get; init; }
/// <summary>
///
/// </summary>
public bool Deleted { get; } = false;
/// <summary>
///
/// </summary>
public string DateFormat { get; init; } = "dd.MM.yyyy";
/// <summary>
///
/// </summary>
public bool Active { get; } = true;
/// <summary>
///
/// </summary>
public string GeneralViewer { get; init; } = "NONE";
/// <summary>
///
/// </summary>
public bool WanEnvironment { get; } = false;
/// <summary>
///
/// </summary>
public int UserIdFkIntEcm { get; init; } = 0;
}
/// <summary>
///
/// </summary>
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IRepository<User> _repo;
/// <summary>
///
/// </summary>
/// <param name="repo"></param>
public CreateUserCommandHandler(IRepository<User> repo)
{
_repo = repo;
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="cancel"></param>
/// <returns></returns>
public async Task<int> Handle(CreateUserCommand request, CancellationToken cancel = default)
{
var user = await _repo.CreateAsync(request, cancel);
return user.Id;
}
}

View File

@@ -0,0 +1,19 @@
using AutoMapper;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application.Users.Commands;
namespace EnvelopeGenerator.Application.Users;
/// <summary>
///
/// </summary>
public class MappingProfile : Profile
{
/// <summary>
///
/// </summary>
public MappingProfile()
{
CreateMap<CreateUserCommand, User>();
}
}

View File

@@ -6,6 +6,7 @@ using EnvelopeGenerator.Application.Receivers.Queries;
using EnvelopeGenerator.Domain.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using EnvelopeGenerator.Application.Receivers.Commands;
namespace EnvelopeGenerator.GeneratorAPI.Controllers;
@@ -20,7 +21,7 @@ namespace EnvelopeGenerator.GeneratorAPI.Controllers;
[ApiController]
[Authorize]
[Obsolete("Use MediatR")]
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, ReceiverCreateDto, ReceiverReadDto, ReceiverUpdateDto, Receiver, int>
public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverService, CreateReceiverCommand, ReceiverReadDto, UpdateReceiverCommand, Receiver, int>
{
/// <summary>
/// Initialisiert eine neue Instanz des <see cref="ReceiverController"/>-Controllers.
@@ -75,13 +76,13 @@ public class ReceiverController : CRUDControllerBaseWithErrorHandling<IReceiverS
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Update(ReceiverUpdateDto updateDto) => base.Update(updateDto);
public override Task<IActionResult> Update(UpdateReceiverCommand updateDto) => base.Update(updateDto);
/// <summary>
/// Diese Methode ist deaktiviert und wird nicht verwendet.
/// </summary>
[NonAction]
public override Task<IActionResult> Create(ReceiverCreateDto createDto)
public override Task<IActionResult> Create(CreateReceiverCommand createDto)
{
return base.Create(createDto);
}

View File

@@ -54,6 +54,7 @@ public static class DIExtensions
services.TryAddScoped<IReceiverRepository, ReceiverRepository>();
services.TryAddScoped<IEnvelopeReceiverReadOnlyRepository, EnvelopeReceiverReadOnlyRepository>();
services.AddDbRepository<EGDbContext, User>(context => context.Users).UseAutoMapper();
services.AddDbRepository<EGDbContext, Config>(context => context.Configs).UseAutoMapper();
services.AddDbRepository<EGDbContext, DocumentReceiverElement>(context => context.DocumentReceiverElements).UseAutoMapper();
services.AddDbRepository<EGDbContext, EnvelopeDocument>(context => context.EnvelopeDocument).UseAutoMapper();

View File

@@ -12,6 +12,7 @@
<PackageReference Include="DigitalData.Core.Infrastructure" Version="2.3.1" />
<PackageReference Include="DigitalData.Core.Infrastructure.AutoMapper" Version="1.0.3" />
<PackageReference Include="DigitalData.EmailProfilerDispatcher" Version="3.1.1" />
<PackageReference Include="QuestPDF" Version="2025.7.1" />
<PackageReference Include="UserManager" Version="1.1.3" />
</ItemGroup>

View File

@@ -18,6 +18,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="DigitalData.Core.Abstraction.Application" Version="1.2.1" />
<PackageReference Include="DigitalData.Core.Abstractions" Version="4.0.0" />

View File

@@ -0,0 +1,211 @@
using Bogus;
using DigitalData.Core.Abstraction.Application.Repository;
using DigitalData.UserManager.Domain.Entities;
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.EnvelopeReceivers.Commands;
using EnvelopeGenerator.Application.Envelopes.Commands;
using EnvelopeGenerator.Application.Receivers.Commands;
using EnvelopeGenerator.Application.Users.Commands;
using EnvelopeGenerator.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace EnvelopeGenerator.Tests.Application;
public class Fake
{
public static readonly Faker Provider = new("de");
public static Host CreateHost() => Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
// add appsettings.json
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
})
.ConfigureServices((context, services) =>
{
IConfiguration configuration = context.Configuration;
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
#pragma warning restore CS0618
})
.Build()
.ToFake();
public class Host : IHost
{
#region Root
private readonly IHost _root;
public Host(IHost root)
{
_root = root;
}
public IServiceProvider Services => _root.Services;
public void Dispose()
{
_root.Dispose();
}
public Task StartAsync(CancellationToken cancel = default)
{
return _root.StartAsync(cancel);
}
public Task StopAsync(CancellationToken cancel = default)
{
return _root.StopAsync(cancel);
}
#endregion
#region Shortcuts
public IMediator Mediator => Services.GetRequiredService<IMediator>();
public IRepository<TEntity> GetRepository<TEntity>() => Services.GetRequiredService<IRepository<TEntity>>();
public async Task<Host> AddSamples()
{
await AddSampleReceivers();
await AddSampleUser();
return this;
}
#endregion
#region Sample Receivers
public List<(int Id, string EmailAddress)> _sampleReceivers = new();
public IEnumerable<(int Id, string EmailAddress)> SampleReceivers
=> _sampleReceivers.Any()
? _sampleReceivers
: throw new InvalidOperationException(
"No sample receivers have been initialized. Call AddSampleReceivers() before accessing this property."
);
public async Task<Host> AddSampleReceivers()
{
var mediator = Mediator;
foreach (var cmd in Provider.CreateReceiverCommands())
{
var (Id, _) = await mediator.Send(cmd);
_sampleReceivers.Add((Id, cmd.EmailAddress));
}
return this;
}
#endregion
#region Sample User
private User? _user;
public User User => _user ?? throw new InvalidOperationException(
"The 'User' instance has not been initialized. Call AddSampleUser() before accessing this property.");
public async Task<Host> AddSampleUser()
{
var repo = GetRepository<User>();
var cmd = Provider.CreateUserCommand();
_user = await repo.CreateAsync(cmd);
return this;
}
#endregion
}
}
public static class Extensions
{
public static Fake.Host ToFake(this IHost host) => new(host);
#region Receiver Command
public static CreateReceiverCommand CreateReceiverCommand(this Faker fake) => new()
{
EmailAddress = fake.Internet.Email(),
};
public static List<CreateReceiverCommand> CreateReceiverCommands(this Faker fake, int minCount = 10, int maxCount = 20)
=> Enumerable.Range(0, fake.Random.Number(minCount, maxCount))
.Select(_ => fake.CreateReceiverCommand())
.ToList();
#endregion
#region Envelope Command
public static CreateEnvelopeCommand CreateEnvelopeCommand(this Faker fake, int userId) => new()
{
Message = fake.Lorem.Paragraph(fake.Random.Number(2, 5)),
Title = fake.Lorem.Paragraph(fake.Random.Number(1, 2)),
UserId = userId
};
public static List<CreateEnvelopeCommand> CreateEnvelopeCommands(this Faker fake, params int[] userIDs)
=> Enumerable.Range(0, userIDs.Length)
.Select(fake.CreateEnvelopeCommand)
.ToList();
#endregion
#region Envelope Document
public static string CreatePdfAsBase64(this Faker faker)
{
string name = faker.Name.FullName();
string address = faker.Address.FullAddress();
string lorem = faker.Lorem.Paragraphs(2);
QuestPDF.Settings.License = LicenseType.Community;
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Margin(50);
page.Header().Text("Random PDF").FontSize(20).Bold();
page.Content().Column(col =>
{
col.Item().Text($"Vor- und Nachname: {name}");
col.Item().Text($"Adresse: {address}");
col.Item().Text(lorem);
});
});
});
using var ms = new MemoryStream();
document.GeneratePdf(ms);
return Convert.ToBase64String(ms.ToArray());
}
public static DocumentCreateCommand CreateDocumentCommand(this Faker faker) => new()
{
DataAsBase64 = faker.CreatePdfAsBase64()
};
public static List<DocumentCreateCommand> CreateDocumentCommands(this Faker fake, int minCount = 10, int maxCount = 20)
=> Enumerable.Range(0, fake.Random.Number(minCount, maxCount))
.Select(_ => fake.CreateDocumentCommand())
.ToList();
#endregion
#region User Command
public static CreateUserCommand CreateUserCommand(this Faker fake) => new()
{
Prename = fake.Name.FirstName(),
Name = fake.Name.LastName(),
Username = fake.Internet.UserName(),
Shortname = fake.Random.String2(3, 8),
Email = fake.Internet.Email()
};
public static List<CreateUserCommand> CreateUserCommands(this Faker fake, int minCount = 10, int maxCount = 20)
=> Enumerable.Range(0, fake.Random.Number(minCount, maxCount))
.Select(_ => fake.CreateUserCommand())
.ToList();
#endregion
}

View File

@@ -1,47 +1,19 @@
using EnvelopeGenerator.Application;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.Histories.Commands;
using EnvelopeGenerator.Application.Histories.Queries;
using EnvelopeGenerator.Domain;
using EnvelopeGenerator.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace EnvelopeGenerator.Tests.Application;
[TestFixture]
public class HistoryTests
{
private IHost _host;
private IServiceProvider Provider => _host.Services;
private Fake.Host _host;
[SetUp]
public void Setup()
public async Task Setup()
{
_host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
// add appsettings.json
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
})
.ConfigureServices((context, services) =>
{
IConfiguration configuration = context.Configuration;
// add Application and Infrastructure services
#pragma warning disable CS0618
services.AddEnvelopeGeneratorServices(configuration);
services.AddEnvelopeGeneratorInfrastructureServices(
(sp, options) => options.UseInMemoryDatabase("EnvelopeGeneratorTestDb"),
context.Configuration
);
#pragma warning restore CS0618
})
.Build();
_host = Fake.CreateHost();
await _host.AddSampleReceivers();
}
[TearDown]
@@ -50,12 +22,6 @@ public class HistoryTests
_host.Dispose();
}
private async Task<TResponse> Send<TResponse>(IRequest<TResponse> request)
{
var mediator = Provider.GetRequiredService<IMediator>();
return await mediator.Send(request);
}
[Test]
public async Task CreateHistory_And_ReadHistory_Should_Work()
{
@@ -69,14 +35,14 @@ public class HistoryTests
};
// Act
var id = await Send(createCmd);
var id = await _host.Mediator.Send(createCmd);
// Assert
Assert.That(id, Is.Not.Null);
// ReadHistory sorgusu
// ReadHistory query
var query = new ReadHistoryQuery(1);
var result = await Send(query);
var result = await _host.Mediator.Send(query);
Assert.That(result, Is.Not.Empty);
}
@@ -99,11 +65,11 @@ public class HistoryTests
Status = Constants.EnvelopeStatus.EnvelopePartlySigned
};
await Send(createCmd1);
await Send(createCmd2);
await _host.Mediator.Send(createCmd1);
await _host.Mediator.Send(createCmd2);
// Act
var result = await Send(new ReadHistoryQuery(2, Constants.EnvelopeStatus.EnvelopePartlySigned));
var result = await _host.Mediator.Send(new ReadHistoryQuery(2, Constants.EnvelopeStatus.EnvelopePartlySigned));
// Assert
Assert.That(result, Has.Exactly(1).Items);
@@ -115,7 +81,7 @@ public class HistoryTests
public async Task ReadHistory_Should_Return_Empty_When_No_Record()
{
// Act
var result = await Send(new ReadHistoryQuery(999));
var result = await _host.Mediator.Send(new ReadHistoryQuery(999));
// Assert
Assert.That(result, Is.Empty);

View File

@@ -1,41 +0,0 @@
using Microsoft.Extensions.Hosting;
using EnvelopeGenerator.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using EnvelopeGenerator.Application.Services;
using Microsoft.EntityFrameworkCore;
using EnvelopeGenerator.Application;
namespace EnvelopeGenerator.Tests.Application;
public class Mock
{
[Obsolete("Use MediatR")]
public static IHost CreateHost(Action<HostApplicationBuilder>? builderOptions = null, string configPath = "appsettings.json", bool useRealDb = false, params string[] args)
{
var builder = Host.CreateApplicationBuilder(args.Any() ? args : null);
var config = builder.Configuration;
builder.Configuration.AddJsonFile(configPath, optional: true, reloadOnChange: true);
builder.Services
.AddEnvelopeGeneratorInfrastructureServices((provider, opt) =>
{
if (useRealDb)
{
var connStr = config.GetConnectionString("Default")
?? throw new InvalidOperationException("There is no default connection string in appsettings.json.");
opt.UseSqlServer(connStr);
}
else
opt.UseInMemoryDatabase("MockDB");
})
.AddEnvelopeGeneratorServices(builder.Configuration)
.AddScoped<DocumentStatusService>();
builderOptions?.Invoke(builder);
var host = builder.Build();
return host;
}
}

View File

@@ -1,22 +0,0 @@
using Microsoft.Extensions.Hosting;
namespace EnvelopeGenerator.Tests.Application.Services;
[TestFixture]
public class DocumentStatusServiceTests
{
private IHost _host;
[SetUp]
public void SetUp()
{
_host = Mock.CreateHost(useRealDb: true);
}
[TearDown]
public void TearDown()
{
_host.StopAsync();
_host.Dispose();
}
}

View File

@@ -2,11 +2,12 @@
using EnvelopeGenerator.Application.Interfaces.Services;
using EnvelopeGenerator.Application.Dto.Receiver;
using EnvelopeGenerator.Domain.Entities;
using EnvelopeGenerator.Application.Receivers.Commands;
namespace EnvelopeGenerator.Web.Controllers.Test;
[Obsolete("Use MediatR")]
public class TestReceiverController : CRUDControllerBase<IReceiverService, ReceiverCreateDto, ReceiverReadDto, ReceiverUpdateDto, Receiver, int>
public class TestReceiverController : CRUDControllerBase<IReceiverService, CreateReceiverCommand, ReceiverReadDto, UpdateReceiverCommand, Receiver, int>
{
public TestReceiverController(ILogger<TestReceiverController> logger, IReceiverService service) : base(logger, service)
{

View File

@@ -5,8 +5,6 @@ VisualStudioVersion = 17.5.33516.290
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.BBTests", "EnvelopeGenerator.BBTests\EnvelopeGenerator.BBTests.vbproj", "{089D5634-FB6B-42D0-B912-7AA7457044E7}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.Form", "EnvelopeGenerator.Form\EnvelopeGenerator.Form.vbproj", "{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}"
EndProject
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EnvelopeGenerator.CommonServices", "EnvelopeGenerator.CommonServices\EnvelopeGenerator.CommonServices.vbproj", "{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvelopeGenerator.Web", "EnvelopeGenerator.Web\EnvelopeGenerator.Web.csproj", "{5E0E17C0-FF5A-4246-BF87-1ADD85376A27}"
@@ -45,10 +43,6 @@ Global
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{089D5634-FB6B-42D0-B912-7AA7457044E7}.Release|Any CPU.Build.0 = Release|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C}.Release|Any CPU.Build.0 = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -91,7 +85,6 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{089D5634-FB6B-42D0-B912-7AA7457044E7} = {0CBC2432-A561-4440-89BC-671B66A24146}
{6D56C01F-D6CB-4D8A-BD3D-4FD34326998C} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{6EA0C51F-C2B1-4462-8198-3DE0B32B74F8} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}
{5E0E17C0-FF5A-4246-BF87-1ADD85376A27} = {E3C758DC-914D-4B7E-8457-0813F1FDB0CB}
{83ED2617-B398-4859-8F59-B38F8807E83E} = {9943209E-1744-4944-B1BA-4F87FC1A0EEB}