diff --git a/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerDataSourceTests.cs b/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerDataSourceTests.cs index 1de88350..7e435b42 100644 --- a/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerDataSourceTests.cs +++ b/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerDataSourceTests.cs @@ -80,8 +80,9 @@ public async Task Read_players_supports_no_filter() public async Task Read_players_supports_filter_by_player_id() { var playerDataSource = new SqlServerPlayerDataSource(_connectionFactory, _routeNormaliser.Object, _statisticsQueryBuilder.Object); - var expectedPlayers = _testData.Players.Where(x => x.PlayerId != _testData.BowlerWithMultipleIdentities!.PlayerId).Take(3).ToList(); - expectedPlayers.Add(_testData.BowlerWithMultipleIdentities!); + var playerWithMultipleIdentities = _testData.PlayersWithMultipleIdentities.First(); + var expectedPlayers = _testData.Players.Where(x => x.PlayerId != playerWithMultipleIdentities.PlayerId).Take(3).ToList(); + expectedPlayers.Add(playerWithMultipleIdentities); var results = await playerDataSource.ReadPlayers(new PlayerFilter { PlayerIds = expectedPlayers.Select(x => x.PlayerId!.Value).ToList() }); @@ -236,7 +237,7 @@ public async Task Read_players_supports_exclude_by_player_identity_id() var players = _testData.PlayersWhoHavePlayedAtLeastOneMatch().ToList(); var playerDataSource = new SqlServerPlayerDataSource(_connectionFactory, _routeNormaliser.Object, _statisticsQueryBuilder.Object); var playerWithOneIdentity = players.First(x => x.PlayerIdentities.Count == 1); - var playerWithOtherIdentities = _testData.BowlerWithMultipleIdentities!; + var playerWithOtherIdentities = _testData.PlayersWithMultipleIdentities.First(); var identityToExcludeOfSeveral = playerWithOtherIdentities.PlayerIdentities.First().PlayerIdentityId!.Value; var results = await playerDataSource.ReadPlayers( @@ -498,7 +499,7 @@ private async Task Read_player_identities_supports_filter_by_involvement_in_a_se public async Task Read_player_identities_supports_filter_by_player_identity_id() { var playerDataSource = new SqlServerPlayerDataSource(_connectionFactory, _routeNormaliser.Object, _statisticsQueryBuilder.Object); - var identities = _testData.BowlerWithMultipleIdentities!.PlayerIdentities; + var identities = _testData.PlayersWithMultipleIdentities.First().PlayerIdentities; var results = await playerDataSource.ReadPlayerIdentities(new PlayerFilter { PlayerIdentityIds = identities.Select(x => x.PlayerIdentityId!.Value).ToList() }); @@ -727,11 +728,12 @@ public async Task Read_player_by_route_returns_all_identities_when_statistics_ar public async Task ReadPlayerByMemberKey_returns_PlayerRoute_for_matching_player() { var playerDataSource = new SqlServerPlayerDataSource(_connectionFactory, _routeNormaliser.Object, _statisticsQueryBuilder.Object); + var player = _testData.PlayersWithMultipleIdentities.First(p => p.MemberKey.HasValue); - var result = await playerDataSource.ReadPlayerByMemberKey(_testData.BowlerWithMultipleIdentities!.MemberKey!.Value); + var result = await playerDataSource.ReadPlayerByMemberKey(player.MemberKey!.Value); Assert.NotNull(result); - Assert.Equal(_testData.BowlerWithMultipleIdentities.PlayerRoute, result!.PlayerRoute); + Assert.Equal(player.PlayerRoute, result!.PlayerRoute); } [Fact] diff --git a/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerRepositoryTests.cs b/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerRepositoryTests.cs index 8e90965f..12591327 100644 --- a/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerRepositoryTests.cs +++ b/Stoolball.Data.SqlServer.IntegrationTests/Statistics/SqlServerPlayerRepositoryTests.cs @@ -8,6 +8,7 @@ using Moq; using Newtonsoft.Json; using Stoolball.Data.Abstractions; +using Stoolball.Data.Abstractions.Models; using Stoolball.Data.SqlServer.IntegrationTests.Fixtures; using Stoolball.Data.SqlServer.IntegrationTests.Redirects; using Stoolball.Logging; @@ -70,11 +71,11 @@ private PlayerIdentity SetupCopyOfPlayerIdentity(PlayerIdentity playerIdentityTo Player = new Player { PlayerId = playerIdentityToUpdate.Player!.PlayerId, - PlayerIdentities = playerIdentityToUpdate.Player.PlayerIdentities.Select(x => new PlayerIdentity + PlayerIdentities = new PlayerIdentityList(playerIdentityToUpdate.Player.PlayerIdentities.Select(x => new PlayerIdentity { PlayerIdentityId = x.PlayerIdentityId, PlayerIdentityName = x.PlayerIdentityName - }).ToList() + })) } }; @@ -288,22 +289,22 @@ public async Task LinkPlayerIdentity_throws_InvalidOperationException_if_target_ var repo = CreateRepository(); if (isCurrentMember) { - var player1 = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.MemberKey is null && p2.IsOnTheSameTeamAs(p))); - var player2 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.IsOnTheSameTeamAs(player1)); - var currentMember = player1.MemberKey!.Value; - SetupMocksForLinkPlayerIdentity(player1, player2); + var targetPlayer = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.MemberKey is null && p2.IsOnTheSameTeamAs(p))); + var playerWithIdentityToLink = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.IsOnTheSameTeamAs(targetPlayer)); + var currentMember = targetPlayer.MemberKey!.Value; + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); - var exception = await Record.ExceptionAsync(async () => await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember, "Member name")); + var exception = await Record.ExceptionAsync(async () => await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember, "Member name")); Assert.Null(exception); } else { var currentMember = _testData.AnyMemberLinkedToPlayer(); - var player1 = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey != currentMember.memberKey && _testData.Players.Any(p2 => p2.MemberKey is null && p2.IsOnTheSameTeamAs(p))); - var player2 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.IsOnTheSameTeamAs(player1)); - SetupMocksForLinkPlayerIdentity(player1, player2); + var targetPlayer = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey != currentMember.memberKey && _testData.Players.Any(p2 => p2.MemberKey is null && p2.IsOnTheSameTeamAs(p))); + var playerWithIdentityToLink = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.IsOnTheSameTeamAs(targetPlayer)); + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); - await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember.memberKey, currentMember.memberName)); + await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember.memberKey, currentMember.memberName)); } } @@ -316,35 +317,45 @@ public async Task LinkPlayerIdentity_throws_InvalidOperationException_if_identit var repo = CreateRepository(); if (isCurrentMember) { - var player1 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.MemberKey is not null && p2.PlayerIdentities.Count == 1 && p2.IsOnTheSameTeamAs(p))); - var player2 = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey is not null && p.IsOnTheSameTeamAs(player1)); - var currentMember = player2.MemberKey!.Value; - SetupMocksForLinkPlayerIdentity(player1, player2); - - var exception = await Record.ExceptionAsync(async () => await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember, "Member name")); + var targetPlayer = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.PlayerIdentities[0].LinkedBy == PlayerIdentityLinkedBy.DefaultIdentity && + _testData.Players.Any(p2 => p2.MemberKey is not null && + p2.PlayerIdentities.Count == 1 && + p2.PlayerIdentities[0].LinkedBy == PlayerIdentityLinkedBy.Member && + p2.IsOnTheSameTeamAs(p))); + var playerWithIdentityToLink = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey is not null && + p.IsOnTheSameTeamAs(targetPlayer) && + p.PlayerIdentities[0].LinkedBy == PlayerIdentityLinkedBy.Member + ); + var currentMember = playerWithIdentityToLink.MemberKey!.Value; + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); + + var exception = await Record.ExceptionAsync(async () => await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember, "Member name")); Assert.Null(exception); } else { var currentMember = _testData.AnyMemberLinkedToPlayer(); - var player1 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.MemberKey.HasValue && p2.MemberKey != currentMember.memberKey && p2.PlayerIdentities.Count == 1 && p2.IsOnTheSameTeamAs(p))); ; - var player2 = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey != currentMember.memberKey && p.IsOnTheSameTeamAs(player1)); - SetupMocksForLinkPlayerIdentity(player1, player2); + var targetPlayer = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.MemberKey.HasValue && p2.MemberKey != currentMember.memberKey && p2.PlayerIdentities.Count == 1 && p2.IsOnTheSameTeamAs(p))); ; + var playerWithIdentityToLink = _testData.AnyPlayerLinkedToMemberWithOnlyOneIdentity(p => p.MemberKey != currentMember.memberKey && p.IsOnTheSameTeamAs(targetPlayer)); + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); - await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember.memberKey, currentMember.memberName)); + await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, currentMember.memberKey, currentMember.memberName)); } } [Fact] public async Task LinkPlayerIdentity_throws_InvalidOperationException_if_identity_to_link_is_linked_to_other_identities() { - var player1 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(); - var player2 = _testData.AnyPlayerNotLinkedToMemberWithMultipleIdentities(p => p.IsOnTheSameTeamAs(player1) && - p.PlayerIdentities.Count == p.PlayerIdentities.Where(x => x.LinkedBy == PlayerIdentityLinkedBy.Team).Count()); - SetupMocksForLinkPlayerIdentity(player1, player2); + var targetPlayer = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.IsOnTheSameTeamAs(p) && + !p2.MemberKey.HasValue && + p2.PlayerIdentities.Count > 1 && + p2.PlayerIdentities.Count == p2.PlayerIdentities.Where(x => x.LinkedBy == PlayerIdentityLinkedBy.Team).Count())); + var playerWithIdentityToLink = _testData.AnyPlayerNotLinkedToMemberWithMultipleIdentities(p => p.IsOnTheSameTeamAs(targetPlayer) && + p.PlayerIdentities.Count == p.PlayerIdentities.Where(x => x.LinkedBy == PlayerIdentityLinkedBy.Team).Count()); + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); var repo = CreateRepository(); - await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, Guid.NewGuid(), "Member name")); + await Assert.ThrowsAsync(async () => await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, Guid.NewGuid(), "Member name")); } [Fact] @@ -359,63 +370,88 @@ public async Task LinkPlayerIdentity_throws_InvalidOperationException_if_target_ } [Fact] - public async Task LinkPlayerIdentity_merges_players() + public async Task LinkPlayerIdentity_merges_two_default_identities() { - var player1 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(); - var player2 = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.PlayerId != player1.PlayerId && p.IsOnTheSameTeamAs(player1)); + var targetPlayer = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(); + var playerWithIdentityToLink = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => p.PlayerId != targetPlayer.PlayerId && p.IsOnTheSameTeamAs(targetPlayer)); - SetupMocksForLinkPlayerIdentity(player1, player2); + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); var member = _testData.AnyMemberNotLinkedToPlayer(); var repo = CreateRepository(); - var movedIdentityResult = await repo.LinkPlayerIdentity(player1.PlayerId!.Value, player2.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, member.memberKey, member.memberName); + var movedIdentityResult = await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, member.memberKey, member.memberName); await repo.ProcessAsyncUpdatesForPlayers(); - Assert.Equal(player1.PlayerId, movedIdentityResult.PlayerIdForTargetPlayer); - Assert.Equal(player1.PlayerRoute, movedIdentityResult.PreviousRouteForTargetPlayer); - Assert.Equal(player1.MemberKey, movedIdentityResult.MemberKeyForTargetPlayer); + await AssertMergedPlayers(targetPlayer, playerWithIdentityToLink, movedIdentityResult); + } - Assert.Equal(player2.PlayerId, movedIdentityResult.PlayerIdForSourcePlayer); - Assert.Equal(player2.PlayerRoute, movedIdentityResult.PreviousRouteForSourcePlayer); - Assert.Equal(player2.MemberKey, movedIdentityResult.MemberKeyForSourcePlayer); + private async Task AssertMergedPlayers(Player targetPlayer, Player playerWithIdentityToLink, MovedPlayerIdentity movedIdentityResult) + { + Assert.Equal(targetPlayer.PlayerId, movedIdentityResult.PlayerIdForTargetPlayer); + Assert.Equal(targetPlayer.PlayerRoute, movedIdentityResult.PreviousRouteForTargetPlayer); + Assert.Equal(targetPlayer.MemberKey, movedIdentityResult.MemberKeyForTargetPlayer); - Assert.Equal(player2.PlayerRoute, movedIdentityResult.NewRouteForTargetPlayer); + Assert.Equal(playerWithIdentityToLink.PlayerId, movedIdentityResult.PlayerIdForSourcePlayer); + Assert.Equal(playerWithIdentityToLink.PlayerRoute, movedIdentityResult.PreviousRouteForSourcePlayer); + Assert.Equal(playerWithIdentityToLink.MemberKey, movedIdentityResult.MemberKeyForSourcePlayer); + + Assert.Equal(playerWithIdentityToLink.PlayerRoute, movedIdentityResult.NewRouteForTargetPlayer); using (var connectionForAssert = _connectionFactory.CreateDatabaseConnection()) { connectionForAssert.Open(); - var player1Updated = await connectionForAssert.QuerySingleAsync<(string PlayerRoute, Guid? MemberKey)>($"SELECT PlayerRoute, MemberKey FROM {Tables.Player} WHERE PlayerId = @PlayerId", player1); - Assert.Equal(player2.PlayerRoute, player1Updated.PlayerRoute); - Assert.Null(player1Updated.MemberKey); + var targetPlayerUpdated = await connectionForAssert.QuerySingleAsync<(string PlayerRoute, Guid? MemberKey)>($"SELECT PlayerRoute, MemberKey FROM {Tables.Player} WHERE PlayerId = @PlayerId", targetPlayer); + Assert.Equal(playerWithIdentityToLink.PlayerRoute, targetPlayerUpdated.PlayerRoute); + Assert.Null(targetPlayerUpdated.MemberKey); - var expectedIdentities = player1.PlayerIdentities.Select(pi => pi).ToList(); - expectedIdentities.Add(player2.PlayerIdentities[0]); + var expectedIdentities = targetPlayer.PlayerIdentities.Select(pi => pi).ToList(); + expectedIdentities.Add(playerWithIdentityToLink.PlayerIdentities[0]); foreach (var identity in expectedIdentities) { var playerIdentity = await connectionForAssert.QuerySingleAsync<(Guid PlayerId, string LinkedBy)>($"SELECT PlayerId, LinkedBy FROM {Tables.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId", identity); - Assert.Equal(player1.PlayerId, playerIdentity.PlayerId); + Assert.Equal(targetPlayer.PlayerId, playerIdentity.PlayerId); Assert.Equal(PlayerIdentityLinkedBy.Team.ToString(), playerIdentity.LinkedBy); var playerIdsInStatistics = await connectionForAssert.QueryAsync($"SELECT PlayerId FROM {Tables.PlayerInMatchStatistics} WHERE PlayerIdentityId = @PlayerIdentityId", identity); foreach (var playerIdInStatistics in playerIdsInStatistics) { - Assert.Equal(player1.PlayerId, playerIdInStatistics); + Assert.Equal(targetPlayer.PlayerId, playerIdInStatistics); } - var playerRoutesInStatistics = await connectionForAssert.QueryAsync($"SELECT PlayerRoute FROM {Tables.PlayerInMatchStatistics} WHERE PlayerId = @PlayerId", player1); + var playerRoutesInStatistics = await connectionForAssert.QueryAsync($"SELECT PlayerRoute FROM {Tables.PlayerInMatchStatistics} WHERE PlayerId = @PlayerId", targetPlayer); foreach (var route in playerRoutesInStatistics) { - Assert.Equal(player2.PlayerRoute, route); + Assert.Equal(playerWithIdentityToLink.PlayerRoute, route); } } - var obsoletePlayerShouldBeRemoved = await connectionForAssert.QuerySingleOrDefaultAsync($"SELECT COUNT(PlayerId) FROM {Tables.Player} WHERE PlayerId = @PlayerId", player2); + var obsoletePlayerShouldBeRemoved = await connectionForAssert.QuerySingleOrDefaultAsync($"SELECT COUNT(PlayerId) FROM {Tables.Player} WHERE PlayerId = @PlayerId", playerWithIdentityToLink); Assert.Equal(0, obsoletePlayerShouldBeRemoved); } } + [Fact] + public async Task LinkPlayerIdentity_merges_players_if_target_is_linked_to_other_identities_on_the_same_team() + { + var playerWithIdentityToLink = _testData.AnyPlayerNotLinkedToMemberWithOnlyOneIdentity(p => _testData.Players.Any(p2 => p2.IsOnTheSameTeamAs(p) && + !p2.MemberKey.HasValue && + p2.PlayerIdentities.Count > 1 && + p2.PlayerIdentities.Count == p2.PlayerIdentities.Where(x => x.LinkedBy == PlayerIdentityLinkedBy.Team).Count() + )); + var targetPlayer = _testData.AnyPlayerNotLinkedToMemberWithMultipleIdentities(p => p.IsOnTheSameTeamAs(playerWithIdentityToLink) && + p.PlayerIdentities.Count == p.PlayerIdentities.Where(x => x.LinkedBy == PlayerIdentityLinkedBy.Team).Count()); + SetupMocksForLinkPlayerIdentity(targetPlayer, playerWithIdentityToLink); + + var repo = CreateRepository(); + var movedIdentityResult = await repo.LinkPlayerIdentity(targetPlayer.PlayerId!.Value, playerWithIdentityToLink.PlayerIdentities[0].PlayerIdentityId!.Value, PlayerIdentityLinkedBy.Team, Guid.NewGuid(), "Member name"); + await repo.ProcessAsyncUpdatesForPlayers(); + + await AssertMergedPlayers(targetPlayer, playerWithIdentityToLink, movedIdentityResult); + } + + [Fact] public async Task LinkPlayerIdentity_redirects_deleted_player_to_linked_player() { @@ -475,7 +511,8 @@ public async Task UnlinkPlayerIdentity_throws_InvalidOperationException_for_last [Fact] public async Task UnlinkPlayerIdentity_for_penultimate_identity_moves_identity_to_new_player_including_statistics() { - var player = _testData.AnyPlayerNotLinkedToMemberWithMultipleIdentities(p => p.PlayerIdentities.Count == 2); + var player = _testData.AnyPlayerNotLinkedToMemberWithMultipleIdentities(p => p.PlayerIdentities.Count == 2 && + p.PlayerIdentities.Count(pi => pi.LinkedBy == PlayerIdentityLinkedBy.Team) == 2); var identityToKeep = player.PlayerIdentities[0]; var identityToUnlink = player.PlayerIdentities[1]; @@ -491,8 +528,9 @@ public async Task UnlinkPlayerIdentity_for_penultimate_identity_moves_identity_t { connectionForAssert.Open(); - var originalPlayerStillLinked = await connectionForAssert.ExecuteScalarAsync($"SELECT PlayerId FROM {Tables.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId", identityToKeep).ConfigureAwait(false); - Assert.Equal(player.PlayerId, originalPlayerStillLinked); + var originalPlayerStillLinked = await connectionForAssert.QuerySingleAsync<(Guid PlayerId, PlayerIdentityLinkedBy LinkedBy)>($"SELECT PlayerId, LinkedBy FROM {Tables.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId", identityToKeep).ConfigureAwait(false); + Assert.Equal(player.PlayerId, originalPlayerStillLinked.PlayerId); + Assert.Equal(PlayerIdentityLinkedBy.DefaultIdentity, originalPlayerStillLinked.LinkedBy); var newPlayerLinkedToIdentity = await connectionForAssert.QuerySingleAsync<(Guid? playerId, string route, PlayerIdentityLinkedBy linkedBy)>( $@"SELECT p.PlayerId, p.PlayerRoute, p.LinkedBy diff --git a/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac b/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac index 1ef634a6..684218a8 100644 Binary files a/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac and b/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac differ diff --git a/Stoolball.Data.SqlServer.IntegrationTests/StoolballStatisticsMaxResultsDataSourceIntegrationTests.dacpac b/Stoolball.Data.SqlServer.IntegrationTests/StoolballStatisticsMaxResultsDataSourceIntegrationTests.dacpac index 572705bf..df76e3af 100644 Binary files a/Stoolball.Data.SqlServer.IntegrationTests/StoolballStatisticsMaxResultsDataSourceIntegrationTests.dacpac and b/Stoolball.Data.SqlServer.IntegrationTests/StoolballStatisticsMaxResultsDataSourceIntegrationTests.dacpac differ diff --git a/Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs b/Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs index 46a932cf..4486f903 100644 --- a/Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs +++ b/Stoolball.Data.SqlServer/SqlServerPlayerDataSource.cs @@ -77,7 +77,7 @@ public async Task> ReadPlayers(PlayerFilter? filter, IDbConnection return rawResults.GroupBy(x => x.PlayerId).Select(group => { var player = group.First(); - player.PlayerIdentities = group.Select(x => x.PlayerIdentities.Single()).OfType().ToList(); + player.PlayerIdentities = new PlayerIdentityList(group.Select(x => x.PlayerIdentities.Single()).OfType()); return player; }).ToList(); } @@ -116,7 +116,7 @@ public async Task> ReadPlayerIdentities(PlayerFilter? filte // Populate the PlayerIdentities collections of the players with the data that we have foreach (var identity in identities) { - identity.Player!.PlayerIdentities = identities.Where(x => x.Player?.PlayerId == identity.Player.PlayerId).ToList(); + identity.Player!.PlayerIdentities = new PlayerIdentityList(identities.Where(x => x.Player?.PlayerId == identity.Player.PlayerId)); } return identities; @@ -243,7 +243,7 @@ WHERE LOWER(PlayerRoute) = @Route var playerToReturn = playerData.GroupBy(x => x.PlayerId).Select(group => { var player = group.First(); - player.PlayerIdentities = group.Select(x => x.PlayerIdentities.Single()).OfType().ToList(); + player.PlayerIdentities = new PlayerIdentityList(group.Select(x => x.PlayerIdentities.Single()).OfType()); return player; }).FirstOrDefault(); diff --git a/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs b/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs index d20db60e..cff25821 100644 --- a/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs +++ b/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs @@ -313,11 +313,11 @@ public async Task LinkPlayerIdentity(Guid targetPlayer, Gui connection.Open(); using (var transaction = connection.BeginTransaction()) { - var targetPlayerBefore = await connection.QuerySingleAsync<(string PlayerRoute, Guid? MemberKey)>($"SELECT PlayerRoute, MemberKey FROM {Tables.Player} WHERE PlayerId = @PlayerId", new Player { PlayerId = targetPlayer }, transaction); + var targetPlayerBefore = await connection.QuerySingleAsync<(string PlayerRoute, Guid? MemberKey)>($"SELECT PlayerRoute, MemberKey FROM {Tables.Player} WHERE PlayerId = @PlayerId", new Player { PlayerId = targetPlayer }, transaction).ConfigureAwait(false); var identityToLinkBefore = await connection.QuerySingleAsync<(Guid PlayerId, string PlayerRoute, Guid? MemberKey, int Identities, string PlayerIdentityName, Guid TeamId)>( @$"SELECT p.PlayerId, p.PlayerRoute, p.MemberKey, (SELECT COUNT(*) FROM {Tables.PlayerIdentity} WHERE PlayerId = p.PlayerId) AS Identities, pi.PlayerIdentityName, pi.TeamId FROM {Tables.PlayerIdentity} pi INNER JOIN {Tables.Player} p ON pi.PlayerId = p.PlayerId - WHERE pi.PlayerIdentityId = @PlayerIdentityId", new { PlayerIdentityId = identityToLinkToTarget }, transaction); + WHERE pi.PlayerIdentityId = @PlayerIdentityId", new { PlayerIdentityId = identityToLinkToTarget }, transaction).ConfigureAwait(false); // Are the players already linked to each other? If so, abort. if (targetPlayer == identityToLinkBefore.PlayerId) @@ -353,7 +353,7 @@ public async Task LinkPlayerIdentity(Guid targetPlayer, Gui // Does the target player have an identity on the same team as the identity to link? If not, abort. var targetPlayerExistingIdentities = await connection.QueryAsync<(Guid PlayerIdentityId, string PlayerIdentityName, Guid TeamId)>( $"SELECT PlayerIdentityId, PlayerIdentityName, TeamId FROM {Tables.PlayerIdentity} WHERE PlayerId = @PlayerId", - new { PlayerId = targetPlayer }, transaction); + new { PlayerId = targetPlayer }, transaction).ConfigureAwait(false); var targetPlayerHasIdentityOnSameTeam = targetPlayerExistingIdentities.Any(id => id.TeamId == identityToLinkBefore.TeamId); if (!targetPlayerHasIdentityOnSameTeam) @@ -367,19 +367,23 @@ public async Task LinkPlayerIdentity(Guid targetPlayer, Gui // Move the player identities from the identity to link's current player id to the target player's id. if (bestRoute != targetPlayerBefore.PlayerRoute) { - await connection.ExecuteAsync($"UPDATE {Tables.Player} SET PlayerRoute = @PlayerRoute WHERE PlayerId = @PlayerId", new { PlayerRoute = bestRoute, PlayerId = targetPlayer }, transaction); + _ = await connection.ExecuteAsync($"UPDATE {Tables.Player} SET PlayerRoute = @PlayerRoute WHERE PlayerId = @PlayerId", new { PlayerRoute = bestRoute, PlayerId = targetPlayer }, transaction).ConfigureAwait(false); } var movePlayerIdentity = new { LinkedBy = linkedBy.ToString(), PlayerId = targetPlayer, PlayerIdentityId = identityToLinkToTarget }; - await connection.ExecuteAsync($"UPDATE {Tables.PlayerIdentity} SET LinkedBy = @LinkedBy, PlayerId = @PlayerId WHERE PlayerIdentityId = @PlayerIdentityId", movePlayerIdentity, transaction); + _ = await connection.ExecuteAsync($"UPDATE {Tables.PlayerIdentity} SET LinkedBy = @LinkedBy, PlayerId = @PlayerId WHERE PlayerIdentityId = @PlayerIdentityId", movePlayerIdentity, transaction).ConfigureAwait(false); + + // If the target player has and identities with LinkedBy = DefaultIdentity, they should now be linked by this activity + _ = await connection.ExecuteAsync($"UPDATE {Tables.PlayerIdentity} SET LinkedBy = @LinkedBy WHERE PlayerId = @PlayerId AND LinkedBy = '{PlayerIdentityLinkedBy.DefaultIdentity.ToString()}'", + new { LinkedBy = linkedBy.ToString(), PlayerId = targetPlayer }, transaction).ConfigureAwait(false); // We also need to update statistics, and delete the now-unused player that the identity has been moved away from. // However this is done asynchronously by ProcessAsyncUpdatesForPlayers, so we just need to mark the player as safe to delete. - await connection.ExecuteAsync($"UPDATE {Tables.Player} SET Deleted = 1 WHERE PlayerId = @PlayerId", new { identityToLinkBefore.PlayerId }, transaction); + _ = await connection.ExecuteAsync($"UPDATE {Tables.Player} SET Deleted = 1 WHERE PlayerId = @PlayerId", new { identityToLinkBefore.PlayerId }, transaction).ConfigureAwait(false); var deletedPlayer = new Player { PlayerId = identityToLinkBefore.PlayerId, PlayerRoute = identityToLinkBefore.PlayerRoute }; deletedPlayer.PlayerIdentities.Add(new PlayerIdentity { PlayerIdentityId = identityToLinkToTarget }); var serialisedDeletedPlayer = JsonConvert.SerializeObject(deletedPlayer); - await _auditRepository.CreateAudit(new AuditRecord + _ = await _auditRepository.CreateAudit(new AuditRecord { Action = AuditAction.Delete, MemberKey = memberKey, @@ -388,7 +392,7 @@ await _auditRepository.CreateAudit(new AuditRecord State = serialisedDeletedPlayer, RedactedState = serialisedDeletedPlayer, AuditDate = DateTime.UtcNow - }, transaction); + }, transaction).ConfigureAwait(false); _logger.Info(LoggingTemplates.Deleted, serialisedDeletedPlayer, memberName, memberKey, GetType(), nameof(LinkPlayerIdentity)); @@ -399,7 +403,7 @@ await _auditRepository.CreateAudit(new AuditRecord updatedTargetPlayer.PlayerIdentities.Add(reassignedPlayerIdentity); var serialisedUpdatedPlayer = JsonConvert.SerializeObject(updatedTargetPlayer); - await _auditRepository.CreateAudit(new AuditRecord + _ = await _auditRepository.CreateAudit(new AuditRecord { Action = AuditAction.Update, MemberKey = memberKey, @@ -408,7 +412,7 @@ await _auditRepository.CreateAudit(new AuditRecord State = serialisedUpdatedPlayer, RedactedState = serialisedUpdatedPlayer, AuditDate = DateTime.UtcNow - }, transaction); + }, transaction).ConfigureAwait(false); _logger.Info(LoggingTemplates.Updated, serialisedUpdatedPlayer, memberName, memberKey, GetType(), nameof(LinkPlayerIdentity)); @@ -560,19 +564,30 @@ public async Task UnlinkPlayerIdentity(Guid identityToUnlink, Guid memberKey, st connection.Open(); using (var transaction = connection.BeginTransaction()) { - var (totalIdentitiesLinkedToPlayer, playerId, playerIdentityName) = await connection.QuerySingleAsync<(int totalIdentitiesLinkedToMember, Guid playerId, string playerIdentityName)>( - $@"SELECT COUNT(*), PlayerId, (SELECT PlayerIdentityName FROM {Views.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId) AS PlayerIdentityName + var identitiesForPlayer = (await connection.QueryAsync<(Guid PlayerId, Guid PlayerIdentityId, string Name, PlayerIdentityLinkedBy LinkedBy)>( + $@"SELECT PlayerId, PlayerIdentityId, PlayerIdentityName, LinkedBy FROM {Views.PlayerIdentity} - WHERE PlayerId = (SELECT PlayerId FROM {Views.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId) - GROUP BY PlayerId", new { PlayerIdentityId = identityToUnlink }, transaction).ConfigureAwait(false); + WHERE PlayerId = (SELECT PlayerId FROM {Views.PlayerIdentity} WHERE PlayerIdentityId = @PlayerIdentityId)", + new { PlayerIdentityId = identityToUnlink }, transaction).ConfigureAwait(false)).ToList(); - if (totalIdentitiesLinkedToPlayer == 1) + if (identitiesForPlayer.Count == 1) { throw new InvalidOperationException(); } else { - await MoveIdentityToNewPlayer(identityToUnlink, playerIdentityName, memberKey, memberName, transaction, nameof(UnlinkPlayerIdentity)).ConfigureAwait(false); + var identityToUnlinkName = identitiesForPlayer.Single(pi => pi.PlayerIdentityId == identityToUnlink).Name; + await MoveIdentityToNewPlayer(identityToUnlink, identityToUnlinkName, memberKey, memberName, transaction, nameof(UnlinkPlayerIdentity)).ConfigureAwait(false); + + var remainingIdentities = identitiesForPlayer.Where(pi => pi.PlayerIdentityId != identityToUnlink).ToList(); + if (remainingIdentities.Count == 1 && + remainingIdentities[0].LinkedBy == PlayerIdentityLinkedBy.Team || remainingIdentities[0].LinkedBy == PlayerIdentityLinkedBy.StoolballEngland) + { + _ = await connection.ExecuteAsync($"UPDATE {Tables.PlayerIdentity} SET LinkedBy = @LinkedBy WHERE PlayerIdentityId = @PlayerIdentityId", + new { LinkedBy = PlayerIdentityLinkedBy.DefaultIdentity.ToString(), remainingIdentities[0].PlayerIdentityId }, + transaction).ConfigureAwait(false); + + } } transaction.Commit(); diff --git a/Stoolball.Testing/PlayerDataProviders/PlayersLinkedToMembersProvider.cs b/Stoolball.Testing/PlayerDataProviders/PlayersLinkedToMembersProvider.cs index a415ef37..ef16851a 100644 --- a/Stoolball.Testing/PlayerDataProviders/PlayersLinkedToMembersProvider.cs +++ b/Stoolball.Testing/PlayerDataProviders/PlayersLinkedToMembersProvider.cs @@ -5,9 +5,8 @@ using Stoolball.Statistics; using Stoolball.Teams; using Stoolball.Testing.Fakers; -using Stoolball.Testing.PlayerDataProviders; -namespace Stoolball.Testing.TeamDataProviders +namespace Stoolball.Testing.PlayerDataProviders { internal class PlayersLinkedToMembersProvider(IFakerFactory teamFakerFactory, IFakerFactory playerFakerFactory, IFakerFactory playerIdentityFakerFactory) : BasePlayerDataProvider { @@ -44,15 +43,6 @@ internal override IEnumerable CreatePlayers(TestData readOnlyTestData) identity.LinkedBy = PlayerIdentityLinkedBy.Member; } - for (var i = 0; i < 2; i++) - { - players[i].PlayerIdentities.Add(identities[i]); - identities[i].Player = players[i]; - identities[i].Team = team; - identities[i].LinkedBy = PlayerIdentityLinkedBy.Member; - players[i].MemberKey = Guid.NewGuid(); - } - // another player on the same team, not linked to a member var playerWithoutMember = _playerFaker.Generate(1).Single(); playerWithoutMember.PlayerIdentities.Add(identities[2]); diff --git a/Stoolball.Testing/PlayerDataProviders/PlayersNotLinkedToMembersProvider.cs b/Stoolball.Testing/PlayerDataProviders/PlayersNotLinkedToMembersProvider.cs new file mode 100644 index 00000000..da3b8ba1 --- /dev/null +++ b/Stoolball.Testing/PlayerDataProviders/PlayersNotLinkedToMembersProvider.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using Bogus; +using Stoolball.Statistics; +using Stoolball.Teams; +using Stoolball.Testing.Fakers; + +namespace Stoolball.Testing.PlayerDataProviders +{ + internal class PlayersNotLinkedToMembersProvider(IFakerFactory teamFakerFactory, IFakerFactory playerFakerFactory, IFakerFactory playerIdentityFakerFactory) : BasePlayerDataProvider + { + private readonly Faker _teamFaker = teamFakerFactory.Create(); + private readonly Faker _playerFaker = playerFakerFactory.Create(); + private readonly Faker _playerIdentityFaker = playerIdentityFakerFactory.Create(); + + internal override IEnumerable CreatePlayers(TestData readOnlyTestData) + { + var team = _teamFaker.Generate(1).Single(); + + // player with a single identity + var playerWithSingleIdentity = _playerFaker.Generate(1).Single(); + playerWithSingleIdentity.PlayerIdentities.Add(_playerIdentityFaker.Generate(1).Single()); + playerWithSingleIdentity.PlayerIdentities[0].Player = playerWithSingleIdentity; + playerWithSingleIdentity.PlayerIdentities[0].Team = team; + playerWithSingleIdentity.PlayerIdentities[0].LinkedBy = PlayerIdentityLinkedBy.DefaultIdentity; + + // player with two identities both linked by team, on the same team, not linked to member + var playerWithTwoIdentitiesLinkedByTeam = _playerFaker.Generate(1).Single(); + playerWithTwoIdentitiesLinkedByTeam.PlayerIdentities.AddRange(_playerIdentityFaker.Generate(2)); + + foreach (var identity in playerWithTwoIdentitiesLinkedByTeam.PlayerIdentities) + { + identity.Player = playerWithTwoIdentitiesLinkedByTeam; + identity.Team = team; + identity.LinkedBy = PlayerIdentityLinkedBy.Team; + } + + return [playerWithSingleIdentity, playerWithTwoIdentitiesLinkedByTeam]; + } + } +} diff --git a/Stoolball.Testing/SeedDataGenerator.cs b/Stoolball.Testing/SeedDataGenerator.cs index d3b49d73..ee6a4cd4 100644 --- a/Stoolball.Testing/SeedDataGenerator.cs +++ b/Stoolball.Testing/SeedDataGenerator.cs @@ -16,7 +16,6 @@ using Stoolball.Testing.Fakers; using Stoolball.Testing.MatchDataProviders; using Stoolball.Testing.PlayerDataProviders; -using Stoolball.Testing.TeamDataProviders; namespace Stoolball.Testing { @@ -246,9 +245,9 @@ private Team CreateTeamWithFullDetails(string teamName, Faker teamFaker) foreach (var teamInSeason in team.Seasons) { teamInSeason.Team = team; - teamInSeason.Season.Teams.Add(teamInSeason); + teamInSeason.Season!.Teams.Add(teamInSeason); } - competition.Seasons.AddRange(team.Seasons.Select(x => x.Season)); + competition.Seasons.AddRange(team.Seasons.Select(x => x.Season)!); return team; } @@ -558,7 +557,7 @@ private Tournament CreateTournamentInThePastWithFullDetailsExceptMatches(Faker - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityId = Guid.NewGuid(), @@ -698,7 +697,7 @@ private Player CreatePlayer(string playerName, Team team) RouteSegment = playerName.Kebaberize(), Team = team } - } + ] }; } @@ -752,16 +751,6 @@ internal TestData GenerateTestData() var poolOfTeamsWithPlayers = GenerateTeams(teamFaker); - var playerProviders = new BasePlayerDataProvider[]{ - new PlayersLinkedToMembersProvider(_teamFakerFactory, _playerFakerFactory, _playerIdentityFakerFactory), - }; - var playersFromPlayerProviders = new List(); - foreach (var provider in playerProviders) - { - var playersFromProvider = provider.CreatePlayers(testData); - playersFromPlayerProviders.AddRange(playersFromProvider); - } - // Create a pool of competitions for (var i = 0; i < 10; i++) { @@ -812,10 +801,7 @@ internal TestData GenerateTestData() testData.MatchInThePastWithFullDetailsAndTournament = FindMatchInThePastWithFullDetailsAndTournament(testData); var membersFromMatchComments = testData.Matches.SelectMany(x => x.Comments).Select(x => (memberKey: x.MemberKey, memberName: x.MemberName ?? "No name")); - var membersFromPlayerProviders = playersFromPlayerProviders.Where(p => p.MemberKey is not null).Select(p => (memberKey: p.MemberKey!.Value, memberName: "No name")); - testData.Members = membersFromMatchComments - .Union(membersFromPlayerProviders) - .Distinct(new MemberEqualityComparer()).ToList(); + testData.Members = membersFromMatchComments.ToList(); testData.Tournaments.AddRange(testData.Matches.Where(x => x.Tournament != null && !testData.Tournaments.Select(t => t.TournamentId).Contains(x.Tournament.TournamentId)).Select(x => x.Tournament).OfType()); for (var i = 0; i < 10; i++) @@ -882,13 +868,11 @@ internal TestData GenerateTestData() teamWithMatchLocation.MatchLocations.Add(testData.MatchLocationForClub); testData.MatchLocationForClub.Teams.Add(teamWithMatchLocation); - var teamsFromPlayerProviders = playersFromPlayerProviders.SelectMany(p => p.PlayerIdentities).Where(pi => pi.Team is not null).Select(pi => pi.Team).OfType(); var teamsInMatches = testData.Matches.SelectMany(x => x.Teams).Select(x => x.Team).OfType(); var teamsInTournaments = testData.Tournaments.SelectMany(x => x.Teams).Select(x => x.Team).OfType(); var teamsInSeasons = testData.Competitions.SelectMany(x => x.Seasons).SelectMany(x => x.Teams).Select(x => x.Team).OfType(); var teamsAtMatchLocations = testData.MatchLocations.SelectMany(x => x.Teams); testData.Teams = poolOfTeamsWithPlayers.Select(x => x.team) - .Union(teamsFromPlayerProviders) .Union(teamsInMatches) .Union(teamsInTournaments) .Union(teamsInSeasons) @@ -978,11 +962,8 @@ internal TestData GenerateTestData() testData.SeasonWithFullDetails = testData.Seasons.First(x => x.Teams.Any() && x.PointsRules.Any() && x.PointsAdjustments.Any()); - var playerIdentitiesInMatches = testData.Matches.SelectMany(_playerIdentityFinder.PlayerIdentitiesInMatch); - var playerIdentitiesFromPlayerProviders = playersFromPlayerProviders.SelectMany(p => p.PlayerIdentities); - testData.PlayerIdentities = playerIdentitiesInMatches - .Union(playerIdentitiesFromPlayerProviders) - .Distinct(new PlayerIdentityEqualityComparer()).ToList(); + var playerIdentitiesInMatches = testData.Matches.SelectMany(_playerIdentityFinder.PlayerIdentitiesInMatch).Distinct(new PlayerIdentityEqualityComparer()); + testData.PlayerIdentities = playerIdentitiesInMatches.ToList(); testData.Players = testData.PlayerIdentities.Select(x => x.Player).OfType().Distinct(playerComparer).ToList(); var matchProviders = new BaseMatchDataProvider[]{ @@ -1055,29 +1036,26 @@ internal TestData GenerateTestData() testData.Teams.AddRange(testData.Schools.SelectMany(x => x.Teams)); testData.MatchLocations.AddRange(testData.Schools.SelectMany(x => x.Teams).SelectMany(x => x.MatchLocations).Distinct(new MatchLocationEqualityComparer())); - foreach (var team in testData.Teams.Where(t => t.Club == null || - t.Club.Teams.Count() == 1 || - (t.Club.Teams.Count(x => !x.UntilYear.HasValue) == 1 && !t.UntilYear.HasValue))) - { - testData.TeamListings.Add(team.ToTeamListing()); - } - foreach (var club in testData.Clubs.Where(c => c.Teams.Count == 0 || - c.Teams.Count(x => !x.UntilYear.HasValue) > 1)) - { - testData.TeamListings.Add(club.ToTeamListing()); - } + CreateImmutableTestData(testData); - // Prepare collections to avoid repeatedly querying to create the same collections - testData.Awards.AddRange(testData.Matches.SelectMany(m => m.Awards)); - testData.MatchInnings.AddRange(testData.Matches.SelectMany(x => x.MatchInnings)); - testData.BowlingFigures.AddRange(testData.MatchInnings.SelectMany(mi => mi.BowlingFigures)); + BuildCollections(testData); + + EnsureCyclicalRelationshipsArePopulated(testData); + + PopulateCalculatedProperties(testData); + + return testData; + } - // This must happen after ALL scorecards and awards are finalised + /// + /// Ensure that calculated properties are populated. Must not change any source data or relationships. + /// + /// + private void PopulateCalculatedProperties(TestData testData) + { + // The following steps must happen after ALL scorecards and awards are finalised foreach (var identity in testData.PlayerIdentities) { - // Ensure the cyclical relationship between players and identities is populated - if (identity.Player is not null && !identity.Player.PlayerIdentities.Contains(identity)) { identity.Player.PlayerIdentities.Add(identity); } - var matchesPlayedByThisIdentity = _matchFinder.MatchesPlayedByPlayerIdentity(testData.Matches, identity.PlayerIdentityId!.Value); identity.TotalMatches = matchesPlayedByThisIdentity.Select(x => x.MatchId).Distinct().Count(); if (identity.TotalMatches > 0) @@ -1086,8 +1064,96 @@ internal TestData GenerateTestData() identity.LastPlayed = matchesPlayedByThisIdentity.Max(x => x.StartTime); } } + } - return testData; + /// + /// Ensure that objects which have cyclical relationships have those relationships populated, in case test data providers did not populate them. + /// + /// + private static void EnsureCyclicalRelationshipsArePopulated(TestData testData) + { + foreach (var identity in testData.PlayerIdentities) + { + // Ensure the cyclical relationship between players and identities is populated + if (identity.Player is not null && !identity.Player.PlayerIdentities.Contains(identity)) { identity.Player.PlayerIdentities.Add(identity); } + } + } + + /// + /// Build collections from finalised test data to avoid repeatedly querying to create the same collection. + /// + /// + private static void BuildCollections(TestData testData) + { + // Add members created to support other objects + var membersFromPlayers = testData.Players.Where(p => p.MemberKey is not null).Select(p => (memberKey: p.MemberKey!.Value, memberName: "No name")); + testData.Members = testData.Members + .Union(membersFromPlayers, new MemberEqualityComparer()) + .ToList(); + + // Add player identities created to support other objects + var playerIdentitiesFromPlayers = testData.Players.SelectMany(p => p.PlayerIdentities); + testData.PlayerIdentities = testData.PlayerIdentities + .Union(playerIdentitiesFromPlayers, new PlayerIdentityEqualityComparer()) + .ToList(); + + // Add teams created to support other objects + var teamsFromPlayers = testData.Players.SelectMany(p => p.PlayerIdentities).Where(pi => pi.Team is not null).Select(pi => pi.Team).OfType(); + testData.Teams = testData.Teams + .Union(teamsFromPlayers, new TeamEqualityComparer()) + .ToList(); + + // Create collections from team data + testData.TeamListings = CreateTeamListingsFromClubsAndTeams(testData); + + // Create collections from player data + testData.PlayersWithMultipleIdentities = FindPlayersWithMultipleIdentities(testData); + + // Create collections from match data + testData.Awards.AddRange(testData.Matches.SelectMany(m => m.Awards)); + testData.MatchInnings.AddRange(testData.Matches.SelectMany(x => x.MatchInnings)); + testData.BowlingFigures.AddRange(testData.MatchInnings.SelectMany(mi => mi.BowlingFigures)); + } + + private static List CreateTeamListingsFromClubsAndTeams(TestData testData) + { + var teamListings = new List(); + foreach (var team in testData.Teams.Where(t => t.Club == null || + t.Club.Teams.Count() == 1 || + (t.Club.Teams.Count(x => !x.UntilYear.HasValue) == 1 && !t.UntilYear.HasValue))) + { + teamListings.Add(team.ToTeamListing()); + } + foreach (var club in testData.Clubs.Where(c => c.Teams.Count == 0 || + c.Teams.Count(x => !x.UntilYear.HasValue) > 1)) + { + teamListings.Add(club.ToTeamListing()); + } + return teamListings; + } + + /// + /// Create test data from providers, for specific scenarios which must not be altered in case they are no longer valid. + /// + /// + private void CreateImmutableTestData(TestData testData) + { + testData.Players.AddRange(CreateTestDataFromPlayerProviders(testData)); + } + + private List CreateTestDataFromPlayerProviders(TestData testData) + { + var playerProviders = new BasePlayerDataProvider[]{ + new PlayersLinkedToMembersProvider(_teamFakerFactory, _playerFakerFactory, _playerIdentityFakerFactory), + new PlayersNotLinkedToMembersProvider(_teamFakerFactory, _playerFakerFactory, _playerIdentityFakerFactory) + }; + var playersFromPlayerProviders = new List(); + foreach (var provider in playerProviders) + { + var playersFromProvider = provider.CreatePlayers(testData); + playersFromPlayerProviders.AddRange(playersFromProvider); + } + return playersFromPlayerProviders; } private static List FindPlayersWithMultipleIdentities(TestData testData) @@ -1101,6 +1167,14 @@ private static List FindPlayersWithMultipleIdentities(TestData testData) results.Add(identity.Player!); } } + + foreach (var player in testData.Players.Where(p => p.PlayerIdentities.Count() > 1)) + { + if (!results.Any(x => x.PlayerId == player.PlayerId)) + { + results.Add(player); + } + } return results; } @@ -1396,14 +1470,14 @@ private static void ForceTheFifthAndSixthMostRunsResultsToBeEqual(TestData testD Player = x, Runs = testData.Matches.SelectMany(m => m.MatchInnings) .SelectMany(mi => mi.PlayerInnings) - .Where(pi => pi.Batter.Player.PlayerId == x.PlayerId) + .Where(pi => pi.Batter!.Player!.PlayerId == x.PlayerId) .Sum(pi => pi.RunsScored) }).OrderByDescending(x => x.Runs).ToList(); var differenceBetweenFifthAndSixth = allPlayers[4].Runs - allPlayers[5].Runs; var anyInningsByPlayerSix = testData.Matches.SelectMany(m => m.MatchInnings) .SelectMany(mi => mi.PlayerInnings) - .First(pi => pi.Batter.Player.PlayerId == allPlayers[5].Player.PlayerId && pi.RunsScored.HasValue); + .First(pi => pi.Batter!.Player!.PlayerId == allPlayers[5].Player.PlayerId && pi.RunsScored.HasValue); anyInningsByPlayerSix.RunsScored += differenceBetweenFifthAndSixth; } @@ -1414,7 +1488,7 @@ private void ForceTheFifthAndSixthMostWicketsResultsToBeEqual(TestData testData) Player = x, Wickets = testData.Matches.SelectMany(m => m.MatchInnings) .SelectMany(mi => mi.BowlingFigures) - .Where(pi => pi.Bowler.Player.PlayerId == x.PlayerId) + .Where(pi => pi.Bowler!.Player!.PlayerId == x.PlayerId) .Sum(pi => pi.Wickets) }).OrderByDescending(x => x.Wickets).ToList(); @@ -1424,13 +1498,13 @@ private void ForceTheFifthAndSixthMostWicketsResultsToBeEqual(TestData testData) while (differenceBetweenFifthAndSixth > 0) { var matchInningsWherePlayerSixCouldTakeWickets = testData.Matches.SelectMany(m => m.MatchInnings) - .Where(mi => sixthPlayer.Player.PlayerIdentities.Select(pi => pi.Team.TeamId!.Value).Contains(mi.BowlingTeam.Team.TeamId!.Value) && + .Where(mi => sixthPlayer.Player.PlayerIdentities.Select(pi => pi.Team!.TeamId!.Value).Contains(mi.BowlingTeam!.Team!.TeamId!.Value) && mi.PlayerInnings.Any(pi => !StatisticsConstants.DISMISSALS_THAT_ARE_OUT.Contains(pi.DismissalType)) ).First(); var playerInningsToChange = matchInningsWherePlayerSixCouldTakeWickets.PlayerInnings.First(pi => !StatisticsConstants.DISMISSALS_THAT_ARE_OUT.Contains(pi.DismissalType)); playerInningsToChange.DismissalType = DismissalType.Bowled; - playerInningsToChange.Bowler = sixthPlayer.Player.PlayerIdentities.First(x => x.Team.TeamId == matchInningsWherePlayerSixCouldTakeWickets.BowlingTeam.Team.TeamId); + playerInningsToChange.Bowler = sixthPlayer.Player.PlayerIdentities.First(x => x.Team!.TeamId == matchInningsWherePlayerSixCouldTakeWickets.BowlingTeam!.Team!.TeamId); matchInningsWherePlayerSixCouldTakeWickets.BowlingFigures = _bowlingFiguresCalculator.CalculateBowlingFigures(matchInningsWherePlayerSixCouldTakeWickets); @@ -1468,9 +1542,9 @@ internal List GenerateMatchData(TestData testData, Faker teamFaker, } var allIdentities = teamsWithIdentities.SelectMany(x => x.identities); - foreach (var player in allIdentities.Select(x => x.Player)) + foreach (var player in allIdentities.Select(x => x.Player).OfType()) { - player.PlayerIdentities = allIdentities.Where(x => x.Player?.PlayerId == player.PlayerId).ToList(); + player.PlayerIdentities = new PlayerIdentityList(allIdentities.Where(x => x.Player?.PlayerId == player.PlayerId)); } // Create matches for them to play in, with scorecards @@ -1576,12 +1650,12 @@ private Match CreateMatchWithFieldingByMultipleIdentities(TestData testData, Lis // in the first innings a fielder should take catches under multiple identities var firstInnings = match.MatchInnings[0]; - var firstInningsIdentities = firstInnings.BowlingTeam.Team.TeamId == anyTeam1.team.TeamId ? anyTeam1.identities : anyTeam2.identities; + var firstInningsIdentities = firstInnings.BowlingTeam!.Team!.TeamId == anyTeam1.team.TeamId ? anyTeam1.identities : anyTeam2.identities; - var catcherWithMultipleIdentities = firstInningsIdentities.FirstOrDefault(x => firstInningsIdentities.Count(p => p.Player.PlayerId == x.Player.PlayerId) > 1)?.Player.PlayerId; + var catcherWithMultipleIdentities = firstInningsIdentities.FirstOrDefault(x => firstInningsIdentities.Count(p => p.Player!.PlayerId == x.Player!.PlayerId) > 1)?.Player!.PlayerId; if (catcherWithMultipleIdentities.HasValue) { - var catcherIdentities = firstInningsIdentities.Where(x => x.Player.PlayerId == catcherWithMultipleIdentities).ToList(); + var catcherIdentities = firstInningsIdentities.Where(x => x.Player!.PlayerId == catcherWithMultipleIdentities).ToList(); for (var i = 0; i < 6; i++) { @@ -1601,7 +1675,7 @@ private Match CreateMatchWithFieldingByMultipleIdentities(TestData testData, Lis // in the second innings a fielder should complete run-outs under multiple identities var secondInnings = match.MatchInnings[1]; - var secondInningsIdentities = secondInnings.BowlingTeam.Team.TeamId == anyTeam1.team.TeamId ? anyTeam1.identities : anyTeam2.identities; + var secondInningsIdentities = secondInnings.BowlingTeam.Team!.TeamId == anyTeam1.team.TeamId ? anyTeam1.identities : anyTeam2.identities; var fielderWithMultipleIdentities = secondInningsIdentities.FirstOrDefault(x => secondInningsIdentities.Count(p => p.Player.PlayerId == x.Player.PlayerId) > 1)?.Player.PlayerId; if (fielderWithMultipleIdentities.HasValue) @@ -1625,18 +1699,18 @@ private Match CreateMatchWithDifferentTeamsWhereSomeonePlaysOnBothTeams(TestData // 1. Find any player with identities on two teams var anyPlayerWithIdentitiesOnMultipleTeams = teamsWithIdentities.SelectMany(x => x.identities) - .GroupBy(x => x.Player.PlayerId, x => x, (playerId, playerIdentities) => new Player { PlayerId = playerId, PlayerIdentities = playerIdentities.ToList() }) - .Where(x => x.PlayerIdentities.Select(t => t.Team.TeamId!.Value).Distinct().Count() > 1) + .GroupBy(x => x.Player!.PlayerId, x => x, (playerId, playerIdentities) => new Player { PlayerId = playerId, PlayerIdentities = new PlayerIdentityList(playerIdentities) }) + .Where(x => x.PlayerIdentities.Select(t => t.Team!.TeamId!.Value).Distinct().Count() > 1) .First(); // 2. Create a match between those teams - var teamsForPlayer = teamsWithIdentities.Where(t => anyPlayerWithIdentitiesOnMultipleTeams.PlayerIdentities.Select(x => x.Team.TeamId).Contains(t.team.TeamId)).ToList(); + var teamsForPlayer = teamsWithIdentities.Where(t => anyPlayerWithIdentitiesOnMultipleTeams.PlayerIdentities.Select(x => x.Team!.TeamId).Contains(t.team.TeamId)).ToList(); var match = _matchFactory.CreateMatchBetween(teamsForPlayer[0].team, teamsForPlayer[0].identities, teamsForPlayer[1].team, teamsForPlayer[1].identities, _randomiser.FiftyFiftyChance(), testData, nameof(CreateMatchWithDifferentTeamsWhereSomeonePlaysOnBothTeams)); // 3. We know they'll be recorded as a batter in both innings. Ensure they take a wicket too. var wicketTaken = match.MatchInnings.First().PlayerInnings.First(); wicketTaken.DismissalType = DismissalType.CaughtAndBowled; - wicketTaken.Bowler = anyPlayerWithIdentitiesOnMultipleTeams.PlayerIdentities.First(x => x.Team.TeamId == match.MatchInnings.First().BowlingTeam.Team.TeamId); + wicketTaken.Bowler = anyPlayerWithIdentitiesOnMultipleTeams.PlayerIdentities.First(x => x.Team!.TeamId == match.MatchInnings.First().BowlingTeam!.Team!.TeamId); return match; } diff --git a/Stoolball.UnitTests/Statistics/PlayerTests.cs b/Stoolball.UnitTests/Statistics/PlayerTests.cs index 53a25472..67bb9b8c 100644 --- a/Stoolball.UnitTests/Statistics/PlayerTests.cs +++ b/Stoolball.UnitTests/Statistics/PlayerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Stoolball.Statistics; using Xunit; @@ -12,8 +11,8 @@ public void Player_name_orders_by_total_matches_aggregating_duplicates() { var player = new Player { - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityName = "Name One", TotalMatches = 2, @@ -29,7 +28,7 @@ public void Player_name_orders_by_total_matches_aggregating_duplicates() TotalMatches = 2, LastPlayed = DateTimeOffset.Now.Date } - } + ] }; var preferredName = player.PlayerName(); @@ -45,8 +44,8 @@ public void Player_name_prefers_most_recent_player() { var player = new Player { - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityName = "Name One", TotalMatches = 2, @@ -57,7 +56,7 @@ public void Player_name_prefers_most_recent_player() TotalMatches = 2, LastPlayed = DateTimeOffset.Now.Date } - } + ] }; var preferredName = player.PlayerName(); @@ -77,8 +76,8 @@ public void Player_name_prefers_complete_names(string completeName, string initi { var player = new Player { - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityName = "Name", TotalMatches = 2, @@ -94,7 +93,7 @@ public void Player_name_prefers_complete_names(string completeName, string initi TotalMatches = 2, LastPlayed = DateTimeOffset.Now.Date } - } + ] }; var preferredName = player.PlayerName(); @@ -111,8 +110,8 @@ public void Player_name_orders_each_group_by_total_matches_boosting_recent_playe { var player = new Player { - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityName = "Name One", TotalMatches = 2, @@ -188,7 +187,7 @@ public void Player_name_orders_each_group_by_total_matches_boosting_recent_playe TotalMatches = 2, LastPlayed = DateTimeOffset.Now.Date.AddDays(-4) // Fourth most recent, should boost to 2 matches * 2 weight = 4 } - } + ] }; var preferredName = player.PlayerName(); @@ -218,8 +217,8 @@ public void Player_name_prioritises_PreferredName_if_set() var player = new Player { PreferredName = "Preferred", - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity { PlayerIdentityName = "Name Complete", // Would beat the single-word name for the preferred identity in a calculated result TotalMatches = 10, // Would beat fewer matches for the preferred identity in a calculated result @@ -235,7 +234,7 @@ public void Player_name_prioritises_PreferredName_if_set() TotalMatches = 2, LastPlayed = DateTimeOffset.Now.Date.AddYears(-1) } - } + ] }; var preferredName = player.PlayerName(); diff --git a/Stoolball.Web.UnitTests/Navigation/StatisticsBreadcrumbBuilderTests.cs b/Stoolball.Web.UnitTests/Navigation/StatisticsBreadcrumbBuilderTests.cs index b6b10e53..31355475 100644 --- a/Stoolball.Web.UnitTests/Navigation/StatisticsBreadcrumbBuilderTests.cs +++ b/Stoolball.Web.UnitTests/Navigation/StatisticsBreadcrumbBuilderTests.cs @@ -46,7 +46,7 @@ public void Player_filter_adds_player_breadcrumb() { var builder = new StatisticsBreadcrumbBuilder(); var breadcrumbs = new List(); - var player = new Player { PlayerRoute = "/players/example", PlayerIdentities = new List { new PlayerIdentity { PlayerIdentityName = "Example player" } } }; + var player = new Player { PlayerRoute = "/players/example", PlayerIdentities = [new PlayerIdentity { PlayerIdentityName = "Example player" }] }; builder.BuildBreadcrumbs(breadcrumbs, new StatisticsFilter { Player = player }); diff --git a/Stoolball.Web.UnitTests/Statistics/BaseStatisticsTableControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/BaseStatisticsTableControllerTests.cs index 03a241e9..42c927f8 100644 --- a/Stoolball.Web.UnitTests/Statistics/BaseStatisticsTableControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/BaseStatisticsTableControllerTests.cs @@ -93,21 +93,21 @@ private static Player CreatePlayerWithIdentities() { return new Player { - PlayerIdentities = new List + PlayerIdentities = + [ + new PlayerIdentity { - new PlayerIdentity - { - PlayerIdentityName = "Example player 1", - Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Team name 1" }, - TotalMatches = 5, - }, - new PlayerIdentity - { - PlayerIdentityName = "Example player 2", - Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Team name 2" }, - TotalMatches = 10, // deliberately higher than the first identity to test that they're sorted later - } + PlayerIdentityName = "Example player 1", + Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Team name 1" }, + TotalMatches = 5, + }, + new PlayerIdentity + { + PlayerIdentityName = "Example player 2", + Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Team name 2" }, + TotalMatches = 10, // deliberately higher than the first identity to test that they're sorted later } + ] }; } diff --git a/Stoolball.Web.UnitTests/Statistics/CatchesControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/CatchesControllerTests.cs index a2f2e9a7..e3accdb0 100644 --- a/Stoolball.Web.UnitTests/Statistics/CatchesControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/CatchesControllerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -58,10 +57,10 @@ public async Task Player_is_swapped_to_fielder_when_filtering_results() { var player = new Player { - PlayerIdentities = new List { - new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() }, - new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() } - } + PlayerIdentities = [ + new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() }, + new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() } + ] }; var defaultFilter = new StatisticsFilter { Player = player }; var appliedFilter = defaultFilter.Clone(); diff --git a/Stoolball.Web.UnitTests/Statistics/LinkedPlayersForMemberSurfaceControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/LinkedPlayersForMemberSurfaceControllerTests.cs index 6234459c..05d32b0f 100644 --- a/Stoolball.Web.UnitTests/Statistics/LinkedPlayersForMemberSurfaceControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/LinkedPlayersForMemberSurfaceControllerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -48,13 +47,13 @@ private static Player CreatePlayerWith4PlayerIdentities() return new Player { PlayerRoute = "/example-player", - PlayerIdentities = new List - { + PlayerIdentities = + [ new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid(), LinkedBy = PlayerIdentityLinkedBy.Member }, new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid(), LinkedBy = PlayerIdentityLinkedBy.Member }, new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid(), LinkedBy = PlayerIdentityLinkedBy.Member }, new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid(), LinkedBy = PlayerIdentityLinkedBy.Member } - } + ] }; } diff --git a/Stoolball.Web.UnitTests/Statistics/PlayerBattingControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/PlayerBattingControllerTests.cs index f8f1a86e..683696d0 100644 --- a/Stoolball.Web.UnitTests/Statistics/PlayerBattingControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/PlayerBattingControllerTests.cs @@ -120,13 +120,13 @@ public async Task Player_name_is_in_page_title_and_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(Request.Object.Path, null)).Returns(Task.FromResult(player)); @@ -146,13 +146,13 @@ public async Task Player_team_is_in_page_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(Request.Object.Path, null)).Returns(Task.FromResult(player)); @@ -171,13 +171,13 @@ public async Task Filter_team_is_passed_to_humaniser_with_name_added() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Example team" } } - } + ] }; var appliedFilterWithoutNameFromQueryString = new StatisticsFilter { Team = new Team { TeamId = player.PlayerIdentities[0].Team!.TeamId } }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(appliedFilterWithoutNameFromQueryString); diff --git a/Stoolball.Web.UnitTests/Statistics/PlayerBowlingControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/PlayerBowlingControllerTests.cs index da74c92d..2c39e2a4 100644 --- a/Stoolball.Web.UnitTests/Statistics/PlayerBowlingControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/PlayerBowlingControllerTests.cs @@ -119,13 +119,13 @@ public async Task Player_name_is_in_page_title_and_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(Request.Object.Path, null)).Returns(Task.FromResult(player)); @@ -145,13 +145,13 @@ public async Task Player_team_is_in_page_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(Request.Object.Path, null)).Returns(Task.FromResult(player)); @@ -170,13 +170,13 @@ public async Task Filter_team_is_passed_to_humaniser_with_name_added() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Example team" } } - } + ] }; var appliedFilterWithoutNameFromQueryString = new StatisticsFilter { Team = new Team { TeamId = player.PlayerIdentities[0].Team!.TeamId } }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(appliedFilterWithoutNameFromQueryString); diff --git a/Stoolball.Web.UnitTests/Statistics/PlayerFieldingControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/PlayerFieldingControllerTests.cs index 13c46528..ea7eb44e 100644 --- a/Stoolball.Web.UnitTests/Statistics/PlayerFieldingControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/PlayerFieldingControllerTests.cs @@ -42,15 +42,14 @@ private static Player CreatePlayer() { return new Player { - PlayerIdentities = new List - { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityId = Guid.NewGuid(), PlayerIdentityName = "Player one", Team = new Team{ TeamName = "Example team"} } - } + ] }; } @@ -175,13 +174,13 @@ public async Task Filter_team_is_passed_to_humaniser_with_name_added() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Example team" } } - } + ] }; var appliedFilterWithoutNameFromQueryString = new StatisticsFilter { Player = CreatePlayer(), Team = new Team { TeamId = player.PlayerIdentities[0].Team!.TeamId } }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(Request.Object.QueryString.Value)).Returns(appliedFilterWithoutNameFromQueryString); diff --git a/Stoolball.Web.UnitTests/Statistics/PlayerSummaryViewModelFactoryTests.cs b/Stoolball.Web.UnitTests/Statistics/PlayerSummaryViewModelFactoryTests.cs index 58c51f33..69d13a7e 100644 --- a/Stoolball.Web.UnitTests/Statistics/PlayerSummaryViewModelFactoryTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/PlayerSummaryViewModelFactoryTests.cs @@ -115,13 +115,13 @@ public async Task Player_name_is_in_page_title_and_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(REQUEST_QUERYSTRING)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(REQUEST_PATH, It.IsAny())).Returns(Task.FromResult((Player?)player)); @@ -139,13 +139,13 @@ public async Task Player_team_is_in_page_description() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamName = "Example team" } } - } + ] }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(REQUEST_QUERYSTRING)).Returns(new StatisticsFilter()); _playerDataSource.Setup(x => x.ReadPlayerByRoute(REQUEST_PATH, It.IsAny())).Returns(Task.FromResult((Player?)player)); @@ -161,13 +161,13 @@ public async Task Filter_team_is_passed_to_humaniser_with_name_added() { var player = new Player { - PlayerIdentities = new List { + PlayerIdentities = [ new PlayerIdentity { PlayerIdentityName = "Player one", Team = new Team { TeamId = Guid.NewGuid(), TeamName = "Example team" } } - } + ] }; var appliedFilterWithoutNameFromQueryString = new StatisticsFilter { Team = new Team { TeamId = player.PlayerIdentities[0].Team!.TeamId } }; _statisticsFilterQueryStringParser.Setup(x => x.ParseQueryString(REQUEST_QUERYSTRING)).Returns(appliedFilterWithoutNameFromQueryString); diff --git a/Stoolball.Web.UnitTests/Statistics/RunOutsControllerTests.cs b/Stoolball.Web.UnitTests/Statistics/RunOutsControllerTests.cs index 56000e6c..bd0186e5 100644 --- a/Stoolball.Web.UnitTests/Statistics/RunOutsControllerTests.cs +++ b/Stoolball.Web.UnitTests/Statistics/RunOutsControllerTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -58,10 +57,10 @@ public async Task Player_is_swapped_to_fielder_when_filtering_results() { var player = new Player { - PlayerIdentities = new List { - new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() }, - new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() } - } + PlayerIdentities = [ + new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() }, + new PlayerIdentity{ PlayerIdentityId = Guid.NewGuid() } + ] }; var defaultFilter = new StatisticsFilter { Player = player }; var appliedFilter = defaultFilter.Clone(); diff --git a/Stoolball.Web/Teams/DeleteTeamController.cs b/Stoolball.Web/Teams/DeleteTeamController.cs index 78600979..35e94471 100644 --- a/Stoolball.Web/Teams/DeleteTeamController.cs +++ b/Stoolball.Web/Teams/DeleteTeamController.cs @@ -68,7 +68,7 @@ public DeleteTeamController(ILogger logger, model.Team.Players = (await _playerDataSource.ReadPlayerIdentities(new PlayerFilter { TeamIds = teamIds - }).ConfigureAwait(false)).Select(x => new Player { PlayerIdentities = new List { x } }).ToList(); + }).ConfigureAwait(false)).Select(x => new Player { PlayerIdentities = [x] }).ToList(); model.ConfirmDeleteRequest.RequiredText = model.Team.TeamName; diff --git a/Stoolball.Web/Teams/DeleteTeamSurfaceController.cs b/Stoolball.Web/Teams/DeleteTeamSurfaceController.cs index 4da6f4ea..14bc435c 100644 --- a/Stoolball.Web/Teams/DeleteTeamSurfaceController.cs +++ b/Stoolball.Web/Teams/DeleteTeamSurfaceController.cs @@ -95,7 +95,7 @@ public async Task DeleteTeam([Bind("RequiredText", "ConfirmationT model.Team.Players = (await _playerDataSource.ReadPlayerIdentities(new PlayerFilter { TeamIds = teamIds - })).Select(x => new Player { PlayerIdentities = new List { x } }).ToList(); + })).Select(x => new Player { PlayerIdentities = [x] }).ToList(); } model.Metadata.PageTitle = $"Delete {model.Team.TeamName}"; diff --git a/Stoolball/Statistics/Player.cs b/Stoolball/Statistics/Player.cs index c7378fd1..ce98d24b 100644 --- a/Stoolball/Statistics/Player.cs +++ b/Stoolball/Statistics/Player.cs @@ -178,7 +178,7 @@ private IEnumerable CombineDuplicateNames() public Guid? MemberKey { get; set; } - public List PlayerIdentities { get; internal set; } = new List(); + public PlayerIdentityList PlayerIdentities { get; internal set; } = new PlayerIdentityList(); public List History { get; internal set; } = new List(); diff --git a/Stoolball/Statistics/PlayerIdentityList.cs b/Stoolball/Statistics/PlayerIdentityList.cs new file mode 100644 index 00000000..d1db2cc6 --- /dev/null +++ b/Stoolball/Statistics/PlayerIdentityList.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Stoolball.Statistics +{ + public class PlayerIdentityList : List + { + public PlayerIdentityList() : base() { } + public PlayerIdentityList(IEnumerable playerIdentities) : base() + { + foreach (var identity in playerIdentities) { Add(identity); } + } + + public new void Add(PlayerIdentity playerIdentity) + { + if (playerIdentity.PlayerIdentityId.HasValue && + this.Any(pi => pi.PlayerIdentityId == playerIdentity.PlayerIdentityId)) + { + throw new InvalidOperationException($"{nameof(PlayerIdentity)} {playerIdentity.PlayerIdentityId} has already been added"); + } + base.Add(playerIdentity); + } + } +} diff --git a/Stoolball/StoolballEntityCopier.cs b/Stoolball/StoolballEntityCopier.cs index d391943e..a4c35a5f 100644 --- a/Stoolball/StoolballEntityCopier.cs +++ b/Stoolball/StoolballEntityCopier.cs @@ -41,7 +41,7 @@ public StoolballEntityCopier(IDataRedactor dataRedactor) PlayerId = player.PlayerId, PlayerRoute = player.PlayerRoute, MemberKey = player.MemberKey, - PlayerIdentities = player.PlayerIdentities.Select(x => CreateAuditableCopy(x)).OfType().ToList() + PlayerIdentities = new PlayerIdentityList(player.PlayerIdentities.Select(x => CreateAuditableCopy(x)).OfType()) }; } diff --git a/docs/Players.md b/docs/Players.md index e0dbea4a..71ef6e71 100644 --- a/docs/Players.md +++ b/docs/Players.md @@ -78,7 +78,7 @@ Stoolball England administrators can link and unlink identities, subject to the | Has a `MemberKey` and a single identity on the owned team, linked by `Member`. | Has two identities on the owned team, both linked by `Team`. | ~~Member can cancel the operation.~~ __Not allowed. See [#675](https://github.com/stoolball-england/stoolball-org-uk/issues/675).__ | ~~Has three identities, one linked by `Member`. The moved identities are linked by `Member` if they accepted, `Team` otherwise. `MemberKey` unchanged.~~ __Not allowed.__ | ~~Moved to Player A. Player B deleted.~~ __Not allowed.__ | | Has two identities on the owned team, both linked by `Team`. | On the owned team, with a single `DefaultIdentity`. || Has three identities, all linked by `Team`. | Moved to Player A. Player B deleted. | | Has two identities on the owned team, both linked by `Team`. | Has a `MemberKey` and two identities. One identity is on the owned team and one on a different team, both linked by a `Member`. |~~Member can cancel the operation.~~ __Not allowed. See [#675](https://github.com/stoolball-england/stoolball-org-uk/issues/675).__ | ~~Moved to Player B. Player A deleted.~~ __Not allowed.__ | ~~Has four identities, two linked by `Member`. The moved identities are linked by `Member` if they accepted, `Team` otherwise. `MemberKey` unchanged.~~ __Not allowed.__ | -| Has two identities on the owned team, both linked by `Team`. | Has two identities on the owned team, both linked by `Team`. || Moved to Player B. Player A deleted. | Has four identities, all linked by `Team`. `MemberKey` is `NULL`. | +| Has two identities on the owned team, both linked by `Team`. | Has two identities on the owned team, both linked by `Team`. | __Not allowed. See [#673](https://github.com/stoolball-england/stoolball-org-uk/issues/673).__ | ~~Moved to Player B. Player A deleted.~~ __Not allowed.__ | ~~Has four identities, all linked by `Team`. `MemberKey` is `NULL`.~~ __Not allowed.__ | ### Unlink an identity as a team owner