diff --git a/src/TeamUp.Api/Extensions/ResultExtensions.cs b/src/TeamUp.Api/Extensions/ResultExtensions.cs index 10aa8e6..82a2041 100644 --- a/src/TeamUp.Api/Extensions/ResultExtensions.cs +++ b/src/TeamUp.Api/Extensions/ResultExtensions.cs @@ -1,4 +1,6 @@ -using TeamUp.Common; +using Microsoft.AspNetCore.Mvc; + +using TeamUp.Common; namespace TeamUp.Api.Extensions; @@ -22,10 +24,11 @@ public static IResult Match(this Result result, Func success) public static IResult ToResponse(this ErrorBase error) { - return TypedResults.Problem( - title: error.GetType().Name, - detail: error.Message, - statusCode: error switch + return TypedResults.Problem(new ProblemDetails + { + Title = error.GetType().Name, + Detail = error.Message, + Status = error switch { AuthenticationError => StatusCodes.Status401Unauthorized, AuthorizationError => StatusCodes.Status403Forbidden, @@ -37,6 +40,6 @@ public static IResult ToResponse(this ErrorBase error) InternalError => StatusCodes.Status500InternalServerError, _ => StatusCodes.Status500InternalServerError } - ); + }); } } diff --git a/src/TeamUp.Common/Result/Extensions/ResultExtensions.Ensure.cs b/src/TeamUp.Common/Result/Extensions/ResultExtensions.Ensure.cs index 0642cf6..6307275 100644 --- a/src/TeamUp.Common/Result/Extensions/ResultExtensions.Ensure.cs +++ b/src/TeamUp.Common/Result/Extensions/ResultExtensions.Ensure.cs @@ -148,4 +148,24 @@ public static Result EnsureNotNull(this Resul var self = await selfTask; return self.EnsureSecondNotNull(error); } + + public static Result ThenEnsure(this Result self, Func selector, Rule rule, TError error) where TError : ErrorBase + { + if (self.IsFailure) + return self.Error; + + var property = selector(self.Value); + if (!rule(property)) + return error; + + return property; + } + + public static Result ThenEnsure(this Result self, Func selector, TRule rule) where TRule : IRuleWithError + { + if (self.IsFailure) + return self.Error; + + return rule.Apply(selector(self.Value)); + } } diff --git a/src/TeamUp.Contracts/Events/CreateEventRequest.cs b/src/TeamUp.Contracts/Events/CreateEventRequest.cs index 500e5d5..8271f2d 100644 --- a/src/TeamUp.Contracts/Events/CreateEventRequest.cs +++ b/src/TeamUp.Contracts/Events/CreateEventRequest.cs @@ -43,7 +43,9 @@ public Validator(IDateTimeProvider dateTimeProvider) .Must((model, to) => model.FromUtc < to) .WithMessage("Event cannot end before it starts."); - RuleFor(x => x.Description).NotEmpty(); + RuleFor(x => x.Description) + .NotEmpty() + .MaximumLength(EventConstants.EVENT_DESCRIPTION_MAX_SIZE); RuleFor(x => x.MeetTime).GreaterThan(TimeSpan.Zero); diff --git a/src/TeamUp.Contracts/Events/EventConstatnts.cs b/src/TeamUp.Contracts/Events/EventConstatnts.cs new file mode 100644 index 0000000..6de726c --- /dev/null +++ b/src/TeamUp.Contracts/Events/EventConstatnts.cs @@ -0,0 +1,8 @@ +namespace TeamUp.Contracts.Events; + +public static class EventConstants +{ + public const int EVENT_DESCRIPTION_MAX_SIZE = 30; + + public const int EVENT_REPLY_MESSAGE_MAX_SIZE = 80; +} diff --git a/src/TeamUp.Contracts/Events/UpsertEventReplyRequest.cs b/src/TeamUp.Contracts/Events/UpsertEventReplyRequest.cs index b2780e8..a168967 100644 --- a/src/TeamUp.Contracts/Events/UpsertEventReplyRequest.cs +++ b/src/TeamUp.Contracts/Events/UpsertEventReplyRequest.cs @@ -1,4 +1,6 @@ -using FluentValidation; +using System.ComponentModel.DataAnnotations; + +using FluentValidation; using TeamUp.Contracts.Abstractions; @@ -7,6 +9,8 @@ namespace TeamUp.Contracts.Events; public sealed class UpsertEventReplyRequest : IRequestBody { public required ReplyType ReplyType { get; init; } + + [DataType(DataType.Text)] public required string Message { get; init; } public sealed class Validator : AbstractValidator @@ -18,7 +22,7 @@ public Validator() RuleFor(x => x.Message) .Empty().When(x => x.ReplyType == ReplyType.Yes, ApplyConditionTo.CurrentValidator) .NotEmpty().When(x => x.ReplyType == ReplyType.No || x.ReplyType == ReplyType.Maybe, ApplyConditionTo.CurrentValidator) - .MaximumLength(80); + .MaximumLength(EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE); } } } diff --git a/src/TeamUp.Contracts/Teams/TeamConstants.cs b/src/TeamUp.Contracts/Teams/TeamConstants.cs index fec7224..5739651 100644 --- a/src/TeamUp.Contracts/Teams/TeamConstants.cs +++ b/src/TeamUp.Contracts/Teams/TeamConstants.cs @@ -7,4 +7,9 @@ public static class TeamConstants public const int NICKNAME_MIN_SIZE = 3; public const int NICKNAME_MAX_SIZE = 30; + + public const int EVENTTYPE_NAME_MIN_SIZE = 3; + public const int EVENTTYPE_NAME_MAX_SIZE = 30; + + public const int EVENTTYPE_DESCRIPTION_MAX_SIZE = 30; } diff --git a/src/TeamUp.Domain/Aggregates/Events/Event.cs b/src/TeamUp.Domain/Aggregates/Events/Event.cs index b456149..0b42aaa 100644 --- a/src/TeamUp.Domain/Aggregates/Events/Event.cs +++ b/src/TeamUp.Domain/Aggregates/Events/Event.cs @@ -110,6 +110,7 @@ public static Result Create( return fromUtc .Ensure(from => from < toUtc, EventErrors.CannotEndBeforeStart) .Ensure(from => from > dateTimeProvider.DateTimeOffsetUtcNow, EventErrors.CannotStartInPast) + .ThenEnsure(_ => description.Length, len => len <= EventConstants.EVENT_DESCRIPTION_MAX_SIZE, EventErrors.EventDescriptionMaxSize) .Then(_ => new Event( EventId.New(), eventTypeId, diff --git a/src/TeamUp.Domain/Aggregates/Events/EventErrors.cs b/src/TeamUp.Domain/Aggregates/Events/EventErrors.cs index fdb612c..65421de 100644 --- a/src/TeamUp.Domain/Aggregates/Events/EventErrors.cs +++ b/src/TeamUp.Domain/Aggregates/Events/EventErrors.cs @@ -1,9 +1,12 @@ using TeamUp.Common; +using TeamUp.Contracts.Events; namespace TeamUp.Domain.Aggregates.Events; public static class EventErrors { + public static readonly ValidationError EventDescriptionMaxSize = ValidationError.New($"Event's description must be shorter than {EventConstants.EVENT_DESCRIPTION_MAX_SIZE} characters.", "Events.DescriptionMaxSize"); + public static readonly ValidationError CannotEndBeforeStart = ValidationError.New("Event cannot end before it starts.", "Events.CannotEndBeforeStart"); public static readonly ValidationError CannotStartInPast = ValidationError.New("Cannot create event in the past.", "Events.CannotStartInPast"); @@ -11,4 +14,5 @@ public static class EventErrors public static readonly DomainError TimeForResponsesExpired = DomainError.New("Time for responses expired.", "Events.TimeForResponsesExpired"); public static readonly NotFoundError EventNotFound = NotFoundError.New("Event not found.", "Events.NotFound"); + } diff --git a/src/TeamUp.Domain/Aggregates/Teams/EventType.cs b/src/TeamUp.Domain/Aggregates/Teams/EventType.cs index a445f11..3515537 100644 --- a/src/TeamUp.Domain/Aggregates/Teams/EventType.cs +++ b/src/TeamUp.Domain/Aggregates/Teams/EventType.cs @@ -1,4 +1,5 @@ -using TeamUp.Contracts.Teams; +using TeamUp.Common; +using TeamUp.Contracts.Teams; using TeamUp.Domain.Abstractions; namespace TeamUp.Domain.Aggregates.Teams; @@ -20,9 +21,12 @@ private EventType(EventTypeId id, string name, string description, Team team) : TeamId = team.Id; } - public static EventType Create(string name, string description, Team team) + public static Result Create(string name, string description, Team team) { - return new(EventTypeId.New(), name, description, team); + return name + .Ensure(TeamRules.EventTypeNameMinSize, TeamRules.EventTypeNameMaxSize) + .ThenEnsure(_ => description, TeamRules.EventTypeDescriptionMaxSize) + .Then(_ => new EventType(EventTypeId.New(), name, description, team)); } internal void UpdateName(string name) diff --git a/src/TeamUp.Domain/Aggregates/Teams/TeamErrors.cs b/src/TeamUp.Domain/Aggregates/Teams/TeamErrors.cs index c16ce54..39183aa 100644 --- a/src/TeamUp.Domain/Aggregates/Teams/TeamErrors.cs +++ b/src/TeamUp.Domain/Aggregates/Teams/TeamErrors.cs @@ -9,6 +9,9 @@ public static class TeamErrors public static readonly ValidationError TeamNameMaxSize = ValidationError.New($"Name must be shorter than {TeamConstants.TEAM_NAME_MAX_SIZE} characters.", "Teams.NameMaxSize"); public static readonly ValidationError NicknameMinSize = ValidationError.New($"Nickname must be atleast {TeamConstants.NICKNAME_MIN_SIZE} characters long.", "Teams.Members.NicknameMinSize"); public static readonly ValidationError NicknameMaxSize = ValidationError.New($"Nickname must be shorter than {TeamConstants.NICKNAME_MAX_SIZE} characters.", "Teams.Members.NicknameMaxSize"); + public static readonly ValidationError EventTypeNameMinSize = ValidationError.New($"EventType's name must be atleast {TeamConstants.EVENTTYPE_NAME_MIN_SIZE} characters long.", "Teams.EventTypes.NameMinSize"); + public static readonly ValidationError EventTypeNameMaxSize = ValidationError.New($"EventType's name must be shorter than {TeamConstants.EVENTTYPE_NAME_MAX_SIZE} characters.", "Teams.EventTypes.NameMaxSize"); + public static readonly ValidationError EventTypeDescriptionMaxSize = ValidationError.New($"EventType's description must be shorter than {TeamConstants.EVENTTYPE_NAME_MAX_SIZE} characters.", "Teams.EventTypes.DescriptionMaxSize"); public static readonly AuthorizationError NotMemberOfTeam = AuthorizationError.New("Not member of the team.", "Teams.NotMember"); public static readonly AuthorizationError UnauthorizedToChangeTeamName = AuthorizationError.New("Not allowed to change team name.", "Teams.NotAllowedToChangeName"); diff --git a/src/TeamUp.Domain/Aggregates/Teams/TeamRules.cs b/src/TeamUp.Domain/Aggregates/Teams/TeamRules.cs index 71bb932..18f33d6 100644 --- a/src/TeamUp.Domain/Aggregates/Teams/TeamRules.cs +++ b/src/TeamUp.Domain/Aggregates/Teams/TeamRules.cs @@ -10,6 +10,9 @@ public static class TeamRules static readonly Rule TeamNameMaxSizeRule = name => name.Length <= TeamConstants.TEAM_NAME_MAX_SIZE; static readonly Rule NicknameMinSizeRule = nickname => nickname.Length >= TeamConstants.NICKNAME_MIN_SIZE; static readonly Rule NicknameMaxSizeRule = nickname => nickname.Length <= TeamConstants.NICKNAME_MAX_SIZE; + static readonly Rule EventTypeNameMinSizeRule = eventTypeName => eventTypeName.Length >= TeamConstants.EVENTTYPE_NAME_MIN_SIZE; + static readonly Rule EventTypeNameMaxSizeRule = eventTypeName => eventTypeName.Length <= TeamConstants.EVENTTYPE_NAME_MAX_SIZE; + static readonly Rule EventTypeDescriptionMaxRule = eventTypeDescription => eventTypeDescription.Length <= TeamConstants.EVENTTYPE_DESCRIPTION_MAX_SIZE; public static readonly Rule RoleIsNotOwner = role => !role.IsOwner(); public static readonly Rule MemberIsNotTeamOwner = member => !member.Role.IsOwner(); @@ -23,6 +26,11 @@ public static class TeamRules public static readonly RuleWithError NicknameMinSize = new(NicknameMinSizeRule, TeamErrors.NicknameMinSize); public static readonly RuleWithError NicknameMaxSize = new(NicknameMaxSizeRule, TeamErrors.NicknameMaxSize); + public static readonly RuleWithError EventTypeNameMinSize = new(EventTypeNameMinSizeRule, TeamErrors.EventTypeNameMinSize); + public static readonly RuleWithError EventTypeNameMaxSize = new(EventTypeNameMaxSizeRule, TeamErrors.EventTypeNameMaxSize); + + public static readonly RuleWithError EventTypeDescriptionMaxSize = new(EventTypeDescriptionMaxRule, TeamErrors.EventTypeDescriptionMaxSize); + public static readonly RuleWithError MemberCanUpdateTeamRoles = new(MemberCanUpdateTeamRolesRule, TeamErrors.UnauthorizedToUpdateTeamRoles); public static readonly RuleWithError MemberCanChangeOwnership = new(MemberIsOwner, TeamErrors.UnauthorizedToChangeTeamOwnership); public static readonly RuleWithError MemberCanChangeTeamName = new(MemberIsOwner, TeamErrors.UnauthorizedToChangeTeamName); diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/EventGeneratorExtensions.cs b/tests/TeamUp.EndToEndTests/DataGenerators/EventGeneratorExtensions.cs index 9f1a348..faf6b05 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/EventGeneratorExtensions.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/EventGeneratorExtensions.cs @@ -21,7 +21,7 @@ public static EventGenerator WithRandomEventResponses(this EventGenerator genera .DropMicroSeconds() .AsUtc()) .RuleFor(er => er.ReplyType, f => f.Random.ArrayElement([ReplyType.Yes, ReplyType.No, ReplyType.Maybe, ReplyType.Delay])) - .RuleFor(er => er.Message, (f, er) => er.ReplyType == ReplyType.Yes ? string.Empty : f.Random.AlphaNumeric(30)) + .RuleFor(er => er.Message, (f, er) => er.ReplyType == ReplyType.Yes ? string.Empty : f.Random.Text(1, EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE)) .Generate()) .ToList()); } @@ -38,7 +38,7 @@ public static EventGenerator WithEventResponses(this EventGenerator generator, L .DropMicroSeconds() .AsUtc()) .RuleFor(er => er.ReplyType, response.Type) - .RuleFor(er => er.Message, (f, er) => er.ReplyType == ReplyType.Yes ? string.Empty : f.Random.AlphaNumeric(30)) + .RuleFor(er => er.Message, (f, er) => er.ReplyType == ReplyType.Yes ? string.Empty : f.Random.Text(1, EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE)) .Generate()) .ToList()); } diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/EventGenerators.cs b/tests/TeamUp.EndToEndTests/DataGenerators/EventGenerators.cs index 3a00e67..e5e4216 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/EventGenerators.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/EventGenerators.cs @@ -24,7 +24,7 @@ public sealed class EventGenerators : BaseGenerator public static readonly EventGenerator Event = new EventGenerator(binder: EventBinder) .UsePrivateConstructor() .RuleFor(e => e.Id, f => EventId.FromGuid(f.Random.Guid())) - .RuleFor(e => e.Description, f => f.Random.AlphaNumeric(20)) + .RuleFor(e => e.Description, f => f.Random.Text(1, EventConstants.EVENT_DESCRIPTION_MAX_SIZE)) .RuleFor(e => e.MeetTime, f => f.Date.Timespan(TimeSpan.FromHours(24)).DropMicroSeconds()) .RuleFor(e => e.ReplyClosingTimeBeforeMeetTime, f => f.Date.Timespan(TimeSpan.FromHours(24)).DropMicroSeconds()) .RuleFor(e => e.FromUtc, f => f.Date.Between(DateTime.UtcNow.AddDays(3), DateTime.UtcNow.AddMonths(6)).DropMicroSeconds().AsUtc()) @@ -36,7 +36,7 @@ public sealed class EventGenerators : BaseGenerator .RuleFor(er => er.Id, f => EventResponseId.FromGuid(f.Random.Guid())); public static readonly Faker ValidCreateEventRequest = new Faker() - .RuleFor(x => x.Description, f => f.Random.AlphaNumeric(20)) + .RuleFor(x => x.Description, f => f.Random.Text(1, EventConstants.EVENT_DESCRIPTION_MAX_SIZE)) .RuleFor(x => x.MeetTime, f => f.Date.Timespan(TimeSpan.FromHours(24)).DropMicroSeconds()) .RuleFor(x => x.ReplyClosingTimeBeforeMeetTime, f => f.Date.Timespan(TimeSpan.FromHours(24)).DropMicroSeconds()); @@ -45,16 +45,15 @@ public sealed class EventGenerators : BaseGenerator .RuleFor(r => r.Message, (f, r) => r.ReplyType switch { ReplyType.Yes => string.Empty, - ReplyType.Maybe => f.Random.Text(1, 80), - ReplyType.Delay => f.Random.Text(0, 80), - ReplyType.No or _ => f.Random.Text(1, 80), + ReplyType.Delay => f.Random.Text(0, EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE), + ReplyType.Maybe or ReplyType.No or _ => f.Random.Text(1, EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE), }); public sealed class InvalidCreateEventRequest : TheoryData> { public InvalidCreateEventRequest() { - //short description + //empty description this.Add(x => x.Description, new CreateEventRequest { EventTypeId = EventTypeId.FromGuid(default), @@ -158,21 +157,21 @@ public InvalidUpsertEventReplyRequest() this.Add(x => x.Message, new UpsertEventReplyRequest { ReplyType = ReplyType.Maybe, - Message = F.Random.AlphaNumeric(100) + Message = F.Random.AlphaNumeric(EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE + 1) }); //long message this.Add(x => x.Message, new UpsertEventReplyRequest { ReplyType = ReplyType.No, - Message = F.Random.AlphaNumeric(100) + Message = F.Random.AlphaNumeric(EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE + 1) }); //long message this.Add(x => x.Message, new UpsertEventReplyRequest { ReplyType = ReplyType.Delay, - Message = F.Random.AlphaNumeric(100) + Message = F.Random.AlphaNumeric(EventConstants.EVENT_REPLY_MESSAGE_MAX_SIZE + 1) }); } } diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerators.cs b/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerators.cs index bd301bb..7688a46 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerators.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerators.cs @@ -35,15 +35,15 @@ public sealed class TeamGenerators : BaseGenerator public static readonly Faker EventType = new Faker(binder: EventTypeBinder) .UsePrivateConstructor() .RuleFor(et => et.Id, f => EventTypeId.FromGuid(f.Random.Guid())) - .RuleFor(et => et.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(et => et.Description, f => f.Random.AlphaNumeric(30)); + .RuleFor(et => et.Name, f => f.Random.Text(TeamConstants.EVENTTYPE_NAME_MIN_SIZE, TeamConstants.EVENTTYPE_NAME_MAX_SIZE)) + .RuleFor(et => et.Description, f => f.Random.Text(0, TeamConstants.EVENTTYPE_DESCRIPTION_MAX_SIZE)); public static readonly Faker ValidUpsertEventTypeRequest = new Faker() - .RuleFor(r => r.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(r => r.Description, f => f.Random.AlphaNumeric(40)); + .RuleFor(r => r.Name, f => f.Random.Text(TeamConstants.EVENTTYPE_NAME_MIN_SIZE, TeamConstants.EVENTTYPE_NAME_MAX_SIZE)) + .RuleFor(r => r.Description, f => f.Random.Text(0, TeamConstants.EVENTTYPE_DESCRIPTION_MAX_SIZE)); - public static string GenerateValidTeamName() => F.Random.AlphaNumeric(10); - public static string GenerateValidNickname() => F.Random.AlphaNumeric(10); + public static string GenerateValidTeamName() => F.Random.Text(TeamConstants.TEAM_NAME_MIN_SIZE, TeamConstants.TEAM_NAME_MAX_SIZE); + public static string GenerateValidNickname() => F.Random.Text(TeamConstants.NICKNAME_MIN_SIZE, TeamConstants.NICKNAME_MAX_SIZE); public sealed class InvalidUpsertEventTypeRequest : TheoryData> { diff --git a/tests/TeamUp.EndToEndTests/DataGenerators/UserGenerators.cs b/tests/TeamUp.EndToEndTests/DataGenerators/UserGenerators.cs index 50e6edd..222f550 100644 --- a/tests/TeamUp.EndToEndTests/DataGenerators/UserGenerators.cs +++ b/tests/TeamUp.EndToEndTests/DataGenerators/UserGenerators.cs @@ -55,7 +55,7 @@ public InvalidRegisterUserRequests() this.Add(x => x.Name, new RegisterUserRequest { Email = F.Internet.Email(), - Name = F.Lorem.Random.AlphaNumeric(TeamConstants.TEAM_NAME_MAX_SIZE + 1), + Name = F.Random.AlphaNumeric(TeamConstants.TEAM_NAME_MAX_SIZE + 1), Password = GenerateValidPassword() });