From b34229e78208f67abd72818895758274aed6f87a Mon Sep 17 00:00:00 2001 From: Timo Notheisen <65653426+tnotheis@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:11:31 +0100 Subject: [PATCH] Queue external events for new messages if relationship is in status pending (#1035) * feat: queue external events * feat: unblock external events when relationship goes to status Active * feat: delete blocked external events when relationship goes to status Rejected or Revoked * test: add integration tests * feat: allow sending messages while relationship is in status pending * test: update unit test * test: check for correct error code * test: move test to correct location * test: add additional waits to fix failing tests * test: remove redundant step definitions --- .../Features/Messages/POST.feature | 6 ++ .../SyncRuns/{id}/ExternalEvents/GET.feature | 23 +++++++ .../RelationshipsStepDefinitions.cs | 19 ++++++ .../Messages.Domain/Entities/Relationship.cs | 2 +- .../EnsureSendingMessagesIsAllowedTests.cs | 4 +- .../MessageCreatedDomainEventHandler.cs | 2 +- ...tionshipStatusChangedDomainEventHandler.cs | 26 ++++++++ .../ISynchronizationDbContext.cs | 1 + .../Database/SynchronizationDbContext.cs | 9 +++ .../MessageCreatedDomainEventHandlerTests.cs | 8 ++- ...hipStatusChangedDomainEventHandlerTests.cs | 64 +++++++++++++++++++ 11 files changed, 157 insertions(+), 7 deletions(-) diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Messages/POST.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Messages/POST.feature index 82c04534be..6bde6310db 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Messages/POST.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/Messages/POST.feature @@ -18,6 +18,12 @@ Identity sends a Message Then the response status code is 201 (Created) And the response contains a SendMessageResponse + Scenario: Sending a Message to a pending Relationship + Given Identities i1 and i2 + And a pending Relationship r between i1 and i2 + When i1 sends a POST request to the /Messages endpoint with i2 as recipient + Then the response status code is 201 (Created) + Scenario: Sending a Message to a terminated Relationship Given Identities i1 and i2 And a terminated Relationship r between i1 and i2 diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/SyncRuns/{id}/ExternalEvents/GET.feature b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/SyncRuns/{id}/ExternalEvents/GET.feature index 1ff63207bb..8465e24020 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/SyncRuns/{id}/ExternalEvents/GET.feature +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/Features/SyncRuns/{id}/ExternalEvents/GET.feature @@ -5,6 +5,7 @@ Feature: GET /SyncRuns/{id}/ExternalEvents Given Identities i1 and i2 And a terminated Relationship r between i1 and i2 And i1 has sent a Message m to i2 + And 5 second(s) have passed And a sync run sr started by i2 When i2 sends a GET request to the /SyncRuns/sr.id/ExternalEvents endpoint Then the response status code is 200 (OK) @@ -25,6 +26,7 @@ Feature: GET /SyncRuns/{id}/ExternalEvents Given Identities i1 and i2 And an active Relationship r between i1 and i2 And i1 has sent a Message m to i2 + And 2 second(s) have passed And i1 has terminated r And 2 second(s) have passed And a sync run sr started by i2 @@ -32,6 +34,27 @@ Feature: GET /SyncRuns/{id}/ExternalEvents Then the response status code is 200 (OK) Then the response contains an external event for the Message m + Scenario: Getting external events returns events for messages sent while the Relationship was pending + Given Identities i1 and i2 + And a pending Relationship r between i1 and i2 + And i1 has sent a Message m to i2 + And 2 second(s) have passed + And r was accepted + And 2 second(s) have passed + And a sync run sr started by i2 + When i2 sends a GET request to the /SyncRuns/sr.id/ExternalEvents endpoint + Then the response status code is 200 (OK) + Then the response contains an external event for the Message m + + Scenario: Getting external events does not return events for messages sent while the Relationship is still pending + Given Identities i1 and i2 + And a pending Relationship r between i1 and i2 + And i1 has sent a Message m to i2 + And a sync run sr started by i2 + When i2 sends a GET request to the /SyncRuns/sr.id/ExternalEvents endpoint + Then the response status code is 200 (OK) + And the response does not contain an external event for the Message m + Scenario: Getting external events does not return events for messages that were sent with an old Relationship Given Identities i1 and i2 And an active Relationship r between i1 and i2 diff --git a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs index 69c46687c2..24cdb596db 100644 --- a/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs +++ b/Applications/ConsumerApi/test/ConsumerApi.Tests.Integration/StepDefinitions/RelationshipsStepDefinitions.cs @@ -41,6 +41,15 @@ public async Task GivenAPendingRelationshipBetween(string relationshipName, stri _relationshipsContext.Relationships[relationshipName] = await Utils.CreatePendingRelationshipBetween(peer, creator); } + [Given($"a pending Relationship {RegexFor.SINGLE_THING} between {RegexFor.SINGLE_THING} and {RegexFor.SINGLE_THING}")] + public async Task GivenAPendingRelationshipBetween(string relationshipName, string participant1Name, string participant2Name) + { + var creator = _clientPool.FirstForIdentityName(participant1Name); + var peer = _clientPool.FirstForIdentityName(participant2Name); + + _relationshipsContext.Relationships[relationshipName] = await Utils.CreatePendingRelationshipBetween(peer, creator); + } + [Given($"a rejected Relationship {RegexFor.SINGLE_THING} between {RegexFor.SINGLE_THING} and {RegexFor.SINGLE_THING}")] public async Task GivenARejectedRelationshipBetween(string relationshipName, string participant1Address, string participant2Address) { @@ -87,6 +96,16 @@ public async Task GivenATerminatedRelationshipWithReactivationRequest(string rel _relationshipsContext.Relationships[relationshipName] = await Utils.CreateTerminatedRelationshipWithReactivationRequestBetween(participant1, participant2); } + [Given($"{RegexFor.SINGLE_THING} was accepted")] + public async Task GivenRWasAccepted(string relationshipName) + { + var relationship = _relationshipsContext.Relationships[relationshipName]; + + var clientTo = _clientPool.FirstForIdentityAddress(relationship.To); + + await clientTo.Relationships.AcceptRelationship(relationship.Id, new AcceptRelationshipRequest()); + } + [Given($"{RegexFor.SINGLE_THING} was fully reactivated")] public async Task GivenRWasFullyReactivated(string relationshipName) { diff --git a/Modules/Messages/src/Messages.Domain/Entities/Relationship.cs b/Modules/Messages/src/Messages.Domain/Entities/Relationship.cs index bf1f3b2e6c..a369269892 100644 --- a/Modules/Messages/src/Messages.Domain/Entities/Relationship.cs +++ b/Modules/Messages/src/Messages.Domain/Entities/Relationship.cs @@ -38,7 +38,7 @@ private Relationship(RelationshipId id, IdentityAddress from, IdentityAddress to public void EnsureSendingMessagesIsAllowed(IdentityAddress activeIdentity, int numberOfUnreceivedMessagesFromActiveIdentity, int maxNumberOfUnreceivedMessagesFromOneSender) { - if (Status is not (RelationshipStatus.Active or RelationshipStatus.Terminated)) + if (Status is not (RelationshipStatus.Active or RelationshipStatus.Pending or RelationshipStatus.Terminated)) throw new DomainException(DomainErrors.RelationshipToRecipientNotActive(GetPeerOf(activeIdentity))); if (numberOfUnreceivedMessagesFromActiveIdentity >= maxNumberOfUnreceivedMessagesFromOneSender) diff --git a/Modules/Messages/test/Messages.Domain.Tests/Relationships/EnsureSendingMessagesIsAllowedTests.cs b/Modules/Messages/test/Messages.Domain.Tests/Relationships/EnsureSendingMessagesIsAllowedTests.cs index d0c0f9b24a..3ff5037bc2 100644 --- a/Modules/Messages/test/Messages.Domain.Tests/Relationships/EnsureSendingMessagesIsAllowedTests.cs +++ b/Modules/Messages/test/Messages.Domain.Tests/Relationships/EnsureSendingMessagesIsAllowedTests.cs @@ -8,7 +8,7 @@ namespace Backbone.Modules.Messages.Domain.Tests.Relationships; public class EnsureSendingMessagesIsAllowedTests : AbstractTestsBase { [Fact] - public void Throws_if_relationship_is_pending() + public void Does_not_throw_if_relationship_is_pending() { // Arrange var relationship = CreateRelationship(RelationshipStatus.Pending); @@ -17,7 +17,7 @@ public void Throws_if_relationship_is_pending() var acting = () => relationship.EnsureSendingMessagesIsAllowed(CreateRandomIdentityAddress(), 0, 5); // Assert - acting.Should().Throw().Which.Code.Should().Be("error.platform.validation.message.relationshipToRecipientNotActive"); + acting.Should().NotThrow(); } [Fact] diff --git a/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/MessageCreated/MessageCreatedDomainEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/MessageCreated/MessageCreatedDomainEventHandler.cs index 3d2a4a3e12..8aba41feb0 100644 --- a/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/MessageCreated/MessageCreatedDomainEventHandler.cs +++ b/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/MessageCreated/MessageCreatedDomainEventHandler.cs @@ -51,7 +51,7 @@ private async Task CreateExternalEventForRecipient(MessageCreatedDomainEvent @ev var externalEvent = new MessageReceivedExternalEvent(IdentityAddress.Parse(recipient), payload, relationship.Id); - if (relationship.Status == RelationshipStatus.Terminated) + if (relationship.Status is RelationshipStatus.Pending or RelationshipStatus.Terminated) externalEvent.BlockDelivery(); await _dbContext.CreateExternalEvent(externalEvent); diff --git a/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/RelationshipStatusChanged/RelationshipStatusChangedDomainEventHandler.cs b/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/RelationshipStatusChanged/RelationshipStatusChangedDomainEventHandler.cs index a244a4a870..fd280c303a 100644 --- a/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/RelationshipStatusChanged/RelationshipStatusChangedDomainEventHandler.cs +++ b/Modules/Synchronization/src/Synchronization.Application/DomainEvents/Incoming/RelationshipStatusChanged/RelationshipStatusChangedDomainEventHandler.cs @@ -23,6 +23,8 @@ public async Task Handle(RelationshipStatusChangedDomainEvent @event) { await CreateRelationshipStatusChangedExternalEvent(@event); await DeleteExternalEvents(@event); + await UnblockMessageReceivedExternalEvents(@event); + await DeleteBlockedMessageReceivedExternalEvents(@event); } catch (Exception ex) { @@ -53,4 +55,28 @@ private async Task DeleteExternalEvents(RelationshipStatusChangedDomainEvent @ev await _dbContext.DeleteUnsyncedExternalEventsWithOwnerAndContext(@event.Initiator, @event.RelationshipId); } } + + private async Task UnblockMessageReceivedExternalEvents(RelationshipStatusChangedDomainEvent @event) + { + if (@event.NewStatus != "Active") + return; + + var externalEvents = await _dbContext.GetBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, @event.RelationshipId, CancellationToken.None); + + foreach (var externalEvent in externalEvents) + { + externalEvent.UnblockDelivery(); + _dbContext.Set().Update(externalEvent); + } + + await _dbContext.SaveChangesAsync(CancellationToken.None); + } + + private async Task DeleteBlockedMessageReceivedExternalEvents(RelationshipStatusChangedDomainEvent @event) + { + if (@event.NewStatus is not "Revoked" and not "Rejected") + return; + + await _dbContext.DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, @event.RelationshipId, CancellationToken.None); + } } diff --git a/Modules/Synchronization/src/Synchronization.Application/Infrastructure/ISynchronizationDbContext.cs b/Modules/Synchronization/src/Synchronization.Application/Infrastructure/ISynchronizationDbContext.cs index 4e75909568..62d6070d0a 100644 --- a/Modules/Synchronization/src/Synchronization.Application/Infrastructure/ISynchronizationDbContext.cs +++ b/Modules/Synchronization/src/Synchronization.Application/Infrastructure/ISynchronizationDbContext.cs @@ -22,6 +22,7 @@ Task> GetDatawalletModifications(Iden Task GetPreviousSyncRunWithExternalEvents(IdentityAddress createdBy, CancellationToken cancellationToken); Task> GetUnsyncedExternalEvents(IdentityAddress owner, byte maxErrorCount, CancellationToken cancellationToken); Task> GetBlockedExternalEventsWithTypeAndContext(ExternalEventType type, string context, CancellationToken cancellationToken); + Task DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType type, string context, CancellationToken cancellationToken); Task> GetExternalEventsOfSyncRun(PaginationFilter paginationFilter, IdentityAddress owner, SyncRunId syncRunId, CancellationToken cancellationToken); diff --git a/Modules/Synchronization/src/Synchronization.Infrastructure/Persistence/Database/SynchronizationDbContext.cs b/Modules/Synchronization/src/Synchronization.Infrastructure/Persistence/Database/SynchronizationDbContext.cs index 33b0e51d79..f9b0457dfc 100644 --- a/Modules/Synchronization/src/Synchronization.Infrastructure/Persistence/Database/SynchronizationDbContext.cs +++ b/Modules/Synchronization/src/Synchronization.Infrastructure/Persistence/Database/SynchronizationDbContext.cs @@ -180,6 +180,15 @@ public async Task> GetUnsyncedExternalEvents(IdentityAddress return unsyncedEvents; } + public async Task DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType type, string context, CancellationToken cancellationToken) + { + await ExternalEvents + .Blocked() + .WithType(type) + .WithContext(context) + .ExecuteDeleteAsync(cancellationToken); + } + public async Task> GetExternalEventsOfSyncRun(PaginationFilter paginationFilter, IdentityAddress owner, SyncRunId syncRunId, CancellationToken cancellationToken) { var query = await ExternalEvents diff --git a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/MessageCreatedDomainEventHandlerTests.cs b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/MessageCreatedDomainEventHandlerTests.cs index ce6740cb30..7eecbde65f 100644 --- a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/MessageCreatedDomainEventHandlerTests.cs +++ b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/MessageCreatedDomainEventHandlerTests.cs @@ -62,14 +62,16 @@ await handler.Handle(new MessageCreatedDomainEvent .MustHaveHappenedOnceExactly(); } - [Fact] - public async Task Created_external_events_are_blocked_when_relationship_with_recipient_is_in_status_Terminated() + [Theory] + [InlineData(RelationshipStatus.Pending)] + [InlineData(RelationshipStatus.Terminated)] + public async Task Created_external_events_are_blocked_when_relationship_with_recipient_is_in_status_pending_or_terminated(RelationshipStatus relationshipStatus) { // Arrange var senderAddress = CreateRandomIdentityAddress(); var recipientAddress = CreateRandomIdentityAddress(); - var relationshipToRecipient = new Relationship(new RelationshipId("REL11111111111111111"), senderAddress, recipientAddress, RelationshipStatus.Terminated); + var relationshipToRecipient = new Relationship(new RelationshipId("REL11111111111111111"), senderAddress, recipientAddress, relationshipStatus); var mockSynchronizationDbContext = A.Fake(); var fakeRelationshipsRepository = RelationshipsRepositoryReturning([relationshipToRecipient]); diff --git a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/RelationshipStatusChangedDomainEventHandlerTests.cs b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/RelationshipStatusChangedDomainEventHandlerTests.cs index f9f8e7d0b0..7db7473885 100644 --- a/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/RelationshipStatusChangedDomainEventHandlerTests.cs +++ b/Modules/Synchronization/test/Synchronization.Application.Tests/Tests/DomainEvents/RelationshipStatusChangedDomainEventHandlerTests.cs @@ -1,6 +1,7 @@ using Backbone.Modules.Synchronization.Application.DomainEvents.Incoming.RelationshipStatusChanged; using Backbone.Modules.Synchronization.Application.Infrastructure; using Backbone.Modules.Synchronization.Domain.DomainEvents.Incoming.RelationshipStatusChanged; +using Backbone.Modules.Synchronization.Domain.Entities.Relationships; using Backbone.Modules.Synchronization.Domain.Entities.Sync; using FakeItEasy; using Microsoft.Extensions.Logging; @@ -85,6 +86,69 @@ public async Task Calls_DeleteUnsyncedExternalEventsWithOwnerAndContext_when_new A.CallTo(() => mockDbContext.DeleteUnsyncedExternalEventsWithOwnerAndContext(initiator, relationshipId)).MustHaveHappenedOnceExactly(); } + [Fact] + public async Task Unblocks_MessageReceivedExternalEvents() + { + // Arrange + var relationshipTo = CreateRandomIdentityAddress(); + const string relationshipId = "REL11111111111111111"; + var initiator = CreateRandomIdentityAddress(); + + var relationshipStatusChangedDomainEvent = new RelationshipStatusChangedDomainEvent + { + RelationshipId = relationshipId, + Peer = relationshipTo, + NewStatus = "Active", + Initiator = initiator + }; + + var mockDbContext = A.Fake(); + + var messageReceivedExternalEvent = + new MessageReceivedExternalEvent(CreateRandomIdentityAddress(), new MessageReceivedExternalEvent.EventPayload { Id = "MSG11111111111111111" }, new RelationshipId(relationshipId)); + messageReceivedExternalEvent.BlockDelivery(); + + A.CallTo(() => mockDbContext.GetBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, A._, A._)) + .Returns([messageReceivedExternalEvent]); + + var handler = CreateHandler(mockDbContext); + + // Act + await handler.Handle(relationshipStatusChangedDomainEvent); + + // Assert + messageReceivedExternalEvent.IsDeliveryBlocked.Should().BeFalse(); + A.CallTo(() => mockDbContext.SaveChangesAsync(A._)).MustHaveHappenedOnceExactly(); + } + + [Theory] + [InlineData("Revoked")] + [InlineData("Rejected")] + public async Task Calls_DeleteBlockedExternalEventsWithTypeAndContext_when_new_status_is_Revoked_or_Rejected(string newStatus) + { + // Arrange + var relationshipTo = CreateRandomIdentityAddress(); + const string relationshipId = "REL11111111111111111"; + var initiator = CreateRandomIdentityAddress(); + var @event = new RelationshipStatusChangedDomainEvent + { + RelationshipId = relationshipId, + Peer = relationshipTo, + NewStatus = newStatus, + Initiator = initiator + }; + + var mockDbContext = A.Fake(); + + var handler = CreateHandler(mockDbContext); + + // Act + await handler.Handle(@event); + + // Assert + A.CallTo(() => mockDbContext.DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, relationshipId, A._)).MustHaveHappenedOnceExactly(); + } + private static RelationshipStatusChangedDomainEventHandler CreateHandler(ISynchronizationDbContext dbContext) { return new RelationshipStatusChangedDomainEventHandler(dbContext, A.Fake>());