Skip to content

Commit

Permalink
Deletion of messages after sender and all recipients have been anonym…
Browse files Browse the repository at this point in the history
…ized (#861)

* feat: find orphaned messages

* chore: remove empty EventHandlerService folder from the root folder

* feat: add delete method MessagesRepository

* feat: add IsOrphaned check

* fix: IsAnonymized method

* chore: revert changes because they break single responsibility rule

* feat: add CheckForOrphanedMessages method

* chore: remove unused using

* refactor: IsOrphaned method to void

* feat: add MessageOrphanedDomainEvent

* chore: fix tests

* chore: remove Domain Events

* chore: revert code in ActualDeletionWorker

* fix: remove Domain Event subscription

* feat: add delete method to messages repository

* refactor: make IsOrphaned bool

* chore: revert tests

* feat: add Delete Orhpaned Messages

* feat: add IsMessageOrphaned expression

* chore: remove unused usings

* chore: remove unused methods

* chore: remove blank line

* refactor: remove unused parametar from command

* test: improve test

* test: add expression tests

* chore: revert changes in csproj file

* chore: move method to the bottom

* chore: rename tests

* fix: use logical && operator instead of bitwise &

* refactor: IsMessageOrphaned method

* feat: add MessageOrphaned external event type

* chore: delete orphaned messages command

* test: fix tests

* feat: implement MessageOrphanedDomainEvent

* chore: fix formatting

* test: improve tests

* chore: remove the creation of external event for orphaned messages

* chore: remove expression tests

* wip

* fix: domain event

* test: revert check for raised domain events

* chore: fix formatting issue

* test: improve tests

* chore: remove leftover unused code & formatting

* refactor: rename methods to match convention

* refactor: delete message by Id to avoid unnecessary database roundtrip

* chore: delete unused property from MessageOrphanedDomainEvent

* fix: make MessageOrphanedDomainEvent deserializable

* fix: seal ToString method in StronglyTypedId

* refactor: rename some variables and methods

* refactor: make IsOrphaned method private

* refactor: update test name to match method name

* test: add NotHaveADomainEvent extension method to EntityAssertions

* test: simplify tests

* refactor: move private method below public method

* test: fix nullability problems

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Timo Notheisen <[email protected]>
  • Loading branch information
3 people authored Sep 23, 2024
1 parent 9896fed commit 4e883dd
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 119 deletions.
8 changes: 0 additions & 8 deletions Backbone.sln
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Files.Infrastructure.Tests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Messages.Domain.Tests", "Modules\Messages\test\Messages.Domain.Tests\Messages.Domain.Tests.csproj", "{83A86879-670B-4F22-8835-EE1D0AB49AA9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EventHandlerService", "EventHandlerService", "{11FE034C-FA73-4766-99DB-D2C606018934}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D54A9259-7708-45C1-B8D9-448B97F43B80}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2429FCAB-2058-4403-94DA-DA26B1655A7D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceSnapshotCreator", "Applications\ConsumerApi\test\ConsumerApi.Tests.Performance\PerformanceSnapshotCreator\PerformanceSnapshotCreator.csproj", "{B74B8655-8A94-4520-8BAB-212D7123EDB4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tokens.Domain.Tests", "Modules\Tokens\test\Tokens.Domain.Tests\Tokens.Domain.Tests.csproj", "{EDCB84BE-54C3-4CAD-977E-45EEBEFA1402}"
Expand Down Expand Up @@ -919,8 +913,6 @@ Global
{6CCACC81-F9A8-4953-AE1D-62AB01CC7CBA} = {06D714AE-EDF4-421C-9340-EDA6FCDF491F}
{EE7F27E9-9DD9-41FC-923D-05D595829A03} = {2D0BC8E9-ED6B-49D9-937C-1616ED40FB3E}
{83A86879-670B-4F22-8835-EE1D0AB49AA9} = {BBE908B0-D642-4002-8A88-9F1726BA8CB6}
{D54A9259-7708-45C1-B8D9-448B97F43B80} = {11FE034C-FA73-4766-99DB-D2C606018934}
{2429FCAB-2058-4403-94DA-DA26B1655A7D} = {11FE034C-FA73-4766-99DB-D2C606018934}
{B74B8655-8A94-4520-8BAB-212D7123EDB4} = {248E5883-AFF5-450B-9398-4D0E874A1660}
{EDCB84BE-54C3-4CAD-977E-45EEBEFA1402} = {1E437DEA-7657-48AD-ADA0-7B86608E0768}
{EAA10D43-BD28-40D8-BE07-B8F6DE47C156} = {EFC1F89E-1C44-4385-A0F6-1F2124260561}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ public abstract record StronglyTypedId(string Value)
'9'
];

public sealed override string ToString()
{
return Value;
}

public static implicit operator string(StronglyTypedId id)
{
return id.Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,6 @@ private static string Hex(byte[] bytes)
return Convert.ToHexString(bytes).ToLower();
}

public override string ToString()
{
return Value;
}

#region Converters

public class IdentityAddressTypeConverter : TypeConverter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,28 @@
using FluentAssertions.Execution;
using FluentAssertions.Primitives;

// ReSharper disable once CheckNamespace
#pragma warning disable IDE0130
namespace FluentAssertions;
#pragma warning restore IDE0130
namespace Backbone.UnitTestTools.FluentAssertions.Assertions;

public class EntityAssertions : ObjectAssertions<Entity, EntityAssertions>
public class EntityAssertions : ObjectAssertions<Entity?, EntityAssertions>
{
public EntityAssertions(Entity instance) : base(instance)
public EntityAssertions(Entity? instance) : base(instance)
{
}

protected override string Identifier => "entity";

public TEvent HaveASingleDomainEvent<TEvent>(string because = "", params object[] becauseArgs) where TEvent : DomainEvent
{
if (Subject == null)
throw new Exception("Subject cannot be null.");

Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => Subject.DomainEvents)
.ForCondition(events => events.Count == 1)
.ForCondition(e => e.Count == 1)
.FailWith("Expected {context:entity} to have 1 domain event, but found {0}.", Subject.DomainEvents.Count)
.Then
.ForCondition(events => events[0].GetType() == typeof(TEvent))
.ForCondition(e => e[0].GetType() == typeof(TEvent))
.FailWith("Expected the domain event to be of type {0}, but found {1}.", typeof(TEvent).Name, Subject.DomainEvents[0].GetType().Name);

return (TEvent)Subject.DomainEvents[0];
Expand All @@ -34,6 +34,9 @@ public TEvent HaveASingleDomainEvent<TEvent>(string because = "", params object[
where TEvent1 : DomainEvent
where TEvent2 : DomainEvent
{
if (Subject == null)
throw new Exception("Subject cannot be null.");

var joinedEvents = string.Join(", ", Subject.DomainEvents.Select(e => e.GetType().Name));

Execute.Assertion
Expand All @@ -53,4 +56,16 @@ public TEvent HaveASingleDomainEvent<TEvent>(string because = "", params object[
(TEvent2)Subject.DomainEvents.Single(e => e.GetType() == typeof(TEvent2))
);
}

public void NotHaveADomainEvent<TEvent>(string because = "", params object[] becauseArgs) where TEvent : DomainEvent
{
if (Subject == null)
throw new Exception("Subject cannot be null.");

Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => Subject.DomainEvents)
.ForCondition(events => events.All(e => e is not TEvent))
.FailWith($"Expected {{context:entity}} to not have a {typeof(TEvent).Name}, but it has.", Subject.DomainEvents);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Backbone.BuildingBlocks.Domain;
using FluentAssertions;
using Backbone.UnitTestTools.FluentAssertions.Assertions;

namespace Backbone.UnitTestTools.FluentAssertions.Extensions;
// ReSharper disable once CheckNamespace
#pragma warning disable IDE0130
namespace FluentAssertions;
#pragma warning restore IDE0130

public static class EntityExtensions
{
public static EntityAssertions Should(this Entity instance) => new(instance);
public static EntityAssertions Should(this Entity? instance) => new(instance);
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ private IdentityDeletionProcess(IdentityAddress createdBy, DeviceId createdByDev

public bool HasGracePeriodExpired => Status == DeletionProcessStatus.Approved && SystemTime.UtcNow >= GracePeriodEndsAt;

private void ApproveInternally(IdentityAddress address, DeviceId createdByDevice)
{
ApprovedAt = SystemTime.UtcNow;
ApprovedByDevice = createdByDevice;
GracePeriodEndsAt = SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.LengthOfGracePeriod);
ChangeStatus(DeletionProcessStatus.Approved, address, address);
}

public static IdentityDeletionProcess StartAsSupport(IdentityAddress createdBy)
{
return new IdentityDeletionProcess(createdBy, DeletionProcessStatus.WaitingForApproval);
Expand Down Expand Up @@ -157,6 +149,14 @@ public void Approve(IdentityAddress address, DeviceId approvedByDevice)
_auditLog.Add(IdentityDeletionProcessAuditLogEntry.ProcessApproved(Id, address, approvedByDevice));
}

private void ApproveInternally(IdentityAddress address, DeviceId createdByDevice)
{
ApprovedAt = SystemTime.UtcNow;
ApprovedByDevice = createdByDevice;
GracePeriodEndsAt = SystemTime.UtcNow.AddDays(IdentityDeletionConfiguration.LengthOfGracePeriod);
ChangeStatus(DeletionProcessStatus.Approved, address, address);
}

public void Reject(IdentityAddress address, DeviceId rejectedByDevice)
{
EnsureStatus(DeletionProcessStatus.WaitingForApproval);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Messages.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Messages.Domain.DomainEvents.Outgoing;
using MessageId = Backbone.Modules.Messages.Domain.Ids.MessageId;

namespace Backbone.Modules.Messages.Application.DomainEvents.Incoming.MessageOrphaned;

public class MessageOrphanedDomainEventHandler : IDomainEventHandler<MessageOrphanedDomainEvent>
{
private readonly IMessagesRepository _messagesRepository;

public MessageOrphanedDomainEventHandler(IMessagesRepository messagesRepository)
{
_messagesRepository = messagesRepository;
}

public async Task Handle(MessageOrphanedDomainEvent @event)
{
var messageId = MessageId.Parse(@event.MessageId);
await _messagesRepository.Delete(messageId, CancellationToken.None);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.Modules.Messages.Application.DomainEvents.Incoming.MessageOrphaned;
using Backbone.Modules.Messages.Application.DomainEvents.Incoming.RelationshipStatusChanged;
using Backbone.Modules.Messages.Domain.DomainEvents.Incoming;
using Backbone.Modules.Messages.Domain.DomainEvents.Outgoing;

namespace Backbone.Modules.Messages.Application.Extensions;

public static class IEventBusExtensions
{
public static IEventBus AddMessagesDomainEventSubscriptions(this IEventBus eventBus)
public static void AddMessagesDomainEventSubscriptions(this IEventBus eventBus)
{
SubscribeToMessagesEvents(eventBus);
SubscribeToRelationshipsEvents(eventBus);
}

private static void SubscribeToMessagesEvents(IEventBus eventBus)
{
eventBus.Subscribe<MessageOrphanedDomainEvent, MessageOrphanedDomainEventHandler>();
}

private static void SubscribeToRelationshipsEvents(IEventBus eventBus)
{
eventBus.Subscribe<RelationshipStatusChangedDomainEvent, RelationshipStatusChangedDomainEventHandler>();
return eventBus;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Task<DbPaginationResult<Message>> FindMessagesWithIds(IEnumerable<MessageId> ids
Task Update(Message message);
Task Update(IEnumerable<Message> messages);
Task<IEnumerable<Message>> Find(Expression<Func<Message, bool>> expression, CancellationToken cancellationToken);
Task Delete(MessageId messageId, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task Handle(AnonymizeMessagesOfIdentityCommand request, Cancellatio

foreach (var message in messages)
{
message.ReplaceIdentityAddress(request.IdentityAddress, newIdentityAddress);
message.AnonymizeParticipant(request.IdentityAddress, newIdentityAddress);
}

await _messagesRepository.Update(messages);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Backbone.BuildingBlocks.Domain.Events;
using Backbone.Modules.Messages.Domain.Entities;

namespace Backbone.Modules.Messages.Domain.DomainEvents.Outgoing;

public class MessageOrphanedDomainEvent : DomainEvent
{
/**
* This constructor is used by the deserializer.
*/
public MessageOrphanedDomainEvent()
{
MessageId = null!;
}

public MessageOrphanedDomainEvent(Message message) : base($"{message.Id}/MessageOrphaned")
{
MessageId = message.Id;
}

public string MessageId { get; set; }
}
17 changes: 13 additions & 4 deletions Modules/Messages/src/Messages.Domain/Entities/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,15 @@ public Message(IdentityAddress createdBy, DeviceId createdByDevice, byte[] body,

private bool CanAnonymizeSender => Recipients.All(r => r.IsRelationshipFullyDecomposed);

public void ReplaceIdentityAddress(IdentityAddress oldIdentityAddress, IdentityAddress newIdentityAddress)
public void AnonymizeParticipant(IdentityAddress participantAddress, IdentityAddress anonymizedAddress)
{
if (CreatedBy == oldIdentityAddress)
AnonymizeSender(newIdentityAddress);
if (CreatedBy == participantAddress)
AnonymizeSender(anonymizedAddress);

AnonymizeRecipient(participantAddress, anonymizedAddress);

AnonymizeRecipient(oldIdentityAddress, newIdentityAddress);
if (IsOrphaned(this, anonymizedAddress))
RaiseDomainEvent(new MessageOrphanedDomainEvent(this));
}

public void DecomposeFor(IdentityAddress decomposerAddress, IdentityAddress peerAddress, IdentityAddress anonymizedAddress)
Expand Down Expand Up @@ -93,6 +96,12 @@ private void AnonymizeSender(IdentityAddress anonymizedIdentityAddress)
CreatedBy = anonymizedIdentityAddress;
}

private bool IsOrphaned(Message message, IdentityAddress anonymizedIdentityAddress)
{
return message.CreatedBy == anonymizedIdentityAddress &&
message.Recipients.All(r => r.Address == anonymizedIdentityAddress);
}

#region Expressions

public static Expression<Func<Message, bool>> HasParticipant(IdentityAddress identityAddress)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public async Task<IEnumerable<Message>> Find(Expression<Func<Message, bool>> exp
.Where(expression)
.ToListAsync(cancellationToken);
}

public async Task Delete(MessageId messageId, CancellationToken cancellationToken)
{
await _messages.Where(m => m.Id == messageId).ExecuteDeleteAsync(cancellationToken);
}
}
Loading

0 comments on commit 4e883dd

Please sign in to comment.