Skip to content

Commit

Permalink
Create External Events for peers of Identity to be deleted, Identity …
Browse files Browse the repository at this point in the history
…with canceled deletion process and deleted Identity (#699)

* refactor: remove event bus from StartDeletionProcessAsOwnerCommand handler

* feat: add outgoing domain event for `ToBeDeleted` status change

* feat: add domain event for ToBeDeleted status

* test: add domain event test

* test: remove test

* feat: raise domain event when identity deletion stats
refactor: improve code structure of tests

* refactor: remove domain event from actual deletion worker

* feat: raise doman event when deletion process is canceled

* refactor: improve code structure of Devices\Domain Tests

* feat: handle IdentityToBeDeleted domain event, Relationship module

* feat: raise PeerDeletionCanceledDomainEvent

* feat: raise PeerDeletedDomainEvent

* feat: handle PeerToBeDeleted domain event and create external event

* feat: handle PeerDeletionCanceledDomainEvent and create external event

* feat: handle PeerDeletedDomainEvent and create external event

* chore: fix formatting issues

* refactor: various small improvements

* refactor: improve code structure of Identity

* chore: fix message and method name in EntityAssertions

* chore: change type

* chore: remove empty lines

* chroe: rename var

* chore: rename var

* test: improve HaveDomainEvents and its usings

* test: directly test for QueuedForDeletion tier

* refactor: rename Canceled to Cancelled

* refactor: renaming of private methods

* chore: remove IChangeLogExtensions

* test: rename some variables

* refactor: rename canceled to cancelled

* ci: let publish-helm-chart depend on publish-sse-server, publish-identity-deletion-jobs and publish-database-migrator

* test: minor simplification in EntitiesShouldHaveEmptyDefaultConstructors archunit test

* feat: subscribe for identities events in relationships module

* feat: call Update in Relationships event handlers in order to publish the domain events

---------

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 Jun 19, 2024
1 parent d68c7b6 commit ab36f7a
Show file tree
Hide file tree
Showing 52 changed files with 1,055 additions and 116 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ jobs:
- publish-admin-ui
- publish-consumer-api
- publish-event-handler
- publish-database-migrator
- publish-identity-deletion-jobs
- publish-sse-server
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Backbone.Tests.ArchUnit/DomainDrivenDesign.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public void EntitiesShouldHaveEmptyDefaultConstructors()
{
var constructors = type.GetConstructors();

if (constructors.All(c => c.Parameters.Count() != 0))
if (constructors.All(c => c.Parameters.Any()))
return new ConditionResult(type, false, "Entity should have a parameterless constructor");

return new ConditionResult(type, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,35 @@ public TEvent HaveASingleDomainEvent<TEvent>(string because = "", params object[
.BecauseOf(because, becauseArgs)
.Given(() => Subject.DomainEvents)
.ForCondition(events => events.Count == 1)
.FailWith("Expected {context:entity} to have 1 domain event, but found {0}.",
Subject.DomainEvents.Count)
.FailWith("Expected {context:entity} to have 1 domain event, but found {0}.", Subject.DomainEvents.Count)
.Then
.ForCondition(events => events[0].GetType() == typeof(TEvent))
.FailWith("Expected the domain event to be of type {0}, but found {1}.",
typeof(TEvent), Subject.DomainEvents[0].GetType());
.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];
}

public (TEvent1 event1, TEvent2 event2) HaveDomainEvents<TEvent1, TEvent2>(string because = "", params object[] becauseArgs)
where TEvent1 : DomainEvent
where TEvent2 : DomainEvent
{
var joinedEvents = string.Join(", ", Subject.DomainEvents.Select(e => e.GetType().Name));

Execute.Assertion
.BecauseOf(because, becauseArgs)
.Given(() => Subject.DomainEvents)
.ForCondition(events => events.Count == 2)
.FailWith("Expected {context:entity} to have 2 domain events, but found {0}.", Subject.DomainEvents.Count)
.Then
.ForCondition(events => events.Any(e => e.GetType() == typeof(TEvent1)))
.FailWith("Expected to find a domain event of type {0}, but only found {1}.", typeof(TEvent1).Name, joinedEvents)
.Then
.ForCondition(events => events.Any(e => e.GetType() == typeof(TEvent2)))
.FailWith("Expected to find a domain event of type {0}, but only found {1}.", typeof(TEvent2).Name, joinedEvents);

return (
(TEvent1)Subject.DomainEvents.Single(e => e.GetType() == typeof(TEvent1)),
(TEvent2)Subject.DomainEvents.Single(e => e.GetType() == typeof(TEvent2))
);
}
}
19 changes: 1 addition & 18 deletions Jobs/src/Job.IdentityDeletion/Workers/ActualDeletionWorker.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.BuildingBlocks.Domain.Errors;
using Backbone.DevelopmentKit.Identity.ValueObjects;
using Backbone.Modules.Devices.Application.Identities.Commands.TriggerRipeDeletionProcesses;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;
using Backbone.Modules.Relationships.Application.Relationships.Commands.FindRelationshipsOfIdentity;
using CSharpFunctionalExtensions;
using MediatR;

namespace Backbone.Job.IdentityDeletion.Workers;

public class ActualDeletionWorker : IHostedService
{
private readonly IEventBus _eventBus;
private readonly IHostApplicationLifetime _host;
private readonly IEnumerable<IIdentityDeleter> _identityDeleters;
private readonly IMediator _mediator;
Expand All @@ -25,14 +21,12 @@ public ActualDeletionWorker(IHostApplicationLifetime host,
IEnumerable<IIdentityDeleter> identityDeleters,
IMediator mediator,
IPushNotificationSender pushNotificationSender,
IEventBus eventBus,
ILogger<ActualDeletionWorker> logger)
{
_host = host;
_identityDeleters = identityDeleters;
_mediator = mediator;
_pushNotificationSender = pushNotificationSender;
_eventBus = eventBus;
_logger = logger;
}

Expand Down Expand Up @@ -70,7 +64,6 @@ private async Task ExecuteDeletion(IEnumerable<IdentityAddress> addresses, Cance
private async Task ExecuteDeletion(CancellationToken cancellationToken, IdentityAddress identityAddress)
{
await NotifyIdentityAboutStartingDeletion(cancellationToken, identityAddress);
await NotifyRelationshipsAboutStartingDeletion(identityAddress, cancellationToken);
await Delete(identityAddress);
}

Expand All @@ -79,16 +72,6 @@ private async Task NotifyIdentityAboutStartingDeletion(CancellationToken cancell
await _pushNotificationSender.SendNotification(identityAddress, new DeletionStartsPushNotification(), cancellationToken);
}

private async Task NotifyRelationshipsAboutStartingDeletion(IdentityAddress identityAddress, CancellationToken cancellationToken)
{
var relationships = await _mediator.Send(new FindRelationshipsOfIdentityQuery(identityAddress), cancellationToken);

foreach (var relationship in relationships)
{
_eventBus.Publish(new PeerIdentityDeletedDomainEvent(relationship.Id, identityAddress));
}
}

private async Task Delete(IdentityAddress identityAddress)
{
foreach (var identityDeleter in _identityDeleters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ internal static partial class CancelIdentityDeletionProcessWorkerLogs
EventId = 440986,
EventName = "Job.CancelIdentityDeletionProcessWorker.CompletedWithResults",
Level = LogLevel.Information,
Message = "Automatically canceled identity deletion processes: {concatenatedProcessIds}")]
Message = "Automatically cancelled identity deletion processes: {concatenatedProcessIds}")]
public static partial void WorkerCompletedWithResults(this ILogger logger, string concatenatedProcessIds);

[LoggerMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.BuildingBlocks.Application.Identities;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.BuildingBlocks.Domain.Errors;
using Backbone.DevelopmentKit.Identity.ValueObjects;
Expand Down Expand Up @@ -94,19 +93,17 @@ private void SetupRipeDeletionProcessesCommand(IMediator mediator, params Identi

private static ActualDeletionWorker CreateWorker(IMediator mediator, IPushNotificationSender pushNotificationSender)
{
return CreateWorker(mediator, null, null, pushNotificationSender);
return CreateWorker(mediator, null, pushNotificationSender);
}

private static ActualDeletionWorker CreateWorker(IMediator mediator,
List<IIdentityDeleter>? identityDeleters = null,
IEventBus? eventBus = null,
IPushNotificationSender? pushNotificationSender = null)
{
var hostApplicationLifetime = A.Dummy<IHostApplicationLifetime>();
identityDeleters ??= [A.Dummy<IIdentityDeleter>()];
eventBus ??= A.Dummy<IEventBus>();
pushNotificationSender ??= A.Dummy<IPushNotificationSender>();
var logger = A.Dummy<ILogger<ActualDeletionWorker>>();
return new ActualDeletionWorker(hostApplicationLifetime, identityDeleters, mediator, pushNotificationSender, eventBus, logger);
return new ActualDeletionWorker(hostApplicationLifetime, identityDeleters, mediator, pushNotificationSender, logger);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Backbone.BuildingBlocks.Domain.Events;

namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;
public class IdentityDeletedDomainEvent : DomainEvent
{
public IdentityDeletedDomainEvent(string identityAddress) : base($"{identityAddress}/IdentityDeleted")
{
IdentityAddress = identityAddress;
}

public string IdentityAddress { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Backbone.BuildingBlocks.Domain.Events;

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

public class IdentityDeletionCancelledDomainEvent : DomainEvent
{
public IdentityDeletionCancelledDomainEvent(string identityAddress) : base($"{identityAddress}/IdentityDeletionCancelled", randomizeId: true)
{
IdentityAddress = identityAddress;
}

public string IdentityAddress { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Backbone.BuildingBlocks.Domain.Events;

namespace Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;
public class IdentityToBeDeletedDomainEvent : DomainEvent
{
public IdentityToBeDeletedDomainEvent(string identityAddress) : base($"{identityAddress}/IdentityToBeDeleted", randomizeId: true)
{
IdentityAddress = identityAddress;
}

public string IdentityAddress { get; }
}

This file was deleted.

53 changes: 17 additions & 36 deletions Modules/Devices/src/Devices.Domain/Entities/Identities/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public IdentityDeletionProcess StartDeletionProcessAsOwner(DeviceId asDevice)
DeletionGracePeriodEndsAt = deletionProcess.GracePeriodEndsAt;
TierId = Tier.QUEUED_FOR_DELETION.Id;
Status = IdentityStatus.ToBeDeleted;
RaiseDomainEvent(new IdentityToBeDeletedDomainEvent(Address));

return deletionProcess;
}
Expand Down Expand Up @@ -167,6 +168,7 @@ public IdentityDeletionProcess ApproveDeletionProcess(IdentityDeletionProcessId
deletionProcess.Approve(Address, deviceId);

Status = IdentityStatus.ToBeDeleted;
RaiseDomainEvent(new IdentityToBeDeletedDomainEvent(Address));
DeletionGracePeriodEndsAt = deletionProcess.GracePeriodEndsAt;
TierId = Tier.QUEUED_FOR_DELETION.Id;

Expand All @@ -180,22 +182,7 @@ public void DeletionStarted()

deletionProcess.DeletionStarted(Address);
Status = IdentityStatus.Deleting;
}

public IdentityDeletionProcess CancelDeletionProcess(IdentityDeletionProcessId deletionProcessId, DeviceId canceledByDeviceId)
{
EnsureIdentityOwnsDevice(canceledByDeviceId);

var deletionProcess = DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ??
throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));

deletionProcess.CancelAsOwner(Address, canceledByDeviceId);

TierId = TierIdBeforeDeletion ?? throw new Exception($"Error when trying to cancel deletion process: '{nameof(TierIdBeforeDeletion)}' is null.");
TierIdBeforeDeletion = null;
Status = IdentityStatus.Active;

return deletionProcess;
RaiseDomainEvent(new IdentityDeletedDomainEvent(Address));
}

private IdentityDeletionProcess GetDeletionProcess(IdentityDeletionProcessId deletionProcessId)
Expand Down Expand Up @@ -275,47 +262,41 @@ public static Expression<Func<Identity, bool>> IsReadyForDeletion()
return i => i.Status == IdentityStatus.ToBeDeleted && i.DeletionGracePeriodEndsAt != null && i.DeletionGracePeriodEndsAt < SystemTime.UtcNow;
}

public IdentityDeletionProcess CancelDeletionProcessAsOwner(IdentityDeletionProcessId deletionProcessId, DeviceId canceledByDeviceId)
public IdentityDeletionProcess CancelDeletionProcessAsOwner(IdentityDeletionProcessId deletionProcessId, DeviceId cancelledByDeviceId)
{
EnsureIdentityOwnsDevice(cancelledByDeviceId);

var deletionProcess = GetDeletionProcessWithId(deletionProcessId);
deletionProcess.EnsureStatus(DeletionProcessStatus.Approved);

EnsureIdentityOwnsDevice(canceledByDeviceId);

deletionProcess.CancelAsOwner(Address, canceledByDeviceId);
TierId = TierIdBeforeDeletion!;
deletionProcess.CancelAsOwner(Address, cancelledByDeviceId);
TierId = TierIdBeforeDeletion ?? throw new Exception($"Error when trying to cancel deletion process: '{nameof(TierIdBeforeDeletion)}' is null.");
TierIdBeforeDeletion = null;
Status = IdentityStatus.Active;

return deletionProcess;
}
RaiseDomainEvent(new IdentityDeletionCancelledDomainEvent(Address));

private IdentityDeletionProcess GetDeletionProcessWithId(IdentityDeletionProcessId deletionProcessId)
{
return DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ?? throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
return deletionProcess;
}

public IdentityDeletionProcess CancelDeletionProcessAsSupport(IdentityDeletionProcessId deletionProcessId)
{
EnsureDeletionProcessExists(deletionProcessId);
EnsureDeletionProcessInStatusExists(DeletionProcessStatus.Approved);

var deletionProcess = DeletionProcesses.First(d => d.Id == deletionProcessId);
var deletionProcess = GetDeletionProcessWithId(deletionProcessId);
deletionProcess.EnsureStatus(DeletionProcessStatus.Approved);

deletionProcess.CancelAsSupport(Address);
TierId = TierIdBeforeDeletion!;
TierId = TierIdBeforeDeletion ?? throw new Exception($"Error when trying to cancel deletion process: '{nameof(TierIdBeforeDeletion)}' is null.");
TierIdBeforeDeletion = null;
Status = IdentityStatus.Active;

RaiseDomainEvent(new IdentityDeletionCancelledDomainEvent(Address));

return deletionProcess;
}

private void EnsureDeletionProcessExists(IdentityDeletionProcessId deletionProcessId)
private IdentityDeletionProcess GetDeletionProcessWithId(IdentityDeletionProcessId deletionProcessId)
{
var isDeletionProcessOwnedByDevice = DeletionProcesses.Any(d => d.Id == deletionProcessId);

if (!isDeletionProcessOwnedByDevice)
throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
return DeletionProcesses.FirstOrDefault(x => x.Id == deletionProcessId) ?? throw new DomainException(GenericDomainErrors.NotFound(nameof(IdentityDeletionProcess)));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using Backbone.BuildingBlocks.Application.Abstractions.Exceptions;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.EventBus;
using Backbone.BuildingBlocks.Application.Abstractions.Infrastructure.UserContext;
using Backbone.BuildingBlocks.Application.PushNotifications;
using Backbone.Modules.Devices.Application.Identities.Commands.CancelDeletionProcessAsOwner;
using Backbone.Modules.Devices.Application.Infrastructure.Persistence.Repository;
using Backbone.Modules.Devices.Application.Infrastructure.PushNotifications.DeletionProcess;
using Backbone.Modules.Devices.Domain.DomainEvents.Outgoing;
using Backbone.Modules.Devices.Domain.Entities.Identities;
using Backbone.UnitTestTools.BaseClasses;
using FakeItEasy;
Expand Down Expand Up @@ -35,10 +33,9 @@ public async Task Happy_path()
A.CallTo(() => fakeUserContext.GetDeviceId()).Returns(activeDevice.Id);

var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext, mockPushNotificationSender);
var command = new CancelDeletionProcessAsOwnerCommand(deletionProcess.Id);

// Act
var response = await handler.Handle(command, CancellationToken.None);
var response = await handler.Handle(new CancelDeletionProcessAsOwnerCommand(deletionProcess.Id), CancellationToken.None);

// Assert
A.CallTo(() => mockIdentitiesRepository.Update(A<Identity>.That.Matches(i =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ public async Task Happy_path()
var handler = CreateHandler(mockIdentitiesRepository, fakeUserContext, mockPushNotificationSender);

// Act
var command = new StartDeletionProcessAsOwnerCommand();
var response = await handler.Handle(command, CancellationToken.None);
var response = await handler.Handle(new StartDeletionProcessAsOwnerCommand(), CancellationToken.None);

// Assert
response.Should().NotBeNull();
Expand Down Expand Up @@ -74,15 +73,15 @@ public void Cannot_start_when_given_identity_does_not_exist()
var handler = CreateHandler(fakeIdentitiesRepository, fakeUserContext);

// Act
var command = new StartDeletionProcessAsOwnerCommand();
var acting = async () => await handler.Handle(command, CancellationToken.None);
var acting = async () => await handler.Handle(new StartDeletionProcessAsOwnerCommand(), CancellationToken.None);

// Assert
acting.Should().AwaitThrowAsync<NotFoundException, StartDeletionProcessAsOwnerResponse>().Which.Message.Should().Contain("Identity");
}

private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IUserContext userContext, IPushNotificationSender? pushNotificationSender = null)
{
return new Handler(identitiesRepository, userContext, pushNotificationSender ?? A.Dummy<IPushNotificationSender>());
pushNotificationSender ??= A.Dummy<IPushNotificationSender>();
return new Handler(identitiesRepository, userContext, pushNotificationSender);
}
}
Loading

0 comments on commit ab36f7a

Please sign in to comment.