Skip to content

Commit

Permalink
Team Management - Teams (#5)
Browse files Browse the repository at this point in the history
* added logic and endpoint tests for teams (CRUD etc.)
* added endpoint tests for testing concurrency errors
* fixed dependency tests
* refactoring (naming ...)
  • Loading branch information
skrasekmichael committed Apr 23, 2024
1 parent b5ac482 commit 5c59487
Show file tree
Hide file tree
Showing 69 changed files with 3,785 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ public async Task<Result> SaveChangesAsync(CancellationToken ct = default)
}
catch (DbUpdateConcurrencyException ex)
{
_logger.LogInformation("Database Concurrency Conflict: {msg}", ex.Message);
_logger.LogInformation("{moduleId} Database Concurrency Conflict: {msg}", typeof(TModuleId).Name, ex.Message);
return ConcurrencyError;
}
catch (DbUpdateException ex)
{
_logger.LogError(ex.InnerException, "Database Update Exception");
_logger.LogError(ex.InnerException, "{moduleId} Database Update Exception", typeof(TModuleId).Name);

if (ex.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation })
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using TeamUp.TeamManagement.Domain.Aggregates.Events;
using TeamUp.TeamManagement.Domain.Aggregates.Invitations;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;
using TeamUp.TeamManagement.Domain.Aggregates.Users;

namespace TeamUp.TeamManagement.Application;

public interface ITeamManagementQueryContext
{
public IQueryable<User> Users { get; }
public IQueryable<Team> Teams { get; }
public IQueryable<Invitation> Invitations { get; }
public IQueryable<Event> Events { get; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<None Remove="Teams\ChangeOwnershipCommandHandler.cs~RF67c66b7a.TMP" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Common\TeamUp.Common.Application\TeamUp.Common.Application.csproj" />
<ProjectReference Include="..\TeamUp.TeamManagement.Domain\TeamUp.TeamManagement.Domain.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.ChangeNickname;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class ChangeNicknameCommandHandler : ICommandHandler<ChangeNicknameCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public ChangeNicknameCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(ChangeNicknameCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.ChangeNickname(command.InitiatorId, command.Nickname))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.ChangeOwnership;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class ChangeOwnershipCommandHandler : ICommandHandler<ChangeOwnershipCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public ChangeOwnershipCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(ChangeOwnershipCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.ChangeOwnership(command.InitiatorId, command.NewOwnerId))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams;
using TeamUp.TeamManagement.Contracts.Teams.CreateEventType;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class CreateEventTypeCommandHandler : ICommandHandler<CreateEventTypeCommand, EventTypeId>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public CreateEventTypeCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result<EventTypeId>> Handle(CreateEventTypeCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.CreateEventType(command.InitiatorId, command.Name, command.Description))
.TapAsync(_ => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.Common.Contracts;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams;
using TeamUp.TeamManagement.Contracts.Teams.CreateTeam;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;
using TeamUp.TeamManagement.Domain.Aggregates.Users;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class CreateTeamCommandHandler : ICommandHandler<CreateTeamCommand, TeamId>
{
private readonly ITeamRepository _teamRepository;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IUserRepository _userRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

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

public async Task<Result<TeamId>> Handle(CreateTeamCommand command, CancellationToken ct)
{
var user = await _userRepository.GetUserByIdAsync(command.OwnerId, ct);
return await user
.EnsureNotNull(UserErrors.UserNotFound)
.Ensure(TeamRules.UserDoesNotOwnToManyTeams)
.Then(user => Team.Create(command.Name, user, _dateTimeProvider))
.Tap(_teamRepository.AddTeam)
.Then(team => team.Id)
.TapAsync(_ => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.DeleteTeam;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class DeleteTeamCommandHandler : ICommandHandler<DeleteTeamCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public DeleteTeamCommandHandler(ITeamRepository teamDomainService, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamDomainService;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(DeleteTeamCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.Delete(command.InitiatorId))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore;

using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.Contracts.Teams;
using TeamUp.TeamManagement.Contracts.Teams;
using TeamUp.TeamManagement.Contracts.Teams.GetTeam;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class GetTeamQueryHandler : IQueryHandler<GetTeamQuery, TeamResponse>
{
private readonly ITeamManagementQueryContext _appQueryContext;

public GetTeamQueryHandler(ITeamManagementQueryContext appQueryContext)
{
_appQueryContext = appQueryContext;
}

public async Task<Result<TeamResponse>> Handle(GetTeamQuery query, CancellationToken ct)
{
var team = await _appQueryContext.Teams
.Where(team => team.Id == query.TeamId)
.Select(team => new TeamResponse
{
Name = team.Name,
Members = team.Members
.Select(member => new TeamMemberResponse
{
Id = member.Id,
UserId = member.UserId,
Nickname = member.Nickname,
Role = member.Role
})
.ToList()
.AsReadOnly()
})
.FirstOrDefaultAsync(ct);

return team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Ensure(team => team.Members.Any(member => member.UserId == query.InitiatorId), TeamErrors.NotMemberOfTeam);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.RemoveTeamMember;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class RemoveTeamMemberCommandHandler : ICommandHandler<RemoveTeamMemberCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public RemoveTeamMemberCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(RemoveTeamMemberCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.RemoveTeamMember(command.InitiatorId, command.MemberId))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.SetMemberRole;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class SetMemberRoleCommandHandler : ICommandHandler<SetMemberRoleCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public SetMemberRoleCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(SetMemberRoleCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.SetMemberRole(command.InitiatorId, command.MemberId, command.Role))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RailwayResult;
using RailwayResult.FunctionalExtensions;

using TeamUp.Common.Application;
using TeamUp.TeamManagement.Contracts;
using TeamUp.TeamManagement.Contracts.Teams.SetTeamName;
using TeamUp.TeamManagement.Domain.Aggregates.Teams;

namespace TeamUp.TeamManagement.Application.Teams;

internal sealed class SetTeamNameCommandHandler : ICommandHandler<SetTeamNameCommand>
{
private readonly ITeamRepository _teamRepository;
private readonly IUnitOfWork<TeamManagementModuleId> _unitOfWork;

public SetTeamNameCommandHandler(ITeamRepository teamRepository, IUnitOfWork<TeamManagementModuleId> unitOfWork)
{
_teamRepository = teamRepository;
_unitOfWork = unitOfWork;
}

public async Task<Result> Handle(SetTeamNameCommand command, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(command.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.ChangeTeamName(command.InitiatorId, command.Name))
.TapAsync(() => _unitOfWork.SaveChangesAsync(ct));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\Common\TeamUp.Common.Contracts\TeamUp.Common.Contracts.csproj" />
<ProjectReference Include="..\..\UserAccess\TeamUp.UserAccess.Contracts\TeamUp.UserAccess.Contracts.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using FluentValidation;
using TeamUp.UserAccess.Contracts;
using TeamUp.Common.Contracts;

namespace TeamUp.TeamManagement.Contracts.Teams.ChangeNickname;

public sealed record ChangeNicknameCommand : ICommand
{
public required UserId InitiatorId { get; init; }
public required TeamId TeamId { get; init; }
public required string Nickname { get; init; }

public sealed class Validator : AbstractValidator<ChangeNicknameCommand>
{
public Validator()
{
RuleFor(x => x.InitiatorId).NotEmpty();
RuleFor(x => x.TeamId).NotEmpty();
RuleFor(x => x.Nickname)
.NotEmpty()
.MinimumLength(TeamConstants.NICKNAME_MIN_SIZE)
.MaximumLength(TeamConstants.NICKNAME_MAX_SIZE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;

namespace TeamUp.TeamManagement.Contracts.Teams.ChangeNickname;

public sealed class ChangeNicknameRequest
{
[DataType(DataType.Text)]
public required string Nickname { get; init; }
}
Loading

0 comments on commit 5c59487

Please sign in to comment.