From cda36d13dbea1c0dcc0ea38b93ed3c4077c59116 Mon Sep 17 00:00:00 2001 From: Quinten Van Assche Date: Tue, 12 Dec 2023 15:10:25 +0100 Subject: [PATCH] feat: or-2006 prevent regsitreer kbo from being instered twice --- .../Extensions/MartenExtensions.cs | 3 + src/AssociationRegistry.Admin.Api/Program.cs | 1 + ...egistreerVerenigingUitKboCommandHandler.cs | 43 +++++++++-- .../EventStore/ILockStore.cs | 12 +++ .../EventStore/LockStore.cs | 58 ++++++++++++++ .../EventStore/Locks/KboLockDocument.cs | 11 +++ .../EventStore/VerenigingsRepository.cs | 19 ++++- .../Kbo/VerenigingVolgensKbo.cs | 2 +- .../Vereniging/IVerenigingsRepository.cs | 4 + ...ngNumberFoundMagdaGeefVerenigingService.cs | 24 +++++- .../Fakes/VerenigingRepositoryMock.cs | 18 ++++- ...WithLock_And_LockNotRemovedWhileWaiting.cs | 51 +++++++++++++ ...d_LockRemovedWhileWaiting_And_Duplicate.cs | 75 +++++++++++++++++++ ...LockRemovedWhileWaiting_And_NoDuplicate.cs | 57 ++++++++++++++ .../WithoutLock_And_Duplicate.cs} | 28 ++++--- .../WithoutLock_And_NoDuplicate.cs | 59 +++++++++++++++ .../Framework/AlwaysLockStoreMock.cs | 24 ++++++ .../Framework/CountedLockStoreMock.cs | 38 ++++++++++ .../Framework/EventStoreMock.cs | 32 +++++++- .../Framework/NoLockStoreMock.cs | 21 ++++++ .../Given_A_VCode.cs | 4 +- .../Given_A_VCode_And_ExpectedVersion.cs | 14 ++-- .../Given_A_New_Vereniging.cs | 3 +- 23 files changed, 567 insertions(+), 34 deletions(-) create mode 100644 src/AssociationRegistry/EventStore/ILockStore.cs create mode 100644 src/AssociationRegistry/EventStore/LockStore.cs create mode 100644 src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs create mode 100644 test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockNotRemovedWhileWaiting.cs create mode 100644 test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_Duplicate.cs create mode 100644 test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate.cs rename test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/{With_A_Duplicate_KboNummer.cs => When_Duplicate_KboNummer/WithoutLock_And_Duplicate.cs} (72%) create mode 100644 test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_NoDuplicate.cs create mode 100644 test/AssociationRegistry.Test/Framework/AlwaysLockStoreMock.cs create mode 100644 test/AssociationRegistry.Test/Framework/CountedLockStoreMock.cs create mode 100644 test/AssociationRegistry.Test/Framework/NoLockStoreMock.cs diff --git a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs index f99b77f98..815ab7caa 100644 --- a/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs +++ b/src/AssociationRegistry.Admin.Api/Infrastructure/Extensions/MartenExtensions.cs @@ -3,6 +3,7 @@ using AssociationRegistry.Magda.Models; using ConfigurationBindings; using Constants; +using EventStore.Locks; using JasperFx.CodeGeneration; using Json; using Marten; @@ -38,6 +39,8 @@ public static IServiceCollection AddMarten( opts.RegisterDocumentType(); opts.RegisterDocumentType(); + opts.RegisterDocumentType(); + opts.RegisterDocumentType(); opts.Schema.For().Identity(x => x.Reference); diff --git a/src/AssociationRegistry.Admin.Api/Program.cs b/src/AssociationRegistry.Admin.Api/Program.cs index f9cbad68d..0f4fc53ca 100755 --- a/src/AssociationRegistry.Admin.Api/Program.cs +++ b/src/AssociationRegistry.Admin.Api/Program.cs @@ -299,6 +299,7 @@ private static void ConfigureServices(WebApplicationBuilder builder) .AddScoped() .AddSingleton() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() diff --git a/src/AssociationRegistry/Acties/RegistreerVerenigingUitKbo/RegistreerVerenigingUitKboCommandHandler.cs b/src/AssociationRegistry/Acties/RegistreerVerenigingUitKbo/RegistreerVerenigingUitKboCommandHandler.cs index 13329f79c..37e9e6530 100644 --- a/src/AssociationRegistry/Acties/RegistreerVerenigingUitKbo/RegistreerVerenigingUitKboCommandHandler.cs +++ b/src/AssociationRegistry/Acties/RegistreerVerenigingUitKbo/RegistreerVerenigingUitKboCommandHandler.cs @@ -23,19 +23,45 @@ public RegistreerVerenigingUitKboCommandHandler( _magdaGeefVerenigingService = magdaGeefVerenigingService; } - public async Task Handle(CommandEnvelope message, CancellationToken cancellationToken = default) + public async Task Handle( + CommandEnvelope message, + CancellationToken cancellationToken = default) { var command = message.Command; - var duplicateResult = await CheckForDuplicate(command.KboNummer); + for (var i = 0; i <= 60; i += 3) + { + var duplicateResult = await CheckForDuplicate(command.KboNummer); - if (duplicateResult.IsFailure()) return duplicateResult; + if (duplicateResult.IsFailure()) return duplicateResult; + + var kboLockDocument = await _verenigingsRepository.GetKboNummerLock(command.KboNummer); + var hasLock = kboLockDocument is not null; + + // Lock door andere instance dus even wachten + if (hasLock) + { + await Task.Delay(3 * 1000, cancellationToken); + + continue; + } + + try + { + await _verenigingsRepository.SetKboNummerLock(command.KboNummer); + var vereniging = await _magdaGeefVerenigingService.GeefVereniging(command.KboNummer, message.Metadata, cancellationToken); - var vereniging = await _magdaGeefVerenigingService.GeefVereniging(command.KboNummer, message.Metadata, cancellationToken); + if (vereniging.IsFailure()) throw new GeenGeldigeVerenigingInKbo(); - if (vereniging.IsFailure()) throw new GeenGeldigeVerenigingInKbo(); + return await RegistreerVereniging(vereniging, message.Metadata, cancellationToken); + } + finally + { + await _verenigingsRepository.DeleteKboNummerLock(command.KboNummer); + } + } - return await RegistreerVereniging(vereniging, message.Metadata, cancellationToken); + throw new ApplicationException($"Kan niet langer wachten op lock voor KBO nummer {command.KboNummer}"); } private async Task CheckForDuplicate(KboNummer kboNummer) @@ -45,7 +71,10 @@ private async Task CheckForDuplicate(KboNummer kboNummer) return duplicateKbo is not null ? DuplicateKboFound.WithVcode(duplicateKbo.VCode!) : Result.Success(); } - private async Task RegistreerVereniging(VerenigingVolgensKbo verenigingVolgensKbo, CommandMetadata messageMetadata, CancellationToken cancellationToken) + private async Task RegistreerVereniging( + VerenigingVolgensKbo verenigingVolgensKbo, + CommandMetadata messageMetadata, + CancellationToken cancellationToken) { var vCode = await _vCodeService.GetNext(); diff --git a/src/AssociationRegistry/EventStore/ILockStore.cs b/src/AssociationRegistry/EventStore/ILockStore.cs new file mode 100644 index 000000000..69be81a09 --- /dev/null +++ b/src/AssociationRegistry/EventStore/ILockStore.cs @@ -0,0 +1,12 @@ +namespace AssociationRegistry.EventStore; + +using Locks; +using Vereniging; + +public interface ILockStore +{ + Task GetKboNummerLock(KboNummer kboNummer); + Task SetKboNummerLock(KboNummer kboNummer); + Task DeleteKboNummerLock(KboNummer kboNummer); + Task CleanKboNummerLocks(); +} diff --git a/src/AssociationRegistry/EventStore/LockStore.cs b/src/AssociationRegistry/EventStore/LockStore.cs new file mode 100644 index 000000000..6212dbd2c --- /dev/null +++ b/src/AssociationRegistry/EventStore/LockStore.cs @@ -0,0 +1,58 @@ +namespace AssociationRegistry.EventStore; + +using Locks; +using Marten; +using Vereniging; + +public class LockStore : ILockStore +{ + private readonly IDocumentStore _documentStore; + + public LockStore(IDocumentStore documentStore) + { + _documentStore = documentStore; + } + + public async Task CleanKboNummerLocks() + { + await using var session = _documentStore.LightweightSession(); + session.DeleteWhere(doc => doc.CreatedAt <= DateTimeOffset.UtcNow.AddMinutes(-1)); + await session.SaveChangesAsync(); + } + + public async Task GetKboNummerLock(KboNummer kboNummer) + { + try + { + await using var session = _documentStore.QuerySession(); + + return await session.LoadAsync(kboNummer); + } + catch + { + return await Task.FromResult(null); + } + } + + public async Task SetKboNummerLock(KboNummer kboNummer) + { + await using var session = _documentStore.LightweightSession(); + + session.Store(new KboLockDocument + { + KboNummer = kboNummer, + CreatedAt = DateTimeOffset.UtcNow, + }); + + await session.SaveChangesAsync(); + } + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + { + await using var session = _documentStore.LightweightSession(); + + session.Delete(kboNummer); + + await session.SaveChangesAsync(); + } +} diff --git a/src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs b/src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs new file mode 100644 index 000000000..a18bc1985 --- /dev/null +++ b/src/AssociationRegistry/EventStore/Locks/KboLockDocument.cs @@ -0,0 +1,11 @@ +namespace AssociationRegistry.EventStore.Locks; + +using Marten.Schema; + +public class KboLockDocument +{ + [Identity] + public string KboNummer { get; set; } + + public DateTimeOffset CreatedAt { get; set; } +} diff --git a/src/AssociationRegistry/EventStore/VerenigingsRepository.cs b/src/AssociationRegistry/EventStore/VerenigingsRepository.cs index d3aa6d0ca..aa5b9cd20 100644 --- a/src/AssociationRegistry/EventStore/VerenigingsRepository.cs +++ b/src/AssociationRegistry/EventStore/VerenigingsRepository.cs @@ -1,15 +1,19 @@ namespace AssociationRegistry.EventStore; using Framework; +using Locks; +using Marten; using Vereniging; public class VerenigingsRepository : IVerenigingsRepository { private readonly IEventStore _eventStore; + private readonly ILockStore _lockStore; - public VerenigingsRepository(IEventStore eventStore) + public VerenigingsRepository(IEventStore eventStore, ILockStore lockStore) { _eventStore = eventStore; + _lockStore = lockStore; } public async Task Save( @@ -49,6 +53,19 @@ public async Task Load(VCode vCode, long? expectedVers return new VCodeAndNaam(verenigingState.VCode, verenigingState.Naam); } + public async Task GetKboNummerLock(KboNummer kboNummer) + { + await _lockStore.CleanKboNummerLocks(); + + return await _lockStore.GetKboNummerLock(kboNummer); + } + + public async Task SetKboNummerLock(KboNummer kboNummer) + => await _lockStore.SetKboNummerLock(kboNummer); + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => await _lockStore.DeleteKboNummerLock(kboNummer); + public record VCodeAndNaam(VCode? VCode, VerenigingsNaam VerenigingsNaam) { public static VCodeAndNaam Fallback(KboNummer kboNummer) diff --git a/src/AssociationRegistry/Kbo/VerenigingVolgensKbo.cs b/src/AssociationRegistry/Kbo/VerenigingVolgensKbo.cs index e55a8fefe..12f19d483 100644 --- a/src/AssociationRegistry/Kbo/VerenigingVolgensKbo.cs +++ b/src/AssociationRegistry/Kbo/VerenigingVolgensKbo.cs @@ -2,7 +2,7 @@ using Vereniging; -public class VerenigingVolgensKbo +public record VerenigingVolgensKbo { public KboNummer KboNummer { get; init; } = null!; public Verenigingstype Type { get; set; } = null!; diff --git a/src/AssociationRegistry/Vereniging/IVerenigingsRepository.cs b/src/AssociationRegistry/Vereniging/IVerenigingsRepository.cs index 71e52b009..ead6b3b17 100644 --- a/src/AssociationRegistry/Vereniging/IVerenigingsRepository.cs +++ b/src/AssociationRegistry/Vereniging/IVerenigingsRepository.cs @@ -1,6 +1,7 @@ namespace AssociationRegistry.Vereniging; using EventStore; +using EventStore.Locks; using Framework; public interface IVerenigingsRepository @@ -8,4 +9,7 @@ public interface IVerenigingsRepository Task Save(VerenigingsBase vereniging, CommandMetadata metadata, CancellationToken cancellationToken); Task Load(VCode vCode, long? expectedVersion) where TVereniging : IHydrate, new(); Task GetVCodeAndNaam(KboNummer kboNummer); + Task GetKboNummerLock(KboNummer kboNummer); + Task SetKboNummerLock(KboNummer kboNummer); + Task DeleteKboNummerLock(KboNummer kboNummer); } diff --git a/test/AssociationRegistry.Test.Admin.Api/Fakes/MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService.cs b/test/AssociationRegistry.Test.Admin.Api/Fakes/MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService.cs index a29690e2d..90b036912 100644 --- a/test/AssociationRegistry.Test.Admin.Api/Fakes/MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService.cs +++ b/test/AssociationRegistry.Test.Admin.Api/Fakes/MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService.cs @@ -1,19 +1,35 @@ namespace AssociationRegistry.Test.Admin.Api.Fakes; using AssociationRegistry.Framework; +using AutoFixture; +using Framework; using Kbo; using ResultNet; using Vereniging; public class MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService : IMagdaGeefVerenigingService { - private readonly VerenigingVolgensKbo _verenigingVolgensKbo; + private readonly VerenigingVolgensKbo? _verenigingVolgensKbo; - public MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService(VerenigingVolgensKbo verenigingVolgensKbo) + public MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService(VerenigingVolgensKbo? verenigingVolgensKbo = null) { _verenigingVolgensKbo = verenigingVolgensKbo; } - public Task> GeefVereniging(KboNummer kboNummer, CommandMetadata metadata, CancellationToken cancellationToken) - => Task.FromResult(VerenigingVolgensKboResult.GeldigeVereniging(_verenigingVolgensKbo)); + public Task> GeefVereniging( + KboNummer kboNummer, + CommandMetadata metadata, + CancellationToken cancellationToken) + => Task.FromResult(VerenigingVolgensKboResult.GeldigeVereniging(_verenigingVolgensKbo ?? VerenigingVolgensKbo(kboNummer))); + + private static VerenigingVolgensKbo VerenigingVolgensKbo(KboNummer kboNummer) + { + var v = new Fixture().CustomizeAdminApi().Create() with + { + KboNummer = kboNummer, + Type = Verenigingstype.VZW, + }; + + return v; + } } diff --git a/test/AssociationRegistry.Test.Admin.Api/Fakes/VerenigingRepositoryMock.cs b/test/AssociationRegistry.Test.Admin.Api/Fakes/VerenigingRepositoryMock.cs index 765b441d6..9baca53cb 100644 --- a/test/AssociationRegistry.Test.Admin.Api/Fakes/VerenigingRepositoryMock.cs +++ b/test/AssociationRegistry.Test.Admin.Api/Fakes/VerenigingRepositoryMock.cs @@ -2,11 +2,14 @@ using AssociationRegistry.Framework; using EventStore; +using EventStore.Locks; using FluentAssertions; +using Test.Framework; using Vereniging; public class VerenigingRepositoryMock : IVerenigingsRepository { + private readonly ILockStore _lockStore; private VerenigingState? _verenigingToLoad; private readonly VerenigingsRepository.VCodeAndNaam _moederVCodeAndNaam; public record SaveInvocation(VerenigingsBase Vereniging); @@ -17,8 +20,12 @@ private record InvocationLoad(VCode VCode, Type Type); public List SaveInvocations { get; } = new(); private readonly List _invocationsLoad = new(); - public VerenigingRepositoryMock(VerenigingState? verenigingToLoad = null, VerenigingsRepository.VCodeAndNaam moederVCodeAndNaam = null!) + public VerenigingRepositoryMock( + VerenigingState? verenigingToLoad = null, + VerenigingsRepository.VCodeAndNaam moederVCodeAndNaam = null!, + ILockStore? lockStore = null) { + _lockStore = lockStore ?? new LockStoreMock(); _verenigingToLoad = verenigingToLoad; _moederVCodeAndNaam = moederVCodeAndNaam; } @@ -53,6 +60,15 @@ public async Task Load(VCode vCode, long? expectedVers public Task GetVCodeAndNaam(KboNummer kboNummer) => Task.FromResult(_moederVCodeAndNaam)!; + public async Task GetKboNummerLock(KboNummer kboNummer) + => await _lockStore.GetKboNummerLock(kboNummer); + + public async Task SetKboNummerLock(KboNummer kboNummer) + => await _lockStore.SetKboNummerLock(kboNummer); + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => await _lockStore.DeleteKboNummerLock(kboNummer); + public void ShouldHaveLoaded(params string[] vCodes) where TVereniging : IHydrate, new() { _invocationsLoad.Should().BeEquivalentTo( diff --git a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockNotRemovedWhileWaiting.cs b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockNotRemovedWhileWaiting.cs new file mode 100644 index 000000000..1c6fe67ff --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockNotRemovedWhileWaiting.cs @@ -0,0 +1,51 @@ +namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid. + CommandHandling. + When_Duplicate_KboNummer; + +using Acties.RegistreerVerenigingUitKbo; +using AssociationRegistry.Framework; +using AutoFixture; +using EventStore; +using Fakes; +using FluentAssertions; +using Framework; +using Kbo; +using Moq; +using Test.Framework; +using Xunit; +using Xunit.Categories; + +[UnitTest] +public class WithLock_And_LockNotRemovedWhileWaiting +{ + private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler; + private readonly CommandEnvelope _envelope; + + public WithLock_And_LockNotRemovedWhileWaiting() + { + var fixture = new Fixture().CustomizeAdminApi(); + + var moederVCodeAndNaam = fixture.Create(); + + _envelope = new CommandEnvelope(fixture.Create(), + fixture.Create()); + + ILockStore lockStoreMock = new AlwaysLockStoreMock(); + var repositoryMock = new VerenigingRepositoryMock(moederVCodeAndNaam: moederVCodeAndNaam, lockStore: lockStoreMock); + + _commandHandler = new RegistreerVerenigingUitKboCommandHandler( + repositoryMock, + new InMemorySequentialVCodeService(), + Mock.Of()); + } + + [Fact] + public void Then_It_Throws_An_Exception() + { + var handle = async () => await _commandHandler + .Handle(_envelope, CancellationToken.None); + + handle.Should().ThrowAsync() + .WithMessage($"Kan niet langer wachten op lock voor KBO nummer {_envelope.Command.KboNummer}"); + } +} diff --git a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_Duplicate.cs b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_Duplicate.cs new file mode 100644 index 000000000..fd01e67ae --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_Duplicate.cs @@ -0,0 +1,75 @@ +namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid. + CommandHandling. + When_Duplicate_KboNummer; + +using Acties.RegistreerVerenigingUitKbo; +using AssociationRegistry.Framework; +using AutoFixture; +using DuplicateVerenigingDetection; +using EventStore; +using Fakes; +using FluentAssertions; +using Framework; +using Kbo; +using Moq; +using ResultNet; +using Test.Framework; +using Xunit; +using Xunit.Categories; + +[UnitTest] +public class WithLock_And_LockRemovedWhileWaiting_And_Duplicate : IAsyncLifetime +{ + private Result _result = null!; + private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler; + private readonly CommandEnvelope _envelope; + private readonly VerenigingsRepository.VCodeAndNaam _moederVCodeAndNaam; + private readonly Mock _magdaGeefVerenigingService; + + public WithLock_And_LockRemovedWhileWaiting_And_Duplicate() + { + var fixture = new Fixture().CustomizeAdminApi(); + + _moederVCodeAndNaam = fixture.Create(); + + _envelope = new CommandEnvelope(fixture.Create(), + fixture.Create()); + + _magdaGeefVerenigingService = new Mock(); + + ILockStore lockStoreMock = new CountedLockStoreMock(2); + var repositoryMock = new VerenigingRepositoryMock(moederVCodeAndNaam: _moederVCodeAndNaam, lockStore: lockStoreMock); + + _commandHandler = new RegistreerVerenigingUitKboCommandHandler( + repositoryMock, + new InMemorySequentialVCodeService(), + _magdaGeefVerenigingService.Object); + } + + public async Task InitializeAsync() + { + _result = await _commandHandler + .Handle(_envelope, CancellationToken.None); + } + + [Fact] + public void Then_The_Result_Is_A_Failure() + { + _result.IsFailure().Should().BeTrue(); + } + + [Fact] + public void Then_The_Result_Contains_The_Duplicate_VCode() + { + ((Result)_result).Data.VCode.Should().BeEquivalentTo(_moederVCodeAndNaam.VCode); + } + + [Fact] + public void Then_The_MagdaService_Is_Not_Invoked() + { + _magdaGeefVerenigingService.Invocations.Should().BeEmpty(); + } + + public Task DisposeAsync() + => Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate.cs b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate.cs new file mode 100644 index 000000000..11a319e8e --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate.cs @@ -0,0 +1,57 @@ +namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid. + CommandHandling. + When_Duplicate_KboNummer; + +using Acties.RegistreerVerenigingUitKbo; +using AssociationRegistry.Framework; +using AutoFixture; +using DuplicateVerenigingDetection; +using EventStore; +using Fakes; +using FluentAssertions; +using Framework; +using Kbo; +using Moq; +using ResultNet; +using Test.Framework; +using Xunit; +using Xunit.Categories; + +[UnitTest] +public class WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate : IAsyncLifetime +{ + private Result _result = null!; + private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler; + private readonly CommandEnvelope _envelope; + + public WithLock_And_LockRemovedWhileWaiting_And_NoDuplicate() + { + var fixture = new Fixture().CustomizeAdminApi(); + + _envelope = new CommandEnvelope(fixture.Create(), + fixture.Create()); + + var lockStoreMock = new CountedLockStoreMock(2); + var repositoryMock = new VerenigingRepositoryMock(lockStore: lockStoreMock); + + _commandHandler = new RegistreerVerenigingUitKboCommandHandler( + repositoryMock, + new InMemorySequentialVCodeService(), + new MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService()); + } + + public async Task InitializeAsync() + { + _result = await _commandHandler + .Handle(_envelope, CancellationToken.None); + } + + [Fact] + public void Then_The_Result_Is_A_Success() + { + _result.IsSuccess().Should().BeTrue(); + } + + public Task DisposeAsync() + => Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/With_A_Duplicate_KboNummer.cs b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_Duplicate.cs similarity index 72% rename from test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/With_A_Duplicate_KboNummer.cs rename to test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_Duplicate.cs index d4d1538d9..e82b398bd 100644 --- a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/With_A_Duplicate_KboNummer.cs +++ b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_Duplicate.cs @@ -1,38 +1,47 @@ -namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid.CommandHandling; +namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid. + CommandHandling. + When_Duplicate_KboNummer; using Acties.RegistreerVerenigingUitKbo; using AssociationRegistry.Framework; -using Fakes; -using Framework; using AutoFixture; using DuplicateVerenigingDetection; using EventStore; +using Fakes; using FluentAssertions; +using Framework; using Kbo; using Moq; using ResultNet; +using Test.Framework; using Xunit; using Xunit.Categories; [UnitTest] -public class With_A_Duplicate_KboNummer : IAsyncLifetime +public class WithoutLock_And_Duplicate : IAsyncLifetime { private Result _result = null!; private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler; private readonly CommandEnvelope _envelope; private readonly VerenigingsRepository.VCodeAndNaam _moederVCodeAndNaam; - private Mock _magdaGeefVerenigingService; + private readonly Mock _magdaGeefVerenigingService; - public With_A_Duplicate_KboNummer() + public WithoutLock_And_Duplicate() { var fixture = new Fixture().CustomizeAdminApi(); _moederVCodeAndNaam = fixture.Create(); - _envelope = new CommandEnvelope(fixture.Create(), fixture.Create()); + _envelope = new CommandEnvelope(fixture.Create(), + fixture.Create()); + _magdaGeefVerenigingService = new Mock(); + + var lockStoreMock = new NoLockStoreMock(); + var repositoryMock = new VerenigingRepositoryMock(moederVCodeAndNaam: _moederVCodeAndNaam, lockStore: lockStoreMock); + _commandHandler = new RegistreerVerenigingUitKboCommandHandler( - new VerenigingRepositoryMock(moederVCodeAndNaam: _moederVCodeAndNaam), + repositoryMock, new InMemorySequentialVCodeService(), _magdaGeefVerenigingService.Object); } @@ -40,10 +49,9 @@ public With_A_Duplicate_KboNummer() public async Task InitializeAsync() { _result = await _commandHandler - .Handle(_envelope, CancellationToken.None); + .Handle(_envelope, CancellationToken.None); } - [Fact] public void Then_The_Result_Is_A_Failure() { diff --git a/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_NoDuplicate.cs b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_NoDuplicate.cs new file mode 100644 index 000000000..55ca16bb7 --- /dev/null +++ b/test/AssociationRegistry.Test.Admin.Api/VerenigingMetRechtspersoonlijkheid/When_RegistreerVerenigingMetRechtspersoonlijkheid/CommandHandling/When_Duplicate_KboNummer/WithoutLock_And_NoDuplicate.cs @@ -0,0 +1,59 @@ +namespace AssociationRegistry.Test.Admin.Api.VerenigingMetRechtspersoonlijkheid.When_RegistreerVerenigingMetRechtspersoonlijkheid. + CommandHandling. + When_Duplicate_KboNummer; + +using Acties.RegistreerVerenigingUitKbo; +using AssociationRegistry.Framework; +using AutoFixture; +using EventStore; +using Fakes; +using FluentAssertions; +using Framework; +using Kbo; +using Moq; +using ResultNet; +using Test.Framework; +using Vereniging; +using Xunit; +using Xunit.Categories; + +[UnitTest] +public class WithoutLock_And_NoDuplicate : IAsyncLifetime +{ + private Result _result = null!; + private readonly RegistreerVerenigingUitKboCommandHandler _commandHandler; + private readonly CommandEnvelope _envelope; + + public WithoutLock_And_NoDuplicate() + { + var fixture = new Fixture().CustomizeAdminApi(); + + + _envelope = new CommandEnvelope(fixture.Create(), + fixture.Create()); + + + ILockStore lockStoreMock = new NoLockStoreMock(); + var repositoryMock = new VerenigingRepositoryMock(lockStore: lockStoreMock); + + _commandHandler = new RegistreerVerenigingUitKboCommandHandler( + repositoryMock, + new InMemorySequentialVCodeService(), + new MagdaGeefVerenigingNumberFoundMagdaGeefVerenigingService()); + } + + public async Task InitializeAsync() + { + _result = await _commandHandler + .Handle(_envelope, CancellationToken.None); + } + + [Fact] + public void Then_The_Result_Is_A_Success() + { + _result.IsSuccess().Should().BeTrue(); + } + + public Task DisposeAsync() + => Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test/Framework/AlwaysLockStoreMock.cs b/test/AssociationRegistry.Test/Framework/AlwaysLockStoreMock.cs new file mode 100644 index 000000000..83488810c --- /dev/null +++ b/test/AssociationRegistry.Test/Framework/AlwaysLockStoreMock.cs @@ -0,0 +1,24 @@ +namespace AssociationRegistry.Test.Framework; + +using EventStore; +using EventStore.Locks; +using Vereniging; + +public class AlwaysLockStoreMock : ILockStore +{ + + public async Task GetKboNummerLock(KboNummer kboNummer) + => await Task.FromResult(new KboLockDocument + { + KboNummer = kboNummer, + }); + + public async Task SetKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task CleanKboNummerLocks() + => await Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test/Framework/CountedLockStoreMock.cs b/test/AssociationRegistry.Test/Framework/CountedLockStoreMock.cs new file mode 100644 index 000000000..d420e3a91 --- /dev/null +++ b/test/AssociationRegistry.Test/Framework/CountedLockStoreMock.cs @@ -0,0 +1,38 @@ +namespace AssociationRegistry.Test.Framework; + +using EventStore; +using EventStore.Locks; +using Vereniging; + +public class CountedLockStoreMock : ILockStore +{ + private readonly int _retryCount; + private int _callCounter; + + public CountedLockStoreMock(int retryCount = 1) + { + _retryCount = retryCount; + _callCounter = 0; + } + + public async Task GetKboNummerLock(KboNummer kboNummer) + { + _callCounter++; + + return await Task.FromResult(_callCounter > _retryCount + ? null + : new KboLockDocument + { + KboNummer = kboNummer, + }); + } + + public async Task SetKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task CleanKboNummerLocks() + => await Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test/Framework/EventStoreMock.cs b/test/AssociationRegistry.Test/Framework/EventStoreMock.cs index ccaf68095..4ea5b98b9 100644 --- a/test/AssociationRegistry.Test/Framework/EventStoreMock.cs +++ b/test/AssociationRegistry.Test/Framework/EventStoreMock.cs @@ -2,14 +2,13 @@ using EventStore; using AssociationRegistry.Framework; +using EventStore.Locks; using Vereniging; public class EventStoreMock : IEventStore { public record SaveInvocation(string AggregateId, IEvent[] Events); - public record LoadInvocation(string AggregateId, Type Type); - private readonly IEvent[] _events; public EventStoreMock(params IEvent[] events) @@ -19,15 +18,21 @@ public EventStoreMock(params IEvent[] events) public readonly List SaveInvocations = new(); - public Task Save(string aggregateId, CommandMetadata metadata, CancellationToken cancellationToken = default, params IEvent[] events) + public Task Save( + string aggregateId, + CommandMetadata metadata, + CancellationToken cancellationToken = default, + params IEvent[] events) { SaveInvocations.Add(new SaveInvocation(aggregateId, events)); + return Task.FromResult(new StreamActionResult(-1, -1)); } public Task Load(string aggregateId) where T : class, IHasVersion, new() { var result = new T(); + for (var i = 0; i < _events.Length; i++) { result = ((dynamic)result).Apply((dynamic)_events[i]); @@ -40,3 +45,24 @@ public Task Save(string aggregateId, CommandMetadata metadat public Task Load(KboNummer kboNummer) where T : class, IHasVersion, new() => Task.FromException(new NotImplementedException()); } + +public class LockStoreMock : ILockStore +{ + private List docs = new(); + + public async Task GetKboNummerLock(KboNummer kboNummer) + => docs.SingleOrDefault(d => d.KboNummer == kboNummer); + + public async Task SetKboNummerLock(KboNummer kboNummer) + => docs.Add(new KboLockDocument + { + KboNummer = kboNummer, + CreatedAt = DateTimeOffset.UtcNow, + }); + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => docs = docs.Where(d => d.KboNummer != kboNummer).ToList(); + + public async Task CleanKboNummerLocks() + => docs = docs.Where(d => d.CreatedAt <= DateTimeOffset.UtcNow.AddMinutes(-1)).ToList(); +} diff --git a/test/AssociationRegistry.Test/Framework/NoLockStoreMock.cs b/test/AssociationRegistry.Test/Framework/NoLockStoreMock.cs new file mode 100644 index 000000000..5b63842ca --- /dev/null +++ b/test/AssociationRegistry.Test/Framework/NoLockStoreMock.cs @@ -0,0 +1,21 @@ +namespace AssociationRegistry.Test.Framework; + +using EventStore; +using EventStore.Locks; +using Vereniging; + +public class NoLockStoreMock : ILockStore +{ + + public async Task GetKboNummerLock(KboNummer kboNummer) + => await Task.FromResult(null); + + public async Task SetKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task DeleteKboNummerLock(KboNummer kboNummer) + => await Task.CompletedTask; + + public async Task CleanKboNummerLocks() + => await Task.CompletedTask; +} diff --git a/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode.cs b/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode.cs index 031d6bfdb..ba82544f1 100644 --- a/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode.cs +++ b/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode.cs @@ -23,7 +23,9 @@ public Given_A_VCode() _vCode = fixture.Create(); var eventStoreMock = new EventStoreMock( fixture.Create() with {VCode = _vCode}); - _repo = new VerenigingsRepository(eventStoreMock); + + var lockStoreMock = new LockStoreMock(); + _repo = new VerenigingsRepository(eventStoreMock,lockStoreMock); } [Fact] diff --git a/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode_And_ExpectedVersion.cs b/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode_And_ExpectedVersion.cs index 924baede2..4494224f6 100644 --- a/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode_And_ExpectedVersion.cs +++ b/test/AssociationRegistry.Test/When_Loading_A_Vereniging/Given_A_VCode_And_ExpectedVersion.cs @@ -20,20 +20,24 @@ public Given_A_VCode_And_ExpectedVersion() { var fixture = new Fixture().CustomizeDomain(); _vCode = fixture.Create(); + var eventStoreMock = new EventStoreMock( fixture.Create() with { VCode = _vCode }); - _repo = new VerenigingsRepository(eventStoreMock); + + var lockStoreMock = new LockStoreMock(); + _repo = new VerenigingsRepository(eventStoreMock, lockStoreMock); } [Fact] public async Task Then_A_FeitelijkeVereniging_Is_Returned() { var feteitelijkeVerenging = await _repo.Load(_vCode, 1); + feteitelijkeVerenging - .Should() - .NotBeNull() - .And - .BeOfType(); + .Should() + .NotBeNull() + .And + .BeOfType(); } [Fact] diff --git a/test/AssociationRegistry.Test/When_saving_a_vereniging/Given_A_New_Vereniging.cs b/test/AssociationRegistry.Test/When_saving_a_vereniging/Given_A_New_Vereniging.cs index feda5c632..5b4085a4e 100644 --- a/test/AssociationRegistry.Test/When_saving_a_vereniging/Given_A_New_Vereniging.cs +++ b/test/AssociationRegistry.Test/When_saving_a_vereniging/Given_A_New_Vereniging.cs @@ -22,8 +22,9 @@ public class Given_A_New_Vereniging : IAsyncLifetime public Given_A_New_Vereniging() { _eventStore = new EventStoreMock(); + var lockStoreMock = new LockStoreMock(); - _repo = new VerenigingsRepository(_eventStore); + _repo = new VerenigingsRepository(_eventStore, lockStoreMock); _vCode = VCode.Create(1001); _naam = VerenigingsNaam.Create("Vereniging 1");