Skip to content

Commit

Permalink
Refactoring Result Pattern and Endpoint Tests (#20)
Browse files Browse the repository at this point in the history
* refactored result pattern - removed unnecessary exclamation marks
* squashed tests into larger theories
* fixed tests and variable naming
* refactored test files to have one file per endpoint
  • Loading branch information
skrasekmichael authored Feb 21, 2024
1 parent aed978e commit 9efa463
Show file tree
Hide file tree
Showing 22 changed files with 433 additions and 632 deletions.
12 changes: 6 additions & 6 deletions src/TeamUp.Common/Result/Extensions/ResultExtensions.And.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static partial class ResultExtensions
if (self.IsFailure)
return self.Error;

return (self.Value!, func());
return (self.Value, func());
}

public static Result<(TFirst, TSecond)> And<TFirst, TSecond>(this Result<TFirst> self, Func<Result<TSecond>> func)
Expand All @@ -19,27 +19,27 @@ public static partial class ResultExtensions
if (result.IsFailure)
return result.Error;

return (self.Value!, result.Value!);
return (self.Value, result.Value);
}

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

var result = func(self.Value!);
var result = func(self.Value);
if (result.IsFailure)
return result.Error;

return (self.Value!, result.Value!);
return (self.Value, result.Value);
}

public static async Task<Result<(TFirst, TSecond)>> AndAsync<TFirst, TSecond>(this Result<TFirst> self, Func<TFirst, Task<TSecond>> func)
{
if (self.IsFailure)
return self.Error;

var result = await func(self.Value!);
return (self.Value!, result);
var result = await func(self.Value);
return (self.Value, result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static Result<TValue> Ensure<TValue, TError>(this Result<TValue> self, Ru
if (self.IsFailure)
return self;

if (!rule(self.Value!))
if (!rule(self.Value))
return error;

return self;
Expand All @@ -40,7 +40,7 @@ public static Result<TValue> Ensure<TValue, TRule>(this Result<TValue> self, par

foreach (var rule in rules)
{
var result = rule.Apply(self.Value!);
var result = rule.Apply(self.Value);
if (result.IsFailure)
return result;
}
Expand Down Expand Up @@ -78,7 +78,7 @@ public static Result<TValue> EnsureNotNull<TValue, TProperty, TError>(this Resul
if (self.IsFailure)
return self.Error;

if (selector(self.Value!) is null)
if (selector(self.Value) is null)
return error;

return self.Value;
Expand All @@ -100,7 +100,7 @@ public static Result<TValue> EnsureNotNull<TValue, TProperty, TError>(this Resul
if (self.IsFailure)
return self;

return rule.Apply(self.Value!);
return rule.Apply(self.Value);
}

public static async Task<Result<(TFirst, TSecond)>> Ensure<TFirst, TSecond, TError>(this Task<Result<(TFirst, TSecond)>> selfTask, Rule<TFirst, TSecond> rule, TError error) where TError : ErrorBase
Expand Down
4 changes: 2 additions & 2 deletions src/TeamUp.Common/Result/Extensions/ResultExtensions.Tap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static Result<TValue> Tap<TValue>(this Result<TValue> self, Action<TValue
if (self.IsFailure)
return self;

func(self.Value!);
func(self.Value);
return self;
}

Expand Down Expand Up @@ -35,7 +35,7 @@ public static async Task<Result<TValue>> TapAsync<TValue>(this Result<TValue> se
if (self.IsFailure)
return self;

await asyncFunc(self.Value!);
await asyncFunc(self.Value);
return self;
}

Expand Down
12 changes: 6 additions & 6 deletions src/TeamUp.Common/Result/Extensions/ResultExtensions.Then.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ public static Result<TOut> Then<TValue, TOut>(this Result<TValue> self, Func<TVa
if (self.IsFailure)
return self.Error;

return mapper(self.Value!);
return mapper(self.Value);
}

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

return mapper(self.Value!);
return mapper(self.Value);
}

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

return mapper(self.Value!);
return mapper(self.Value);
}

public static async Task<Result<TOut>> ThenAsync<TValue, TOut>(this Result<TValue> self, Func<TValue, Task<TOut>> asyncMapper)
{
if (self.IsFailure)
return self.Error;

return await asyncMapper(self.Value!);
return await asyncMapper(self.Value);
}

public static async Task<Result<TOut>> Then<TValue, TOut>(this Task<Result<TValue>> selfTask, Func<TValue, TOut> mapper)
Expand Down Expand Up @@ -67,7 +67,7 @@ public static async Task<Result<TOut>> ThenAsync<TFirst, TSecond, TOut>(this Res
if (self.IsFailure)
return self.Error;

return await asyncMapper(self.Value!.Item1, self.Value.Item2);
return await asyncMapper(self.Value.Item1, self.Value.Item2);
}

public static async Task<Result<TOut>> ThenAsync<TFirst, TSecond, TOut>(this Task<Result<(TFirst, TSecond)>> selfTask, Func<TFirst, TSecond, Task<TOut>> asyncMapper)
Expand All @@ -81,7 +81,7 @@ public static async Task<Result<TOut>> ThenAsync<TFirst, TSecond, TOut>(this Res
if (self.IsFailure)
return self.Error;

return await asyncMapper(self.Value!.Item1, self.Value.Item2);
return await asyncMapper(self.Value.Item1, self.Value.Item2);
}

public static async Task<Result<TOut>> ThenAsync<TFirst, TSecond, TOut>(this Task<Result<(TFirst, TSecond)>> selfTask, Func<TFirst, TSecond, Task<Result<TOut>>> asyncMapper)
Expand Down
2 changes: 1 addition & 1 deletion src/TeamUp.Common/Result/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public sealed class Result<TValue>
public bool IsFailure => !IsSuccess;

private readonly TValue? _value;
public TValue? Value => IsSuccess ? _value :
public TValue Value => IsSuccess ? _value! :
throw new InvalidOperationException("Value of failure result cannot be accessed.");

private readonly ErrorBase? _error = null;
Expand Down
19 changes: 17 additions & 2 deletions tests/TeamUp.EndToEndTests/DataGenerators/TeamGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ public sealed class TeamGenerator : BaseGenerator

public static readonly Faker<Team> EmptyTeam = new Faker<Team>(binder: TeamBinder)
.UsePrivateConstructor()
.RuleFor(t => t.Id, f => TeamId.FromGuid(f.Random.Uuid()))
.RuleFor(t => t.Id, f => TeamId.FromGuid(f.Random.Guid()))
.RuleFor(t => t.Name, GenerateValidTeamName());

public static readonly Faker<TeamMember> EmptyTeamMember = new Faker<TeamMember>(binder: TeamMemberBinder)
.UsePrivateConstructor()
.RuleFor(tm => tm.Id, f => TeamMemberId.FromGuid(f.Random.Uuid()));
.RuleFor(tm => tm.Id, f => TeamMemberId.FromGuid(f.Random.Guid()));

public static readonly Faker<UpsertEventTypeRequest> ValidUpsertEventTypeRequest = new Faker<UpsertEventTypeRequest>()
.RuleFor(r => r.Name, f => f.Random.AlphaNumeric(10))
Expand Down Expand Up @@ -54,6 +54,21 @@ public static Team GenerateTeamWith(User user, TeamRole role, List<User> members
};
}

public static Team GenerateTeamWith(
User user1, TeamRole role1,
User user2, TeamRole role2,
List<User> members)
{
var hasOwner = (role1 != TeamRole.Owner && role2 != TeamRole.Owner && members.Count == 0);
hasOwner.Should().Be(false, "team has to have exactly 1 owner");

return (role1, role2) switch
{
(TeamRole.Owner, _) or (_, TeamRole.Owner) => GenerateTeamWith(members, (user1, role1), (user2, role2)),
_ => GenerateTeamWith(members[1..], (members.First(), TeamRole.Owner), (user1, role1), (user2, role2))
};
}

public static Team GenerateTeamWith(User owner, List<User> members, params (User User, TeamRole Role)[] userMembers)
=> GenerateTeamWith(members, userMembers.Concat([(owner, TeamRole.Owner)]).ToArray());

Expand Down
4 changes: 4 additions & 0 deletions tests/TeamUp.EndToEndTests/DataGenerators/UserGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public sealed class UserGenerator : BaseGenerator
.RuleFor(u => u.Password, new Password())
.RuleFor(u => u.Status, UserStatus.Activated);

public static readonly Faker<User> NotActivatedUser = EmptyUser
.RuleFor(u => u.Password, new Password())
.RuleFor(u => u.Status, UserStatus.NotActivated);

public static User GenerateUser(Password password, UserStatus status)
{
return EmptyUser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ public InviteUserTests(TeamApiWebApplicationFactory appFactory) : base(appFactor
[InlineData(TeamRole.Coordinator)]
[InlineData(TeamRole.Admin)]
[InlineData(TeamRole.Owner)]
public async Task InviteUser_ThatIsActivated_AsCoordinatorOrHigher_Should_CreateInvitationInDatabase_And_SendInvitationEmail(TeamRole teamRole)
public async Task InviteUser_ThatIsActivated_AsCoordinatorOrHigher_Should_CreateInvitationInDatabase_And_SendInvitationEmail(TeamRole initiatorRole)
{
//arrange
var initiatorUser = UserGenerator.ActivatedUser.Generate();
var targetUser = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, initiatorRole, members);

await UseDbContextAsync(dbContext =>
{
Expand Down Expand Up @@ -57,12 +57,12 @@ await UseDbContextAsync(dbContext =>
[InlineData(TeamRole.Coordinator)]
[InlineData(TeamRole.Admin)]
[InlineData(TeamRole.Owner)]
public async Task InviteUser_ThatIsNotRegistered_AsCoordinatorOrHigher_Should_CreateInvitationInDatabase_And_GenerateNewUser_And_SendInvitationEmail(TeamRole teamRole)
public async Task InviteUser_ThatIsNotRegistered_AsCoordinatorOrHigher_Should_CreateInvitationInDatabase_And_GenerateNewUser_And_SendInvitationEmail(TeamRole initiatorRole)
{
//arrange
var initiatorUser = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, initiatorRole, members);

await UseDbContextAsync(dbContext =>
{
Expand Down Expand Up @@ -111,12 +111,12 @@ await UseDbContextAsync(async dbContext =>
[InlineData(TeamRole.Coordinator)]
[InlineData(TeamRole.Admin)]
[InlineData(TeamRole.Owner)]
public async Task InviteUser_ThatIsAlreadyInTeam_AsCoordinatorOrHigher_Should_ResultInBadRequest(TeamRole teamRole)
public async Task InviteUser_ThatIsAlreadyInTeam_AsCoordinatorOrHigher_Should_ResultInBadRequest_DomainError(TeamRole initiatorRole)
{
//arrange
var initiatorUser = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, initiatorRole, members);
var targetUser = members.First();

await UseDbContextAsync(dbContext =>
Expand Down Expand Up @@ -184,13 +184,13 @@ await UseDbContextAsync(dbContext =>
[InlineData(TeamRole.Coordinator)]
[InlineData(TeamRole.Admin)]
[InlineData(TeamRole.Owner)]
public async Task InviteUser_ThatIsAlreadyInvited_AsCoordinatorOrHigher_Should_ResultInConflict(TeamRole teamRole)
public async Task InviteUser_ThatIsAlreadyInvited_AsCoordinatorOrHigher_Should_ResultInConflict(TeamRole initiatorRole)
{
//arrange
var initiatorUser = UserGenerator.ActivatedUser.Generate();
var targetUser = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, initiatorRole, members);
var invitation = InvitationGenerator.GenerateInvitation(targetUser.Id, team.Id, DateTime.UtcNow);

await UseDbContextAsync(dbContext =>
Expand Down Expand Up @@ -289,7 +289,7 @@ await UseDbContextAsync(dbContext =>

[Theory]
[ClassData(typeof(InvitationGenerator.InvalidInviteUserRequest))]
public async Task InviteUser_WithInvalidRequest_Should_ResultInBadRequest(InvalidRequest<InviteUserRequest> request)
public async Task InviteUser_WithInvalidRequest_Should_ResultInBadRequest_ValidationErrors(InvalidRequest<InviteUserRequest> request)
{
//arrange
var initiatorUser = UserGenerator.ActivatedUser.Generate();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,78 +1,36 @@
using Microsoft.EntityFrameworkCore;

using TeamUp.Contracts.Teams;
using TeamUp.Domain.Aggregates.Teams;

namespace TeamUp.EndToEndTests.EndpointTests.Teams;

public sealed class ChangeNicknameTests : BaseTeamTests
{
public ChangeNicknameTests(TeamApiWebApplicationFactory appFactory) : base(appFactory) { }

[Fact]
public async Task ChangeNickname_ToValidNickname_AsOwner_Should_UpdateNicknameInDatabase()
{
//arrange
var owner = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(owner, members);

await UseDbContextAsync(dbContext =>
{
dbContext.Users.Add(owner);
dbContext.Users.AddRange(members);
dbContext.Teams.Add(team);
return dbContext.SaveChangesAsync();
});

Authenticate(owner);

var targetMemberId = team.Members.FirstOrDefault(member => member.UserId == owner.Id)!.Id;
var request = new ChangeNicknameRequest
{
Nickname = TeamGenerator.GenerateValidNickname()
};

//act
var response = await Client.PutAsJsonAsync($"/api/v1/teams/{team.Id.Value}/nickname", request);

//assert
response.Should().Be200Ok();

var member = await UseDbContextAsync(dbContext =>
{
return dbContext
.Set<TeamMember>()
.SingleOrDefaultAsync(member => member.Id == targetMemberId);
});

member.ShouldNotBeNull();
member.Nickname.Should().Be(request.Nickname);
}

[Theory]
[InlineData(TeamRole.Member)]
[InlineData(TeamRole.Coordinator)]
[InlineData(TeamRole.Admin)]
public async Task ChangeNickname_ToValidNickname_AsAdminOrLower_Should_UpdateNicknameInDatabase(TeamRole teamRole)
[InlineData(TeamRole.Owner)]
public async Task ChangeNickname_ToValidNickname_AsTeamMember_Should_UpdateNicknameInDatabase(TeamRole teamRole)
{
//arrange
var owner = UserGenerator.ActivatedUser.Generate();
var initiatorUser = UserGenerator.ActivatedUser.Generate();
var members = UserGenerator.ActivatedUser.Generate(18);
var team = TeamGenerator.GenerateTeamWith(owner, members, (initiatorUser, teamRole));
var members = UserGenerator.ActivatedUser.Generate(19);
var team = TeamGenerator.GenerateTeamWith(initiatorUser, teamRole, members);

await UseDbContextAsync(dbContext =>
{
dbContext.Users.AddRange([owner, initiatorUser]);
dbContext.Users.Add(initiatorUser);
dbContext.Users.AddRange(members);
dbContext.Teams.Add(team);
return dbContext.SaveChangesAsync();
});

Authenticate(initiatorUser);

var targetMemberId = team.Members.FirstOrDefault(member => member.UserId == initiatorUser.Id)!.Id;
var targetMemberId = team.Members.First(member => member.UserId == initiatorUser.Id).Id;
var request = new ChangeNicknameRequest
{
Nickname = TeamGenerator.GenerateValidNickname()
Expand Down Expand Up @@ -163,7 +121,7 @@ await UseDbContextAsync(dbContext =>
[InlineData("")]
[InlineData("x")]
[InlineData("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")]
public async Task ChangeNickname_ToInvalidName_Should_ResultInBadRequest(string invalidName)
public async Task ChangeNickname_ToInvalidName_Should_ResultInBadRequest_ValidationErrors(string invalidName)
{
//arrange
var owner = UserGenerator.ActivatedUser.Generate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ await UseDbContextAsync(dbContext =>
}

[Fact]
public async Task ChangeOwnership_WhenNotMember_Should_ResultInForbidden()
public async Task ChangeOwnership_WhenNotMemberOfTeam_Should_ResultInForbidden()
{
//arrange
var owner = UserGenerator.ActivatedUser.Generate();
Expand Down
Loading

0 comments on commit 9efa463

Please sign in to comment.