Skip to content

Commit

Permalink
Queue external events for new messages if relationship is in status p…
Browse files Browse the repository at this point in the history
…ending (#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
  • Loading branch information
tnotheis authored Jan 28, 2025
1 parent 0421851 commit b34229e
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -25,13 +26,35 @@ 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
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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -17,7 +17,7 @@ public void Throws_if_relationship_is_pending()
var acting = () => relationship.EnsureSendingMessagesIsAllowed(CreateRandomIdentityAddress(), 0, 5);

// Assert
acting.Should().Throw<DomainException>().Which.Code.Should().Be("error.platform.validation.message.relationshipToRecipientNotActive");
acting.Should().NotThrow<Exception>();
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<ExternalEvent>().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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Task<DbPaginationResult<DatawalletModification>> GetDatawalletModifications(Iden
Task<SyncRun?> GetPreviousSyncRunWithExternalEvents(IdentityAddress createdBy, CancellationToken cancellationToken);
Task<List<ExternalEvent>> GetUnsyncedExternalEvents(IdentityAddress owner, byte maxErrorCount, CancellationToken cancellationToken);
Task<List<ExternalEvent>> GetBlockedExternalEventsWithTypeAndContext(ExternalEventType type, string context, CancellationToken cancellationToken);
Task DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType type, string context, CancellationToken cancellationToken);

Task<DbPaginationResult<ExternalEvent>> GetExternalEventsOfSyncRun(PaginationFilter paginationFilter,
IdentityAddress owner, SyncRunId syncRunId, CancellationToken cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ public async Task<List<ExternalEvent>> 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<DbPaginationResult<ExternalEvent>> GetExternalEventsOfSyncRun(PaginationFilter paginationFilter, IdentityAddress owner, SyncRunId syncRunId, CancellationToken cancellationToken)
{
var query = await ExternalEvents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISynchronizationDbContext>();
var fakeRelationshipsRepository = RelationshipsRepositoryReturning([relationshipToRecipient]);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<ISynchronizationDbContext>();

var messageReceivedExternalEvent =
new MessageReceivedExternalEvent(CreateRandomIdentityAddress(), new MessageReceivedExternalEvent.EventPayload { Id = "MSG11111111111111111" }, new RelationshipId(relationshipId));
messageReceivedExternalEvent.BlockDelivery();

A.CallTo(() => mockDbContext.GetBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, A<string>._, A<CancellationToken>._))
.Returns([messageReceivedExternalEvent]);

var handler = CreateHandler(mockDbContext);

// Act
await handler.Handle(relationshipStatusChangedDomainEvent);

// Assert
messageReceivedExternalEvent.IsDeliveryBlocked.Should().BeFalse();
A.CallTo(() => mockDbContext.SaveChangesAsync(A<CancellationToken>._)).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<ISynchronizationDbContext>();

var handler = CreateHandler(mockDbContext);

// Act
await handler.Handle(@event);

// Assert
A.CallTo(() => mockDbContext.DeleteBlockedExternalEventsWithTypeAndContext(ExternalEventType.MessageReceived, relationshipId, A<CancellationToken>._)).MustHaveHappenedOnceExactly();
}

private static RelationshipStatusChangedDomainEventHandler CreateHandler(ISynchronizationDbContext dbContext)
{
return new RelationshipStatusChangedDomainEventHandler(dbContext, A.Fake<ILogger<RelationshipStatusChangedDomainEventHandler>>());
Expand Down

0 comments on commit b34229e

Please sign in to comment.