Skip to content

Commit

Permalink
Team Management Domain + Delete User + Activate User (#4)
Browse files Browse the repository at this point in the history
* added domain for team management module (domain, contracts, migrations ...)
* added delete user endpoint, command, command handler, domain logic and endpoint tests
* added activate user endpoint, command, command handler, domain logic and endpoint tests
* added missing endpoint tests (login, get account details)
* added missing data generators, extension methods and private binder
* fixed bug - reset callback counters between tests
* additional fixes (dependency tests ...)
* refactoring (naming, structure ...)
  • Loading branch information
skrasekmichael committed Apr 22, 2024
1 parent 121c9c7 commit b5ac482
Show file tree
Hide file tree
Showing 100 changed files with 3,571 additions and 68 deletions.
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="TeamUp.Tests.EndToEnd" />
<InternalsVisibleTo Include="TeamUp.Tests.Common"/>
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<PackageVersion Include="MediatR.Contracts" Version="2.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
Expand Down
36 changes: 18 additions & 18 deletions TeamUp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamUp.TeamManagement.Infra
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notifications", "Notifications", "{86FE1981-2361-4417-89F3-90CE0DC21692}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamUp.Notifications.Application", "src\Modules\Notifications\TeamUp.Notifications.Application\TeamUp.Notifications.Application.csproj", "{256074E6-3736-41D1-8F44-6EB9EC25E151}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamUp.Notifications.Application", "src\Modules\Notifications\TeamUp.Notifications.Application\TeamUp.Notifications.Application.csproj", "{256074E6-3736-41D1-8F44-6EB9EC25E151}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamUp.Notifications.Contracts", "src\Modules\Notifications\TeamUp.Notifications.Contracts\TeamUp.Notifications.Contracts.csproj", "{25BE033F-3973-4BC8-8C1D-246C3C4AD3C6}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamUp.Notifications.Contracts", "src\Modules\Notifications\TeamUp.Notifications.Contracts\TeamUp.Notifications.Contracts.csproj", "{25BE033F-3973-4BC8-8C1D-246C3C4AD3C6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamUp.Notifications.Infrastructure", "src\Modules\Notifications\TeamUp.Notifications.Infrastructure\TeamUp.Notifications.Infrastructure.csproj", "{4A940BAC-8B19-4167-95C2-944394502CA1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TeamUp.Notifications.Infrastructure", "src\Modules\Notifications\TeamUp.Notifications.Infrastructure\TeamUp.Notifications.Infrastructure.csproj", "{4A940BAC-8B19-4167-95C2-944394502CA1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -82,6 +82,18 @@ Global
{832DB0B9-B7DB-458C-864A-E67D91DCD49F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{832DB0B9-B7DB-458C-864A-E67D91DCD49F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{832DB0B9-B7DB-458C-864A-E67D91DCD49F}.Release|Any CPU.Build.0 = Release|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Release|Any CPU.Build.0 = Release|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Release|Any CPU.Build.0 = Release|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Release|Any CPU.Build.0 = Release|Any CPU
{512F9E61-FB73-4190-B2FA-5D2F32B49439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{512F9E61-FB73-4190-B2FA-5D2F32B49439}.Debug|Any CPU.Build.0 = Debug|Any CPU
{512F9E61-FB73-4190-B2FA-5D2F32B49439}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -146,18 +158,6 @@ Global
{5A322584-C568-46BB-8D3E-2D80C0236249}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A322584-C568-46BB-8D3E-2D80C0236249}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A322584-C568-46BB-8D3E-2D80C0236249}.Release|Any CPU.Build.0 = Release|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C58155F-2EB2-41AE-BF00-98EC13772624}.Release|Any CPU.Build.0 = Release|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3E971F8-563C-43D1-B822-8164B22D8C11}.Release|Any CPU.Build.0 = Release|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DC6465B-FDC2-4637-99E3-31B77725B016}.Release|Any CPU.Build.0 = Release|Any CPU
{256074E6-3736-41D1-8F44-6EB9EC25E151}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{256074E6-3736-41D1-8F44-6EB9EC25E151}.Debug|Any CPU.Build.0 = Debug|Any CPU
{256074E6-3736-41D1-8F44-6EB9EC25E151}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -176,6 +176,9 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{832DB0B9-B7DB-458C-864A-E67D91DCD49F} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{5C58155F-2EB2-41AE-BF00-98EC13772624} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{A3E971F8-563C-43D1-B822-8164B22D8C11} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{6DC6465B-FDC2-4637-99E3-31B77725B016} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{BFA5143A-1983-436E-A220-461495EBA817} = {F18695E6-478D-4823-B07E-A16FAE200201}
{512F9E61-FB73-4190-B2FA-5D2F32B49439} = {BFA5143A-1983-436E-A220-461495EBA817}
{09E2F563-9F92-4CC3-858D-DC7C20F43624} = {BFA5143A-1983-436E-A220-461495EBA817}
Expand All @@ -196,9 +199,6 @@ Global
{9F969E92-96C1-4606-B7CD-8853EAF3BB30} = {9692F720-59EE-4CAA-97C4-BA4F79D34FBB}
{E68CC821-BA52-4BB1-88E6-38E043DB22D8} = {9692F720-59EE-4CAA-97C4-BA4F79D34FBB}
{5A322584-C568-46BB-8D3E-2D80C0236249} = {9692F720-59EE-4CAA-97C4-BA4F79D34FBB}
{5C58155F-2EB2-41AE-BF00-98EC13772624} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{A3E971F8-563C-43D1-B822-8164B22D8C11} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{6DC6465B-FDC2-4637-99E3-31B77725B016} = {E18384AC-84A4-47B8-B6ED-F9BB5CD37926}
{86FE1981-2361-4417-89F3-90CE0DC21692} = {6B1672A0-DAF3-4002-93E7-DB4C995C5477}
{256074E6-3736-41D1-8F44-6EB9EC25E151} = {86FE1981-2361-4417-89F3-90CE0DC21692}
{25BE033F-3973-4BC8-8C1D-246C3C4AD3C6} = {86FE1981-2361-4417-89F3-90CE0DC21692}
Expand Down
2 changes: 1 addition & 1 deletion src/Common/TeamUp.Common.Domain/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected Entity(TId id)
Id = id;
}

protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
public void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);

public void ClearDomainEvents() => _domainEvents.Clear();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
using RailwayResult;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Domain.Aggregates;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Domain.Aggregates.Users;
using TeamUp.UserAccess.Contracts.CreateUser;

namespace TeamUp.TeamManagement.Application.Users;

internal sealed class UserCreatedEventHandler : IIntegrationEventHandler<UserCreatedIntegrationEvent>
{
private readonly IUserRepository _userRepository;
private readonly IUnitOfWork<Contracts.TeamManagementModuleId> _unitOfWork;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public UserCreatedEventHandler(IUserRepository userRepository, IUnitOfWork<Contracts.TeamManagementModuleId> unitOfWork)
public UserCreatedEventHandler(IUserRepository userRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_userRepository = userRepository;
_unitOfWork = unitOfWork;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;
using TeamUp.TeamManagement.Domain.Aggregates.Teams.DomainEvents;
using TeamUp.TeamManagement.Domain.Aggregates.Users;
using TeamUp.UserAccess.Contracts.DeleteAccount;

namespace TeamUp.TeamManagement.Application.Users;

internal class UserDeletedEventHandler : IIntegrationEventHandler<UserDeletedIntegrationEvent>
{
private readonly IUserRepository _userRepository;
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public UserDeletedEventHandler(IUserRepository userRepository, ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_userRepository = userRepository;
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(UserDeletedIntegrationEvent integrationEvent, CancellationToken ct)
{
var teams = await _teamRepository.GetTeamsByUserIdAsync(integrationEvent.UserId, ct);

foreach (var team in teams)
{
team.GetTeamMemberByUserId(integrationEvent.UserId)
.Ensure(TeamRules.MemberCanChangeOwnership)
.Tap(initiator =>
{
if (team.Members.Count == 1)
{
//remove team if user that is being removed is the only member
_teamRepository.RemoveTeam(team);
}
else
{
//change ownership when removing user that is owner of the team
var newOwner = team.GetHighestNonOwnerTeamMember()!;
initiator.UpdateRole(TeamRole.Admin);
newOwner.UpdateRole(TeamRole.Owner);
team.AddDomainEvent(new TeamOwnershipChangedDomainEvent(initiator, newOwner));
}
});

//db will cascade delete member, but number of members needs to be updated manually
team.DecreaseNumberOfMembers();
}

//delete user
var user = await _userRepository.GetUserByIdAsync(integrationEvent.UserId, ct);
if (user is not null)
{
_userRepository.RemoveUser(user);
}

return await _unitOfWork.SaveChangesAsync(ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TeamUp.TeamManagement.Contracts.Events;

public static class EventConstants
{
public const int EVENT_DESCRIPTION_MAX_SIZE = 30;

public const int EVENT_REPLY_MESSAGE_MAX_SIZE = 80;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Events;

public sealed record EventId : TypedId<EventId>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Events;

public sealed record EventResponseId : TypedId<EventResponseId>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TeamUp.TeamManagement.Contracts.Events;

public enum EventStatus
{
Open = 0,
Closed = 1,
Canceled = 2,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TeamUp.TeamManagement.Contracts.Events;

public enum ReplyType
{
No = 0,
Maybe = 1,
Delay = 2,
Yes = 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Invitations;

public sealed record InvitationId : TypedId<InvitationId>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Teams;

public sealed record EventTypeId : TypedId<EventTypeId>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace TeamUp.TeamManagement.Contracts;
namespace TeamUp.TeamManagement.Contracts.Teams;

public static class Constants
public static class TeamConstants
{
public const int TEAM_NAME_MIN_SIZE = 3;
public const int TEAM_NAME_MAX_SIZE = 30;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Teams;

public sealed record TeamId : TypedId<TeamId>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Teams;

public sealed record TeamMemberId : TypedId<TeamMemberId>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace TeamUp.TeamManagement.Contracts.Teams;

public enum TeamRole
{
Member = 0,
Coordinator = 1,
Admin = 2,
Owner = 3
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Domain;

namespace TeamUp.TeamManagement.Domain.Aggregates.Events.DomainEvents;

public sealed record EventResponseCreatedDomainEvent(EventResponse Response) : IDomainEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Domain;

namespace TeamUp.TeamManagement.Domain.Aggregates.Events.DomainEvents;

public sealed record EventResponseUpdatedDomainEvent(EventResponse Response) : IDomainEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using TeamUp.Common.Domain;

namespace TeamUp.TeamManagement.Domain.Aggregates.Events.DomainEvents;

public sealed record EventStatusChangedDomainEvent(Event Event) : IDomainEvent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using RailwayResult;
using RailwayResult.Errors;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Contracts;
using TeamUp.Common.Domain;
using TeamUp.TeamManagement.Contracts.Events;
using TeamUp.TeamManagement.Contracts.Teams;
using TeamUp.TeamManagement.Domain.Aggregates.Events.DomainEvents;

namespace TeamUp.TeamManagement.Domain.Aggregates.Events;

public sealed class Event : AggregateRoot<Event, EventId>
{
private readonly List<EventResponse> _eventResponses = [];

public EventTypeId EventTypeId { get; private set; }
public TeamId TeamId { get; }

public DateTime FromUtc { get; private set; }
public DateTime ToUtc { get; private set; }
public string Description { get; private set; }
public EventStatus Status { get; private set; }
public TimeSpan MeetTime { get; private set; }
public TimeSpan ReplyClosingTimeBeforeMeetTime { get; private set; }
public IReadOnlyList<EventResponse> EventResponses => _eventResponses.AsReadOnly();

#pragma warning disable CS8618 // EF Core constructor
private Event() : base() { }
#pragma warning restore CS8618

internal Event(
EventId id,
EventTypeId eventTypeId,
TeamId teamId,
DateTime fromUtc,
DateTime toUtc,
string description,
EventStatus status,
TimeSpan meetTime,
TimeSpan replyClosingTimeBeforeMeetTime) : base(id)
{
EventTypeId = eventTypeId;
TeamId = teamId;
FromUtc = fromUtc;
ToUtc = toUtc;
Description = description;
Status = status;
MeetTime = meetTime;
ReplyClosingTimeBeforeMeetTime = replyClosingTimeBeforeMeetTime;
}

public Result SetMemberResponse(IDateTimeProvider dateTimeProvider, TeamMemberId memberId, EventReply reply)
{
static bool TimeForResponsesHasNotExpired(IDateTimeProvider dateTimeProvider, DateTime responseCloseTime) => dateTimeProvider.UtcNow < responseCloseTime;

return Status
.Ensure(status => status.IsOpenForResponses(), EventErrors.NotOpenForResponses)
.Then(_ => GetResponseCloseTime())
.Ensure(responseCloseTime => TimeForResponsesHasNotExpired(dateTimeProvider, responseCloseTime), EventErrors.TimeForResponsesExpired)
.Then(_ => _eventResponses.Find(er => er.TeamMemberId == memberId))
.Tap(response =>
{
if (response is null)
_eventResponses.Add(EventResponse.Create(dateTimeProvider, memberId, Id, reply));
else
response.UpdateReply(dateTimeProvider, reply);
})
.ToResult();
}

public void UpdateStatus(EventStatus status)
{
if (Status == status)
return;

Status = status;
AddDomainEvent(new EventStatusChangedDomainEvent(this));
}

private DateTime GetResponseCloseTime() => FromUtc - MeetTime - ReplyClosingTimeBeforeMeetTime;

public static Result<Event> Create(
EventTypeId eventTypeId,
TeamId teamId,
DateTime fromUtc,
DateTime toUtc,
string description,
TimeSpan meetTime,
TimeSpan replyClosingTimeBeforeMeetTime,
IDateTimeProvider dateTimeProvider)
{
return fromUtc
.Ensure(from => from < toUtc, EventErrors.CannotEndBeforeStart)
.Ensure(from => from > dateTimeProvider.DateTimeOffsetUtcNow, EventErrors.CannotStartInPast)
.Ensure<DateTime, ValidationError>(_ => description.Length <= EventConstants.EVENT_DESCRIPTION_MAX_SIZE, EventErrors.EventDescriptionMaxSize)
.Then(_ => new Event(
EventId.New(),
eventTypeId,
teamId,
fromUtc,
toUtc,
description,
EventStatus.Open,
meetTime,
replyClosingTimeBeforeMeetTime
));
}
}
Loading

0 comments on commit b5ac482

Please sign in to comment.