Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Event Type #18

Merged
merged 1 commit into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/TeamUp.Api/Endpoints/TeamEndpointGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public void MapEndpoints(RouteGroupBuilder group)
.MapEndpoint<RemoveTeamMemberEndpoint>()
.MapEndpoint<UpdateTeamMemberRoleEndpoint>()
.MapEndpoint<UpdateTeamNameEndpoint>()
.MapEndpoint<ChangeNicknameEndpoint>();
.MapEndpoint<ChangeNicknameEndpoint>()
.MapEndpoint<CreateEventTypeEndpoint>();
}
}
47 changes: 47 additions & 0 deletions src/TeamUp.Api/Endpoints/Teams/CreateEventTypeEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using MediatR;

using Microsoft.AspNetCore.Mvc;

using TeamUp.Api.Extensions;
using TeamUp.Application.Teams.CreateEventType;
using TeamUp.Contracts.Teams;
using TeamUp.Domain.Aggregates.Teams;

namespace TeamUp.Api.Endpoints.Teams;

public sealed class CreateEventTypeEndpoint : IEndpointGroup
{
public void MapEndpoints(RouteGroupBuilder group)
{
group.MapPost("/{teamId:guid}/event-types", CreateEventTypeAsync)
.Produces<EventTypeId>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status401Unauthorized)
.Produces(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound)
.ProducesValidationProblem()
.WithName(nameof(CreateEventTypeEndpoint))
.MapToApiVersion(1);
}

private async Task<IResult> CreateEventTypeAsync(
[FromRoute] Guid teamId,
[FromBody] UpsertEventTypeRequest request,
[FromServices] ISender sender,
[FromServices] IHttpContextAccessor httpContextAccessor,
[FromServices] LinkGenerator linkGenerator,
CancellationToken ct)
{
var command = new CreateEventTypeCommand(
httpContextAccessor.GetLoggedUserId(),
TeamId.FromGuid(teamId),
request.Name,
request.Description
);

var result = await sender.Send(command, ct);
return result.Match(eventTypeId => TypedResults.Created(
uri: linkGenerator.GetPathByName(nameof(GetTeamEndpoint)),
value: eventTypeId
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using TeamUp.Application.Abstractions;
using TeamUp.Common;
using TeamUp.Domain.Aggregates.Teams;
using TeamUp.Domain.Aggregates.Users;

namespace TeamUp.Application.Teams.CreateEventType;

public sealed record CreateEventTypeCommand(UserId InitiatorId, TeamId TeamId, string Name, string Description) : ICommand<Result<EventTypeId>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using TeamUp.Application.Abstractions;
using TeamUp.Common;
using TeamUp.Domain.Abstractions;
using TeamUp.Domain.Aggregates.Teams;

namespace TeamUp.Application.Teams.CreateEventType;

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

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

public async Task<Result<EventTypeId>> Handle(CreateEventTypeCommand request, CancellationToken ct)
{
var team = await _teamRepository.GetTeamByIdAsync(request.TeamId, ct);
return await team
.EnsureNotNull(TeamErrors.TeamNotFound)
.Then(team => team.CreateEventType(request.InitiatorId, request.Name, request.Description))
.TapAsync(_ => _unitOfWork.SaveChangesAsync(ct));
}
}
8 changes: 8 additions & 0 deletions src/TeamUp.Common/Result/Extensions/ResultExtensions.Then.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ public static Result Then<TFirst, TSecond>(this Result<(TFirst, TSecond)> self,
return Result.Success;
}

public static Result<TOut> Then<TFirst, TSecond, TOut>(this Result<(TFirst, TSecond)> self, Func<TFirst, TSecond, TOut> mapper)
{
if (self.IsFailure)
return self.Error;

return mapper(self.Value.Item1, self.Value.Item2);
}

public static Result<TOut> Then<TFirst, TSecond, TOut>(this Result<(TFirst, TSecond)> self, Func<TFirst, TSecond, Result<TOut>> mapper)
{
if (self.IsFailure)
Expand Down
23 changes: 23 additions & 0 deletions src/TeamUp.Contracts/Teams/UpsertEventTypeRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;

using FluentValidation;

namespace TeamUp.Contracts.Teams;

public sealed class UpsertEventTypeRequest : IRequestBody
{
[DataType(DataType.Text)]
public required string Name { get; init; }

[DataType(DataType.Text)]
public required string Description { get; init; }

public sealed class Validator : AbstractValidator<UpsertEventTypeRequest>
{
public Validator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Description).NotEmpty();
}
}
}
20 changes: 8 additions & 12 deletions src/TeamUp.Domain/Aggregates/Teams/Team.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,13 @@ private Team(TeamId id, string name) : base(id)
Name = name;
}

public Result UpdateOrCreateEventType(EventTypeId id, string name, string description)
public Result<EventTypeId> CreateEventType(UserId initiatorId, string name, string description)
{
var eventType = _eventTypes.Find(et => et.Id == id);
if (eventType is null)
{
_eventTypes.Add(EventType.Create(name, description, this));
return Result.Success;
}

eventType.UpdateName(name);
eventType.UpdateDescription(description);
return Result.Success;
return GetTeamMemberByUserId(initiatorId)
.Ensure(Rules.MemberCanCreateEventTypes)
.Then(_ => EventType.Create(name, description, this))
.Tap(_eventTypes.Add)
.Then(eventType => eventType.Id);
}

internal void AddTeamMember(User user, IDateTimeProvider dateTimeProvider, TeamRole role = TeamRole.Member)
Expand All @@ -69,7 +64,8 @@ public Result RemoveTeamMember(UserId initiatorId, TeamMemberId teamMemberId)
.Ensure(Rules.MemberIsNotTeamOwner, Errors.CannotRemoveTeamOwner)
.And(() => GetTeamMemberByUserId(initiatorId))
.Ensure(Rules.MemberCanBeRemovedByInitiator)
.Then((member, _) => _members.Remove(member));
.Tap((member, _) => _members.Remove(member))
.ToResult();
}

public Result ChangeNickname(UserId initiatorId, string newNickname)
Expand Down
1 change: 1 addition & 0 deletions src/TeamUp.Domain/Aggregates/Teams/TeamErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static class TeamErrors
public static readonly AuthorizationError UnauthorizedToCreateEvents = AuthorizationError.New("Not allowed to create events.", "Teams.NotAllowedToCreateEvents");
public static readonly AuthorizationError UnauthorizedToInviteTeamMembers = AuthorizationError.New("Not allowed to invite team members.", "Teams.NotAllowedToInviteTeamMembers");
public static readonly AuthorizationError UnauthorizedToDeleteTeam = AuthorizationError.New("Not allowed to delete team.", "Teams.NotAllowedToDeleteTeam");
public static readonly AuthorizationError UnauthorizedToCreateEventTypes = AuthorizationError.New("Not allowed to create event types.", "Teams.NotAllowedToCreateEventTypes");

public static readonly NotFoundError TeamNotFound = NotFoundError.New("Team not found.", "Teams.NotFound");
public static readonly NotFoundError MemberNotFound = NotFoundError.New("Member not found.", "Teams.Members.NotFound");
Expand Down
2 changes: 2 additions & 0 deletions src/TeamUp.Domain/Aggregates/Teams/TeamRole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public static class TeamRoleExtensions
{
public static bool IsOwner(this TeamRole role) => role == TeamRole.Owner;

public static bool CanManipulateEventTypes(this TeamRole role) => role >= TeamRole.Coordinator;

public static bool CanCreateEvents(this TeamRole role) => role >= TeamRole.Coordinator;

public static bool CanUpdateTeamRoles(this TeamRole role) => role >= TeamRole.Admin;
Expand Down
2 changes: 2 additions & 0 deletions src/TeamUp.Domain/Aggregates/Teams/TeamRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class TeamRules
public static readonly Rule<TeamMember> MemberIsNotTeamOwner = member => !member.Role.IsOwner();
public static readonly Rule<TeamMember> MemberIsOwner = member => member.Role.IsOwner();
static readonly Rule<TeamMember> MemberCanUpdateTeamRolesRule = member => member.Role.CanUpdateTeamRoles();
static readonly Rule<TeamMember> MemberCanManipulateEventTypesRule = member => member.Role.CanManipulateEventTypes();

public static readonly RuleWithError<string> TeamNameMinSize = new(TeamNameMinSizeRule, TeamErrors.TeamNameMinSize);
public static readonly RuleWithError<string> TeamNameMaxSize = new(TeamNameMaxSizeRule, TeamErrors.TeamNameMaxSize);
Expand All @@ -23,6 +24,7 @@ public static class TeamRules
public static readonly RuleWithError<TeamMember> MemberCanUpdateTeamRoles = new(MemberCanUpdateTeamRolesRule, TeamErrors.UnauthorizedToUpdateTeamRoles);
public static readonly RuleWithError<TeamMember> MemberCanChangeOwnership = new(MemberIsOwner, TeamErrors.UnauthorizedToChangeTeamOwnership);
public static readonly RuleWithError<TeamMember> MemberCanChangeTeamName = new(MemberIsOwner, TeamErrors.UnauthorizedToChangeTeamName);
public static readonly RuleWithError<TeamMember> MemberCanCreateEventTypes = new(MemberCanManipulateEventTypesRule, TeamErrors.UnauthorizedToCreateEventTypes);

public static readonly RuleWithError<(TeamMember Member, TeamMember Initiator)> MemberCanBeRemovedByInitiator = new(
context => context.Initiator.Role.CanRemoveTeamMembers() || context.Initiator.Id == context.Member.Id,
Expand Down
24 changes: 24 additions & 0 deletions tests/TeamUp.EndToEndTests/DataGenerators/InvalidRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Linq.Expressions;
using System.Reflection;

namespace TeamUp.EndToEndTests.DataGenerators;

public sealed class InvalidRequest<TRequest>
{
public required string InvalidProperty { get; init; }
public required TRequest Request { get; init; }

public static InvalidRequest<TRequest> Create<TOut>(Expression<Func<TRequest, TOut>> property, TRequest request)
{
if (property.Body is not MemberExpression memberExpression || memberExpression.Member is not PropertyInfo propertyInfo)
{
throw new ArgumentException("Expression must be a MemberExpression pointing to a PropertyInfo", nameof(property));
}

return new()
{
InvalidProperty = propertyInfo.Name,
Request = request
};
}
}
25 changes: 25 additions & 0 deletions tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Linq;

using TeamUp.Contracts.Teams;
using TeamUp.Contracts.Users;

namespace TeamUp.EndToEndTests.DataGenerators;

public sealed class TeamGenerator : BaseGenerator
Expand All @@ -25,6 +28,10 @@ public sealed class TeamGenerator : BaseGenerator
.UsePrivateConstructor()
.RuleFor(tm => tm.Id, f => TeamMemberId.FromGuid(f.Random.Uuid()));

public static readonly Faker<UpsertEventTypeRequest> ValidUpsertEventTypeRequest = new Faker<UpsertEventTypeRequest>()
.RuleFor(r => r.Name, f => f.Random.AlphaNumeric(10))
.RuleFor(r => r.Description, f => f.Random.AlphaNumeric(40));

public static Team GenerateTeamWithOwner(User owner)
{
return EmptyTeam
Expand Down Expand Up @@ -69,4 +76,22 @@ public static Team GenerateTeamWith(List<User> members, params (User User, TeamR
)
.Generate();
}

public sealed class InvalidUpsertEventTypeRequest : TheoryData<InvalidRequest<UpsertEventTypeRequest>>
{
public InvalidUpsertEventTypeRequest()
{
this.Add(x => x.Name, new UpsertEventTypeRequest
{
Name = "",
Description = "xxx"
});

this.Add(x => x.Description, new UpsertEventTypeRequest
{
Name = "xxx",
Description = ""
});
}
}
}
10 changes: 5 additions & 5 deletions tests/TeamUp.EndToEndTests/DataGenerators/UserGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,32 @@ public static User GenerateUser(Password password, UserStatus status)
.Generate();
}

public sealed class InvalidRegisterUserRequests : TheoryData<RegisterUserRequest>
public sealed class InvalidRegisterUserRequests : TheoryData<InvalidRequest<RegisterUserRequest>>
{
public InvalidRegisterUserRequests()
{
Add(new RegisterUserRequest()
this.Add(x => x.Email, new RegisterUserRequest
{
Email = "invalid email",
Name = F.Internet.UserName(),
Password = GenerateValidPassword()
});

Add(new RegisterUserRequest()
this.Add(x => x.Name, new RegisterUserRequest
{
Email = F.Internet.Email(),
Name = "xx",
Password = GenerateValidPassword()
});

Add(new RegisterUserRequest()
this.Add(x => x.Name, new RegisterUserRequest
{
Email = F.Internet.Email(),
Name = F.Lorem.Random.AlphaNumeric(User.NAME_MAX_SIZE + 1),
Password = GenerateValidPassword()
});

Add(new RegisterUserRequest()
this.Add(x => x.Password, new RegisterUserRequest
{
Email = F.Internet.Email(),
Name = F.Internet.UserName(),
Expand Down
Loading
Loading