From 776dd8b6d09399c7af7ba550bbfc4ab15cfc5f21 Mon Sep 17 00:00:00 2001 From: Rick Mason Date: Wed, 16 Aug 2023 13:22:59 +0100 Subject: [PATCH 1/7] Add integration tests for SqlServerMatchRepository #484 --- .../Matches/SqlServerMatchRepositoryTests.cs | 1066 ++++++++++++++++- .../StoolballIntegrationTests.dacpac | Bin 28567 -> 29023 bytes .../SqlServerMatchRepositoryUnitTests.cs | 474 ++++++++ Stoolball.Data.SqlServer/DapperWrapper.cs | 34 +- Stoolball.Data.SqlServer/IDapperWrapper.cs | 69 +- .../SqlServerMatchRepository.cs | 353 +++--- Stoolball.Testing/SeedDataGenerator.cs | 1 + .../EditCloseOfPlaySurfaceController.cs | 2 +- Stoolball/Awards/AwardNotFoundException.cs | 11 + Stoolball/IStoolballEntityCopier.cs | 8 + ...{AssemblyInfo.cs => InternalsVisibleTo.cs} | 19 +- Stoolball/Matches/BattingScorecardComparer.cs | 2 +- .../Matches/BattingScorecardComparison.cs | 2 +- Stoolball/Matches/MatchNotFoundException.cs | 11 + Stoolball/StoolballEntityCopier.cs | 140 ++- 15 files changed, 1927 insertions(+), 265 deletions(-) create mode 100644 Stoolball.Data.SqlServer.UnitTests/SqlServerMatchRepositoryUnitTests.cs create mode 100644 Stoolball/Awards/AwardNotFoundException.cs rename Stoolball/{AssemblyInfo.cs => InternalsVisibleTo.cs} (77%) create mode 100644 Stoolball/Matches/MatchNotFoundException.cs diff --git a/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerMatchRepositoryTests.cs b/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerMatchRepositoryTests.cs index 323d288b7..2eeafd18c 100644 --- a/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerMatchRepositoryTests.cs +++ b/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerMatchRepositoryTests.cs @@ -1,11 +1,14 @@ -using System; +using System; using System.Collections.Generic; +using System.Data; +using System.Linq; using System.Threading.Tasks; using System.Transactions; using AngleSharp.Css.Dom; using Dapper; using Ganss.XSS; using Moq; +using Stoolball.Awards; using Stoolball.Data.Abstractions; using Stoolball.Data.SqlServer.IntegrationTests.Fixtures; using Stoolball.Logging; @@ -13,6 +16,7 @@ using Stoolball.Routing; using Stoolball.Security; using Stoolball.Statistics; +using Stoolball.Teams; using Xunit; namespace Stoolball.Data.SqlServer.IntegrationTests.Matches @@ -22,46 +26,1051 @@ public class SqlServerMatchRepositoryTests : IDisposable { private readonly SqlServerTestDataFixture _databaseFixture; private readonly TransactionScope _scope; + private readonly Mock _sanitizer = new(); + private readonly Mock _auditRepository = new(); + private readonly Mock> _logger = new(); + private readonly Mock _routeGenerator = new(); + private readonly Mock _redirectsRepository = new(); + private readonly Mock _matchNameBuilder = new(); + private readonly Mock _playerTypeSelector = new(); + private readonly Mock _bowlingScorecardComparer = new(); + private readonly Mock _battingScorecardComparer = new(); + private readonly Mock _playerRepository = new(); + private readonly Mock _dataRedactor = new(); + private readonly Mock _statisticsRepository = new(); + private readonly Mock _oversHelper = new(); + private readonly Mock _statisticsBuilder = new(); + private readonly Mock _matchInningsFactory = new(); + private readonly Mock _seasonDataSource = new(); + private readonly Guid _memberKey; + private readonly string _memberName; public SqlServerMatchRepositoryTests(SqlServerTestDataFixture databaseFixture) { _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); _scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + + _sanitizer.Setup(x => x.AllowedTags).Returns(new HashSet()); + _sanitizer.Setup(x => x.AllowedAttributes).Returns(new HashSet()); + _sanitizer.Setup(x => x.AllowedCssProperties).Returns(new HashSet()); + _sanitizer.Setup(x => x.AllowedAtRules).Returns(new HashSet()); + + _memberKey = _databaseFixture.TestData.Members[0].memberKey; + _memberName = _databaseFixture.TestData.Members[0].memberName; + + _battingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(new BattingScorecardComparison()); + + _playerRepository.Setup(x => x.CreateOrMatchPlayerIdentity(It.IsAny(), _memberKey, _memberName, It.IsAny())).Returns(new CreateOrMatchPlayerIdentityReturns((PlayerIdentity pi, Guid _, string _, IDbTransaction _) => Task.FromResult(pi))); + + } + private SqlServerMatchRepository CreateRepository() + { + return new SqlServerMatchRepository( + _databaseFixture.ConnectionFactory, + new DapperWrapper(), + _auditRepository.Object, + _logger.Object, + _routeGenerator.Object, + _redirectsRepository.Object, + _sanitizer.Object, + _matchNameBuilder.Object, + _playerTypeSelector.Object, + _bowlingScorecardComparer.Object, + _battingScorecardComparer.Object, + _playerRepository.Object, + _dataRedactor.Object, + _statisticsRepository.Object, + _oversHelper.Object, + _statisticsBuilder.Object, + _matchInningsFactory.Object, + _seasonDataSource.Object, + new StoolballEntityCopier(_dataRedactor.Object)); + } + + // TODO: Test repository.CreateMatch(); + // TODO: Test repository.UpdateMatch(); + // TODO: Test repository.UpdateMatchFormat(); + // TODO: Test repository.UpdateStartOfPlay(); + + private Stoolball.Matches.Match CloneValidMatch() + { + var matchToCopy = _databaseFixture.TestData.MatchInThePastWithFullDetails!; + var inningsToCopy = matchToCopy.MatchInnings.First(x => x.PlayerInnings.Count > 1 && x.OversBowled.Count > 1); + var match = new Stoolball.Matches.Match + { + MatchId = matchToCopy.MatchId, + MatchInnings = new List + { + new MatchInnings + { + MatchInningsId = inningsToCopy.MatchInningsId, + BattingTeam = new TeamInMatch + { + Team = new Team + { + TeamId = inningsToCopy.BattingTeam!.Team!.TeamId + } + }, + BowlingTeam = new TeamInMatch + { + Team = new Team + { + TeamId = inningsToCopy.BowlingTeam!.Team!.TeamId + } + }, + OverSets = inningsToCopy.OverSets, + Byes = inningsToCopy.Byes, + Wides = inningsToCopy.Wides, + NoBalls = inningsToCopy.NoBalls, + BonusOrPenaltyRuns = inningsToCopy.BonusOrPenaltyRuns, + Runs = inningsToCopy.Runs, + Wickets = inningsToCopy.Wickets + } + }, + PlayersPerTeam = matchToCopy.PlayersPerTeam + }; + foreach (var playerInnings in inningsToCopy.PlayerInnings) + { + match.MatchInnings[0].PlayerInnings.Add(new PlayerInnings + { + PlayerInningsId = playerInnings.PlayerInningsId, + BattingPosition = playerInnings.BattingPosition, + Batter = playerInnings.Batter, + DismissalType = playerInnings.DismissalType, + DismissedBy = playerInnings.DismissedBy, + Bowler = playerInnings.Bowler, + RunsScored = playerInnings.RunsScored, + BallsFaced = playerInnings.BallsFaced, + }); + } + foreach (var over in inningsToCopy.OversBowled) + { + match.MatchInnings[0].OversBowled.Add(new Over + { + OverId = over.OverId, + OverSet = over.OverSet, + OverNumber = over.OverNumber, + Bowler = over.Bowler, + BallsBowled = over.BallsBowled, + Wides = over.Wides, + NoBalls = over.NoBalls, + RunsConceded = over.RunsConceded + }); + } + + return match; + } + + delegate Task CreateOrMatchPlayerIdentityReturns(PlayerIdentity pi, Guid memberKey, string memberName, IDbTransaction transaction); + + [Theory] + [InlineData(false, false, false, false)] + [InlineData(true, false, false, false)] + [InlineData(false, true, false, false)] + [InlineData(false, false, true, false)] + [InlineData(false, false, false, true)] + [InlineData(true, true, true, true)] + public async Task UpdateBattingScorecard_inserts_new_player_innings(bool hasFielder, bool hasBowler, bool hasRuns, bool hasBallsFaced) + { + var repository = CreateRepository(); + + var match = CloneValidMatch(); + var possibleBatters = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == match.MatchInnings[0].BattingTeam!.Team!.TeamId); + var possibleFielders = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == match.MatchInnings[0].BowlingTeam!.Team!.TeamId); + + var playerInnings = AddOneNewPlayerInnings(match.MatchInnings[0].PlayerInnings, possibleBatters, possibleFielders, hasFielder, hasBowler, hasRuns, hasBallsFaced); + + var returnedInnings = await repository.UpdateBattingScorecard(match, match.MatchInnings[0].MatchInningsId!.Value, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Equal(match.MatchInnings[0].PlayerInnings.Count, returnedInnings.PlayerInnings.Count); + Assert.Equal(1, returnedInnings.PlayerInnings.Count(x => + x.BattingPosition == playerInnings.BattingPosition && + x.Batter!.PlayerIdentityId == playerInnings.Batter.PlayerIdentityId && + x.DismissalType == playerInnings.DismissalType && + x.DismissedBy?.PlayerIdentityId == playerInnings.DismissedBy?.PlayerIdentityId && + x.Bowler?.PlayerIdentityId == playerInnings.Bowler?.PlayerIdentityId && + x.RunsScored == playerInnings.RunsScored && + x.BallsFaced == playerInnings.BallsFaced)); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedInnings = await connection.QuerySingleOrDefaultAsync<(Guid BatterPlayerIdentityId, int? BattingPosition, DismissalType DismissalType, Guid? DismissedByPlayerIdentityId, Guid? BowledByPlayerIdentityId, int? RunsScored, int? BallsFaced)?>( + @$"SELECT BatterPlayerIdentityId, BattingPosition, DismissalType, DismissedByPlayerIdentityId, BowlerPlayerIdentityId, RunsScored, BallsFaced + FROM {Tables.PlayerInnings} + WHERE MatchInningsId = @MatchInningsId + AND BattingPosition = @BattingPosition", + new + { + match.MatchInnings[0].MatchInningsId, + playerInnings.BattingPosition + }).ConfigureAwait(false); + + Assert.NotNull(savedInnings); + Assert.Equal(playerInnings.Batter!.PlayerIdentityId, savedInnings.Value.BatterPlayerIdentityId); + Assert.Equal(match.MatchInnings[0].PlayerInnings.Count, savedInnings.Value.BattingPosition); + Assert.Equal(playerInnings.DismissalType, savedInnings.Value.DismissalType); + Assert.Equal(playerInnings.DismissedBy?.PlayerIdentityId, savedInnings.Value.DismissedByPlayerIdentityId); + Assert.Equal(playerInnings.Bowler?.PlayerIdentityId, savedInnings.Value.BowledByPlayerIdentityId); + Assert.Equal(playerInnings.RunsScored, savedInnings.Value.RunsScored); + Assert.Equal(playerInnings.BallsFaced, savedInnings.Value.BallsFaced); + } + } + + private PlayerInnings AddOneNewPlayerInnings(List innings, IEnumerable possibleBatters, IEnumerable possibleFielders, bool hasFielder, bool hasBowler, bool hasRuns, bool hasBallsFaced) + { + var playerInnings = new PlayerInnings + { + BattingPosition = innings.Count + 1, + Batter = possibleBatters.First(), + DismissalType = DismissalType.RunOut, + DismissedBy = hasFielder ? possibleFielders.First() : null, + Bowler = hasBowler ? possibleFielders.Last() : null, + RunsScored = hasRuns ? 57 : null, + BallsFaced = hasBallsFaced ? 64 : null + }; + innings.Add(playerInnings); + + _battingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(new BattingScorecardComparison { PlayerInningsAdded = new List { playerInnings } }); + return playerInnings; + } + + [Theory] + [InlineData(false, false, false, false, false, false)] + [InlineData(true, false, false, false, false, false)] + [InlineData(false, true, false, false, false, false)] + [InlineData(false, false, true, false, false, false)] + [InlineData(false, false, false, true, false, false)] + [InlineData(false, false, false, false, true, false)] + [InlineData(false, false, false, false, false, true)] + public async Task UpdateBattingScorecard_updates_player_innings_previously_added(bool batterHasChanged, bool dismissalTypeHasChanged, bool fielderHasChanged, bool bowlerHasChanged, bool runsScoredHasChanged, bool ballsFacedHasChanged) + { + var repository = CreateRepository(); + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + var modifiedPlayerInnings = modifiedInnings.PlayerInnings.Last(); + var originalPlayerInnings = new PlayerInnings + { + PlayerInningsId = modifiedPlayerInnings.PlayerInningsId, + BattingPosition = modifiedPlayerInnings.BattingPosition, + Batter = modifiedPlayerInnings.Batter, + DismissalType = modifiedPlayerInnings.DismissalType, + DismissedBy = modifiedPlayerInnings.DismissedBy, + Bowler = modifiedPlayerInnings.Bowler, + RunsScored = modifiedPlayerInnings.RunsScored, + BallsFaced = modifiedPlayerInnings.BallsFaced + }; + + + if (batterHasChanged) + { + var possibleBatters = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == modifiedMatch.MatchInnings[0].BattingTeam!.Team!.TeamId); + modifiedPlayerInnings.Batter = possibleBatters.First(x => x.PlayerIdentityId != modifiedPlayerInnings.Batter?.PlayerIdentityId); + } + if (dismissalTypeHasChanged) { modifiedPlayerInnings.DismissalType = modifiedPlayerInnings.DismissalType == DismissalType.Caught ? DismissalType.CaughtAndBowled : DismissalType.Caught; } + if (fielderHasChanged || bowlerHasChanged) + { + var possibleFielders = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == modifiedMatch.MatchInnings[0].BowlingTeam!.Team!.TeamId); + if (fielderHasChanged) { modifiedPlayerInnings.DismissedBy = possibleFielders.First(x => x.PlayerIdentityId != modifiedPlayerInnings.DismissedBy?.PlayerIdentityId); } + if (bowlerHasChanged) { modifiedPlayerInnings.Bowler = possibleFielders.First(x => x.PlayerIdentityId != modifiedPlayerInnings.Bowler?.PlayerIdentityId); } + } + if (runsScoredHasChanged) { modifiedPlayerInnings.RunsScored = modifiedPlayerInnings.RunsScored.HasValue ? modifiedPlayerInnings.RunsScored + 1 : 60; } + if (ballsFacedHasChanged) { modifiedPlayerInnings.BallsFaced = modifiedPlayerInnings.BallsFaced.HasValue ? modifiedPlayerInnings.BallsFaced + 1 : 70; } + + var comparison = SetupUnchangedBattingComparison(modifiedInnings); + comparison.PlayerInningsUnchanged.Remove(modifiedPlayerInnings); + comparison.PlayerInningsChanged.Add((originalPlayerInnings, modifiedPlayerInnings)); + + var result = await repository.UpdateBattingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(modifiedInnings.PlayerInnings.Count, result.PlayerInnings.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedInnings = await connection.QuerySingleOrDefaultAsync<(Guid BatterPlayerIdentityId, int? BattingPosition, DismissalType DismissalType, Guid? DismissedByPlayerIdentityId, Guid? BowledByPlayerIdentityId, int? RunsScored, int? BallsFaced)?>( + @$"SELECT BatterPlayerIdentityId, BattingPosition, DismissalType, DismissedByPlayerIdentityId, BowlerPlayerIdentityId, RunsScored, BallsFaced + FROM {Tables.PlayerInnings} + WHERE PlayerInningsId = @PlayerInningsId", + modifiedPlayerInnings + ).ConfigureAwait(false); + + Assert.NotNull(savedInnings); + Assert.Equal(modifiedPlayerInnings.Batter!.PlayerIdentityId, savedInnings.Value.BatterPlayerIdentityId); + Assert.Equal(modifiedPlayerInnings.BattingPosition, savedInnings.Value.BattingPosition); + Assert.Equal(modifiedPlayerInnings.DismissalType, savedInnings.Value.DismissalType); + Assert.Equal(modifiedPlayerInnings.DismissedBy?.PlayerIdentityId, savedInnings.Value.DismissedByPlayerIdentityId); + Assert.Equal(modifiedPlayerInnings.Bowler?.PlayerIdentityId, savedInnings.Value.BowledByPlayerIdentityId); + Assert.Equal(modifiedPlayerInnings.RunsScored, savedInnings.Value.RunsScored); + Assert.Equal(modifiedPlayerInnings.BallsFaced, savedInnings.Value.BallsFaced); + } } [Fact] - public async Task Delete_match_succeeds() + public async Task UpdateBattingScorecard_deletes_player_innings_removed_from_scorecard() { - var sanitizer = new Mock(); - sanitizer.Setup(x => x.AllowedTags).Returns(new HashSet()); - sanitizer.Setup(x => x.AllowedAttributes).Returns(new HashSet()); - sanitizer.Setup(x => x.AllowedCssProperties).Returns(new HashSet()); - sanitizer.Setup(x => x.AllowedAtRules).Returns(new HashSet()); + var repository = CreateRepository(); + + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + + var playerInningsToRemove = modifiedInnings.PlayerInnings.Last(); + modifiedInnings.PlayerInnings.Remove(playerInningsToRemove); + + var comparison = SetupUnchangedBattingComparison(modifiedInnings); + comparison.PlayerInningsRemoved.Add(playerInningsToRemove); + + var result = await repository.UpdateBattingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(modifiedInnings.PlayerInnings.Count, result.PlayerInnings.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedInningsId = await connection.QuerySingleOrDefaultAsync( + $"SELECT PlayerInningsId FROM {Tables.PlayerInnings} WHERE PlayerInningsId = @PlayerInningsId", + playerInningsToRemove).ConfigureAwait(false); + + Assert.Null(savedInningsId); + } + } + + [Fact] + public async Task UpdateBattingScorecard_unchanged_player_innings_are_retained() + { + var repository = CreateRepository(); + + var match = _databaseFixture.TestData.MatchInThePastWithFullDetails!; + var innings = match.MatchInnings.First(x => x.PlayerInnings.Count > 0); + + var comparison = new BattingScorecardComparison { PlayerInningsUnchanged = innings.PlayerInnings }; + _battingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(comparison); + + var result = await repository.UpdateBattingScorecard( + match, + innings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(innings.PlayerInnings.Count, result.PlayerInnings.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + foreach (var playerInnings in innings.PlayerInnings) + { + var savedInnings = await connection.QuerySingleOrDefaultAsync<(Guid BatterPlayerIdentityId, DismissalType DismissalType, Guid? DismissedByPlayerIdentityId, Guid? BowledByPlayerIdentityId, int? RunsScored, int? BallsFaced)?>( + @$"SELECT BatterPlayerIdentityId, DismissalType, DismissedByPlayerIdentityId, BowlerPlayerIdentityId, RunsScored, BallsFaced + FROM {Tables.PlayerInnings} + WHERE PlayerInningsId = @PlayerInningsId", + playerInnings).ConfigureAwait(false); + + Assert.NotNull(savedInnings); + Assert.Equal(playerInnings.Batter!.PlayerIdentityId, savedInnings.Value.BatterPlayerIdentityId); + Assert.Equal(playerInnings.DismissalType, savedInnings.Value.DismissalType); + Assert.Equal(playerInnings.DismissedBy?.PlayerIdentityId, savedInnings.Value.DismissedByPlayerIdentityId); + Assert.Equal(playerInnings.Bowler?.PlayerIdentityId, savedInnings.Value.BowledByPlayerIdentityId); + Assert.Equal(playerInnings.RunsScored, savedInnings.Value.RunsScored); + Assert.Equal(playerInnings.BallsFaced, savedInnings.Value.BallsFaced); + } + } + } + + [Theory] + [InlineData(null, null, null, null, null, null)] + [InlineData(5, 10, 15, 20, 140, 8)] + public async Task UpdateBattingScorecard_updates_extras_and_final_score(int? byes, int? wides, int? noBalls, int? bonus, int? runs, int? wickets) + { + var repository = CreateRepository(); + + Func inningsSelector = mi => mi.Byes != byes && mi.Wides != wides && mi.NoBalls != noBalls && mi.BonusOrPenaltyRuns != bonus && mi.Runs != runs && mi.Wickets != wickets; + var match = _databaseFixture.TestData.Matches.First(m => m.MatchInnings.Any(inningsSelector)); + var innings = match.MatchInnings.First(inningsSelector); + + var updatedMatch = new Stoolball.Matches.Match + { + MatchId = match.MatchId, + MatchInnings = new List + { + new MatchInnings + { + MatchInningsId = innings.MatchInningsId, + Byes = byes, + Wides = wides, + NoBalls = noBalls, + BonusOrPenaltyRuns = bonus, + Runs = runs, + Wickets = wickets, + BattingTeam = innings.BattingTeam, + BowlingTeam = innings.BowlingTeam + } + } + }; + + var returnedInnings = await repository.UpdateBattingScorecard(updatedMatch, updatedMatch.MatchInnings[0].MatchInningsId!.Value, _memberKey, _memberName).ConfigureAwait(false); + + Assert.NotNull(returnedInnings); + Assert.Equal(innings.MatchInningsId, returnedInnings.MatchInningsId); + Assert.Equal(byes, returnedInnings.Byes); + Assert.Equal(wides, returnedInnings.Wides); + Assert.Equal(noBalls, returnedInnings.NoBalls); + Assert.Equal(bonus, returnedInnings.BonusOrPenaltyRuns); + Assert.Equal(runs, returnedInnings.Runs); + Assert.Equal(wickets, returnedInnings.Wickets); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedInnings = await connection.QuerySingleOrDefaultAsync( + @$"SELECT Byes, Wides, NoBalls, BonusOrPenaltyRuns, Runs, Wickets + FROM {Tables.MatchInnings} + WHERE MatchInningsId = @MatchInningsId", + new + { + innings.MatchInningsId + }).ConfigureAwait(false); + + Assert.NotNull(savedInnings); + Assert.Equal(byes, savedInnings.Byes); + Assert.Equal(wides, savedInnings.Wides); + Assert.Equal(noBalls, savedInnings.NoBalls); + Assert.Equal(bonus, savedInnings.BonusOrPenaltyRuns); + Assert.Equal(runs, savedInnings.Runs); + Assert.Equal(wickets, savedInnings.Wickets); + } + } + + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public async Task UpdateBattingScorecard_updates_players_per_team_for_match(int numberOfInningsComparedToPlayersPerTeam) + { + var repository = CreateRepository(); + + var match = CloneValidMatch(); + var possibleBatters = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == match.MatchInnings[0].BattingTeam!.Team!.TeamId); + var possibleFielders = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == match.MatchInnings[0].BowlingTeam!.Team!.TeamId); + + var expectedPlayersPerTeam = match.PlayersPerTeam; + if (numberOfInningsComparedToPlayersPerTeam > expectedPlayersPerTeam) { expectedPlayersPerTeam += numberOfInningsComparedToPlayersPerTeam; } + + if (numberOfInningsComparedToPlayersPerTeam >= 0) + { + while (match.MatchInnings[0].PlayerInnings.Count < expectedPlayersPerTeam) + { + _ = AddOneNewPlayerInnings(match.MatchInnings[0].PlayerInnings, possibleBatters, possibleFielders, true, true, true, true); + } + } + else + { + while (match.MatchInnings[0].PlayerInnings.Count >= expectedPlayersPerTeam) + { + match.MatchInnings[0].PlayerInnings.RemoveAt(match.MatchInnings[0].PlayerInnings.Count - 1); + } + } + + _ = await repository.UpdateBattingScorecard(match, match.MatchInnings[0].MatchInningsId!.Value, _memberKey, _memberName).ConfigureAwait(false); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedPlayersPerTeam = await connection.QuerySingleOrDefaultAsync( + @$"SELECT PlayersPerTeam + FROM {Tables.Match} + WHERE MatchId = @MatchId", + match).ConfigureAwait(false); + + Assert.Equal(expectedPlayersPerTeam, savedPlayersPerTeam); + } + } + + [Theory] + [InlineData(false, false, false, false, false, false, false, false, false, false, false, false)] + [InlineData(true, false, false, false, false, false, false, false, false, false, false, false)] + [InlineData(false, true, false, false, false, false, false, false, false, false, false, false)] + [InlineData(false, false, true, false, false, false, false, false, false, false, false, false)] + [InlineData(false, false, false, true, false, false, false, false, false, false, false, false)] + [InlineData(false, false, false, false, true, false, false, false, false, false, false, false)] + [InlineData(false, false, false, false, false, true, false, false, false, false, false, false)] + [InlineData(false, false, false, false, false, false, true, false, false, false, false, false)] + [InlineData(false, false, false, false, false, false, false, true, false, false, false, false)] + [InlineData(false, false, false, false, false, false, false, false, true, false, false, false)] + [InlineData(false, false, false, false, false, false, false, false, false, true, false, false)] + [InlineData(false, false, false, false, false, false, false, false, false, false, true, false)] + [InlineData(false, false, false, false, false, false, false, false, false, false, false, true)] + public async Task UpdateBattingScorecard_updates_bowling_figures_and_player_statistics_if_data_has_changed( + bool batterHasChanged, bool dismissalTypeHasChanged, bool fielderHasChanged, bool bowlerHasChanged, bool runsScoredHasChanged, bool ballsFacedHasChanged, + bool byesHasChanged, bool widesHasChanged, bool noBallsHasChanged, bool bonusHasChanged, bool teamRunsHasChanged, bool teamWicketsHasChanged + ) + { + var repository = CreateRepository(); + var modifiedMatch = CloneValidMatch(); + var modifiedMatchInnings = modifiedMatch.MatchInnings[0]; + var modifiedPlayerInnings = modifiedMatchInnings.PlayerInnings.Last(); + var originalPlayerInnings = new PlayerInnings + { + PlayerInningsId = modifiedPlayerInnings.PlayerInningsId, + BattingPosition = modifiedPlayerInnings.BattingPosition, + Batter = modifiedPlayerInnings.Batter, + DismissalType = modifiedPlayerInnings.DismissalType, + DismissedBy = modifiedPlayerInnings.DismissedBy, + Bowler = modifiedPlayerInnings.Bowler, + RunsScored = modifiedPlayerInnings.RunsScored, + BallsFaced = modifiedPlayerInnings.BallsFaced + }; + var playerInningsHasChanged = batterHasChanged || dismissalTypeHasChanged || fielderHasChanged || bowlerHasChanged || runsScoredHasChanged || ballsFacedHasChanged; + var matchInningsHasChanged = byesHasChanged || widesHasChanged || noBallsHasChanged || bonusHasChanged || teamRunsHasChanged || teamWicketsHasChanged; + var anythingHasChanged = playerInningsHasChanged || matchInningsHasChanged; + + if (anythingHasChanged) + { + if (playerInningsHasChanged) + { + var possibleBatters = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == modifiedMatch.MatchInnings[0].BattingTeam!.Team!.TeamId); + var possibleFielders = _databaseFixture.TestData.PlayerIdentities.Where(x => x.Team!.TeamId == modifiedMatch.MatchInnings[0].BowlingTeam!.Team!.TeamId); + + if (batterHasChanged) { modifiedPlayerInnings.Batter = possibleBatters.First(x => x.PlayerIdentityId != modifiedPlayerInnings.Batter!.PlayerIdentityId); } + if (dismissalTypeHasChanged) { modifiedPlayerInnings.DismissalType = modifiedPlayerInnings.DismissalType == DismissalType.Caught ? DismissalType.CaughtAndBowled : DismissalType.Caught; } + if (fielderHasChanged) { modifiedPlayerInnings.DismissedBy = possibleFielders.First(x => x.PlayerIdentityId != modifiedPlayerInnings.DismissedBy?.PlayerIdentityId); } + if (bowlerHasChanged) { modifiedPlayerInnings.Bowler = possibleFielders.First(x => x.PlayerIdentityId != modifiedPlayerInnings.Bowler?.PlayerIdentityId); } + if (runsScoredHasChanged) { modifiedPlayerInnings.RunsScored = modifiedPlayerInnings.RunsScored.HasValue ? modifiedPlayerInnings.RunsScored + 1 : 60; } + if (ballsFacedHasChanged) { modifiedPlayerInnings.BallsFaced = modifiedPlayerInnings.BallsFaced.HasValue ? modifiedPlayerInnings.BallsFaced + 1 : 70; } + + var comparison = new BattingScorecardComparison { PlayerInningsChanged = new List<(PlayerInnings, PlayerInnings)> { (originalPlayerInnings, modifiedPlayerInnings) } }; + _battingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(comparison); + } + + if (byesHasChanged) { modifiedMatchInnings.Byes = modifiedMatchInnings.Byes.HasValue ? modifiedMatchInnings.Byes + 1 : 10; } + if (widesHasChanged) { modifiedMatchInnings.Wides = modifiedMatchInnings.Wides.HasValue ? modifiedMatchInnings.Wides + 1 : 9; } + if (noBallsHasChanged) { modifiedMatchInnings.NoBalls = modifiedMatchInnings.NoBalls.HasValue ? modifiedMatchInnings.NoBalls + 1 : 7; } + if (bonusHasChanged) { modifiedMatchInnings.BonusOrPenaltyRuns = modifiedMatchInnings.BonusOrPenaltyRuns.HasValue ? modifiedMatchInnings.BonusOrPenaltyRuns + 1 : 5; } + if (teamRunsHasChanged) { modifiedMatchInnings.Runs = modifiedMatchInnings.Runs.HasValue ? modifiedMatchInnings.Runs + 1 : 110; } + if (teamWicketsHasChanged) { modifiedMatchInnings.Wickets = modifiedMatchInnings.Wickets.HasValue ? modifiedMatchInnings.Wickets + 1 : 9; } + } + + _ = await repository.UpdateBattingScorecard(modifiedMatch, modifiedMatchInnings.MatchInningsId!.Value, _memberKey, _memberName).ConfigureAwait(false); + + _statisticsRepository.Verify(x => x.UpdateBowlingFigures(It.Is(mi => mi.MatchInningsId == modifiedMatchInnings.MatchInningsId), _memberKey, _memberName, It.IsAny()), bowlerHasChanged ? Times.Once() : Times.Never()); + _statisticsRepository.Verify(x => x.UpdatePlayerStatistics(It.IsAny>(), It.IsAny()), anythingHasChanged ? Times.Once() : Times.Never()); + } + [Fact] + public async Task UpdateBowlingScorecard_inserts_new_overs_and_extends_the_final_overset() + { + var repository = CreateRepository(); + + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + + var comparison = SetupUnchangedBowlingComparison(modifiedInnings); + + var totalOversInOversets = modifiedInnings.OverSets.Sum(o => o.Overs); + var finalOverSet = modifiedInnings.OverSets.Last(); + var totalOvers = modifiedInnings.OversBowled.Count; + + do + { + AddOneNewBowlingOver(modifiedInnings, comparison); + totalOvers++; + } + while (totalOvers <= totalOversInOversets); + + var result = await repository.UpdateBowlingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(modifiedInnings.OversBowled.Count, result.OversBowled.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + Assert.True(comparison.OversAdded.Any()); + foreach (var over in comparison.OversAdded) + { + var savedOver = await connection.QuerySingleOrDefaultAsync<(int? OverNumber, Guid? OverSetId, Guid? BowlerId, int? BallsBowled, int? Wides, int? NoBalls, int? RunsConceded)>( + $"SELECT OverNumber, OverSetId, BowlerPlayerIdentityId, BallsBowled, Wides, NoBalls, RunsConceded FROM {Tables.Over} WHERE OverId = @OverId", + new { over.OverId }).ConfigureAwait(false); + + Assert.Equal(over.OverNumber, savedOver.OverNumber); + Assert.Equal(over.OverSet!.OverSetId, savedOver.OverSetId); + Assert.Equal(over.Bowler!.PlayerIdentityId, savedOver.BowlerId); + Assert.Equal(over.BallsBowled, savedOver.BallsBowled); + Assert.Equal(over.Wides, savedOver.Wides); + Assert.Equal(over.NoBalls, savedOver.NoBalls); + Assert.Equal(over.RunsConceded, savedOver.RunsConceded); + + var oversInOverSet = await connection.QuerySingleOrDefaultAsync($"SELECT Overs FROM {Tables.OverSet} WHERE OverSetId = @OverSetId", + finalOverSet).ConfigureAwait(false); + + Assert.Equal(finalOverSet.Overs + 1, oversInOverSet); + } + } + } + + [Theory] + [InlineData(false, false, false, false, false)] + public async Task UpdateBowlingScorecard_updates_overs_previously_added(bool bowlerHasChanged, bool ballsBowledHasChanged, bool widesHasChanged, bool noBallsHasChanged, bool runsConcededHasChanged) + { + var repository = CreateRepository(); + + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + var modifiedOver = modifiedInnings.OversBowled.Last(); + if (bowlerHasChanged) + { + modifiedOver.Bowler = _databaseFixture.TestData.PlayerIdentities.First(x => x.Team!.TeamId == modifiedInnings.BowlingTeam!.Team!.TeamId && x.PlayerIdentityId != modifiedOver.Bowler?.PlayerIdentityId); + } + if (ballsBowledHasChanged) { modifiedOver.BallsBowled = modifiedOver.BallsBowled.HasValue ? modifiedOver.BallsBowled + 1 : 8; } + if (widesHasChanged) { modifiedOver.Wides = modifiedOver.Wides.HasValue ? modifiedOver.Wides + 1 : 4; } + if (noBallsHasChanged) { modifiedOver.NoBalls = modifiedOver.NoBalls.HasValue ? modifiedOver.NoBalls + 1 : 5; } + if (runsConcededHasChanged) { modifiedOver.RunsConceded = modifiedOver.RunsConceded.HasValue ? modifiedOver.RunsConceded + 1 : 12; } + + var comparison = SetupUnchangedBowlingComparison(modifiedInnings); + if (bowlerHasChanged || ballsBowledHasChanged || widesHasChanged || noBallsHasChanged || runsConcededHasChanged) + { + comparison.OversUnchanged.Remove(modifiedOver); + comparison.OversChanged.Add((modifiedOver, modifiedOver)); + } + + var result = await repository.UpdateBowlingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(modifiedInnings.OversBowled.Count, result.OversBowled.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedOver = await connection.QuerySingleOrDefaultAsync<(int? OverNumber, Guid? OverSetId, Guid? BowlerId, int? BallsBowled, int? Wides, int? NoBalls, int? RunsConceded)>( + $"SELECT OverNumber, OverSetId, BowlerPlayerIdentityId, BallsBowled, Wides, NoBalls, RunsConceded FROM {Tables.Over} WHERE OverId = @OverId", + new { modifiedOver.OverId }).ConfigureAwait(false); + + Assert.Equal(modifiedOver.OverNumber, savedOver.OverNumber); + Assert.Equal(modifiedOver.OverSet!.OverSetId, savedOver.OverSetId); + Assert.Equal(modifiedOver.Bowler!.PlayerIdentityId, savedOver.BowlerId); + Assert.Equal(modifiedOver.BallsBowled, savedOver.BallsBowled); + Assert.Equal(modifiedOver.Wides, savedOver.Wides); + Assert.Equal(modifiedOver.NoBalls, savedOver.NoBalls); + Assert.Equal(modifiedOver.RunsConceded, savedOver.RunsConceded); + } + } + + [Fact] + public async Task UpdateBowlingScorecard_deletes_overs_removed_from_scorecard() + { + var repository = CreateRepository(); + + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + + var overToRemove = modifiedInnings.OversBowled.Last(); + modifiedInnings.OversBowled.Remove(overToRemove); + + var comparison = SetupUnchangedBowlingComparison(modifiedInnings); + comparison.OversRemoved.Add(overToRemove); + + var result = await repository.UpdateBowlingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(modifiedInnings.OversBowled.Count, result.OversBowled.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedOverId = await connection.QuerySingleOrDefaultAsync( + $"SELECT OverId FROM {Tables.Over} WHERE OverId = @OverId", + overToRemove).ConfigureAwait(false); + + Assert.Null(savedOverId); + } + } + + [Fact] + public async Task UpdateBowlingScorecard_retains_unchanged_overs() + { + var repository = CreateRepository(); + + var match = _databaseFixture.TestData.MatchInThePastWithFullDetails!; + var innings = match.MatchInnings.First(x => x.OversBowled.Count > 0); + + var comparison = new BowlingScorecardComparison { OversUnchanged = innings.OversBowled }; + _bowlingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(comparison); + + var result = await repository.UpdateBowlingScorecard( + match, + innings.MatchInningsId!.Value, + _memberKey, + _memberName); + + Assert.Equal(innings.OversBowled.Count, result.OversBowled.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + foreach (var over in innings.OversBowled) + { + var savedOver = await connection.QuerySingleOrDefaultAsync<(Guid OverId, int OverNumber, Guid BowlerPlayerIdentityId, int? BallsBowled, int? NoBalls, int? Wides, int? RunsConceded)?>( + $"SELECT OverId, OverNumber, BowlerPlayerIdentityId, BallsBowled, NoBalls, Wides, RunsConceded FROM {Tables.Over} WHERE OverId = @OverId", + new + { + over.OverId, + over.OverNumber, + over.Bowler!.PlayerIdentityId, + over.BallsBowled, + over.NoBalls, + over.Wides, + over.RunsConceded + }).ConfigureAwait(false); + + // Don't check OverSetId because that could legitimately change if other overs were added or removed. + // For example, two sets of five overs. One over removed from the start and over six becomes over five, in a different set. + Assert.NotNull(savedOver); + Assert.Equal(over.OverNumber, savedOver.Value.OverNumber); + Assert.Equal(over.Bowler.PlayerIdentityId, savedOver.Value.BowlerPlayerIdentityId); + Assert.Equal(over.BallsBowled, savedOver.Value.BallsBowled); + Assert.Equal(over.NoBalls, savedOver.Value.NoBalls); + Assert.Equal(over.Wides, savedOver.Value.Wides); + Assert.Equal(over.RunsConceded, savedOver.Value.RunsConceded); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task UpdateBowlingScorecard_updates_bowling_figures_and_player_statistics_if_bowling_has_changed(bool bowlingHasChanged) + { + var repository = CreateRepository(); + + var modifiedMatch = CloneValidMatch(); + var modifiedInnings = modifiedMatch.MatchInnings[0]; + + var comparison = SetupUnchangedBowlingComparison(modifiedInnings); + + if (bowlingHasChanged) + { + AddOneNewBowlingOver(modifiedInnings, comparison); + } + + _ = await repository.UpdateBowlingScorecard( + modifiedMatch, + modifiedInnings.MatchInningsId!.Value, + _memberKey, + _memberName); + + _statisticsRepository.Verify(x => x.UpdateBowlingFigures(It.Is(mi => mi.MatchInningsId == modifiedInnings.MatchInningsId), _memberKey, _memberName, It.IsAny()), bowlingHasChanged ? Times.Once() : Times.Never()); + _statisticsRepository.Verify(x => x.UpdatePlayerStatistics(It.IsAny>(), It.IsAny()), bowlingHasChanged ? Times.Once() : Times.Never()); + } + + private static void AddOneNewBowlingOver(MatchInnings innings, BowlingScorecardComparison comparison) + { + var overToAdd = new Over + { + OverId = Guid.NewGuid(), + Bowler = innings.OversBowled[innings.OversBowled.Count - 2].Bowler, + BallsBowled = 8, + NoBalls = 2, + Wides = 3, + RunsConceded = 14, + OverSet = innings.OverSets.FirstOrDefault() + }; + innings.OversBowled.Add(overToAdd); + + comparison.OversAdded.Add(overToAdd); + } + + private BattingScorecardComparison SetupUnchangedBattingComparison(MatchInnings innings) + { + var comparison = new BattingScorecardComparison + { + PlayerInningsUnchanged = new List(innings.PlayerInnings) + }; + _battingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(comparison); + return comparison; + } + + private BowlingScorecardComparison SetupUnchangedBowlingComparison(MatchInnings innings) + { + var comparison = new BowlingScorecardComparison + { + OversUnchanged = new List(innings.OversBowled) + }; + _bowlingScorecardComparer.Setup(x => x.CompareScorecards(It.IsAny>(), It.IsAny>())).Returns(comparison); + return comparison; + } + + [Fact] + public async Task UpdateCloseOfPlay_throws_MatchNotFoundException_for_match_id_that_does_not_exist() + { + var repository = CreateRepository(); + + await Assert.ThrowsAsync( + async () => await repository.UpdateCloseOfPlay(new Stoolball.Matches.Match + { + MatchId = Guid.NewGuid(), + Awards = new List + { + new MatchAward { + Award = new Award { AwardName = StatisticsConstants.PLAYER_OF_THE_MATCH_AWARD }, + PlayerIdentity = new PlayerIdentity{ Team = new Team{ TeamId = Guid.NewGuid() } } + } + } + }, + _memberKey, + _memberName + ).ConfigureAwait(false)).ConfigureAwait(false); + } + + [Fact] + public async Task UpdateCloseOfPlay_throws_AwardNotFoundException_for_match_award_award_name_that_does_not_exist() + { + var repository = CreateRepository(); + + await Assert.ThrowsAsync( + async () => await repository.UpdateCloseOfPlay(new Stoolball.Matches.Match + { + MatchId = _databaseFixture.TestData.Matches[0].MatchId, + Awards = new List + { + new MatchAward { + Award = new Award { AwardName = Guid.NewGuid().ToString() }, + PlayerIdentity = new PlayerIdentity{ Team = new Team{ TeamId = Guid.NewGuid() } } + } + } + }, + _memberKey, + _memberName + ).ConfigureAwait(false)).ConfigureAwait(false); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task UpdateCloseOfPlay_updates_or_preserves_match_name_depending_on_saved_setting(bool updateMatchName) + { + var repository = CreateRepository(); + + var matchToUpdate = _databaseFixture.TestData.Matches.First(x => x.UpdateMatchNameAutomatically == updateMatchName); + var nameBefore = matchToUpdate.MatchName; + var nameAfter = "Updated match name"; + _matchNameBuilder.Setup(x => x.BuildMatchName(It.Is(m => m.MatchId == matchToUpdate.MatchId))).Returns(nameAfter); + + var result = await repository.UpdateCloseOfPlay(matchToUpdate, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Equal(updateMatchName ? nameAfter : nameBefore, result.MatchName); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedMatchName = await connection.QuerySingleAsync( + $"SELECT MatchName FROM {Tables.Match} WHERE MatchId = @MatchId", + new + { + matchToUpdate.MatchId + }).ConfigureAwait(false); + + Assert.Equal(updateMatchName ? nameAfter : nameBefore, savedMatchName); + } + } + + [Fact] + public async Task UpdateCloseOfPlay_saves_match_result() + { + var repository = CreateRepository(); + + var matchToUpdate = _databaseFixture.TestData.Matches.First(x => x.UpdateMatchNameAutomatically == false); + var matchResultBefore = matchToUpdate.MatchResultType; + var matchResultAfter = matchResultBefore == MatchResultType.HomeWin ? MatchResultType.AwayWin : MatchResultType.HomeWin; + var modifiedMatch = new Stoolball.Matches.Match + { + MatchId = matchToUpdate.MatchId, + MatchName = matchToUpdate.MatchName, + MatchResultType = matchResultAfter + }; + + var result = await repository.UpdateCloseOfPlay(modifiedMatch, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Equal(matchResultAfter, result.MatchResultType); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedMatchResult = await connection.QuerySingleAsync( + $"SELECT MatchResultType FROM {Tables.Match} WHERE MatchId = @MatchId", + new + { + matchToUpdate.MatchId + }).ConfigureAwait(false); + + Assert.Equal(matchResultAfter, Enum.Parse(typeof(MatchResultType), savedMatchResult)); + } + } + + [Fact] + public async Task UpdateCloseOfPlay_updates_existing_award() + { + var repository = CreateRepository(); + + // If you change the award or the player identity, that's probably a different award. Only changing the reason is definitely the same award. + var matchToUpdate = _databaseFixture.TestData.Matches.First(x => x.Awards.Count > 0); + var awardBefore = matchToUpdate.Awards[0]; + var awardAfter = new MatchAward + { + AwardedToId = matchToUpdate.Awards[0].AwardedToId, + Award = matchToUpdate.Awards[0].Award, + PlayerIdentity = matchToUpdate.Awards[0].PlayerIdentity, + Reason = matchToUpdate.Awards[0].Reason + Guid.NewGuid().ToString() + }; + var modifiedMatch = new Stoolball.Matches.Match + { + MatchId = matchToUpdate.MatchId, + MatchName = matchToUpdate.MatchName, + Awards = new List { awardAfter } + }; + + var result = await repository.UpdateCloseOfPlay(modifiedMatch, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Equal(modifiedMatch.Awards.Count, result.Awards.Count); + Assert.Equal(modifiedMatch.Awards[0].AwardedToId, result.Awards[0].AwardedToId); + Assert.Equal(modifiedMatch.Awards[0].Reason, result.Awards[0].Reason); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedMatchAward = await connection.QuerySingleOrDefaultAsync( + @$"SELECT AwardedToId FROM {Tables.AwardedTo} ma + WHERE AwardedToId = @AwardedToId + AND MatchId = @MatchId + AND AwardId = @AwardId + AND PlayerIdentityId = @PlayerIdentityId + AND Reason = @Reason", + new + { + awardAfter.AwardedToId, + matchToUpdate.MatchId, + awardAfter.Award!.AwardId, + awardAfter.PlayerIdentity!.PlayerIdentityId, + awardAfter.Reason + }).ConfigureAwait(false); + + Assert.NotNull(savedMatchAward); + } + + } + + [Fact] + public async Task UpdateCloseOfPlay_adds_new_award() + { + var repository = CreateRepository(); + var reason = "A good reason"; + + var matchToUpdate = SetupMatchWithNewAward(reason); + + var result = await repository.UpdateCloseOfPlay(matchToUpdate, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Single(result.Awards.Select(aw => aw.PlayerIdentity?.PlayerIdentityId == matchToUpdate.Awards[0].PlayerIdentity!.PlayerIdentityId && aw.Reason == reason)); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var savedAwardId = await connection.QuerySingleOrDefaultAsync( + $@"SELECT ma.AwardedToId + FROM {Tables.AwardedTo} ma + WHERE ma.MatchId = @MatchId AND ma.PlayerIdentityId = @PlayerIdentityId AND ma.Reason = @reason", + new + { + matchToUpdate.MatchId, + matchToUpdate.Awards[0].PlayerIdentity!.PlayerIdentityId, + reason + }).ConfigureAwait(false); + + Assert.NotNull(savedAwardId); + } + } + + private Stoolball.Matches.Match SetupMatchWithNewAward(string reason) + { + return new Stoolball.Matches.Match + { + MatchId = _databaseFixture.TestData.MatchInThePastWithFullDetails!.MatchId, + Awards = new List { + new MatchAward + { + PlayerIdentity = _databaseFixture.TestData.PlayerIdentities.First(pi => _databaseFixture.TestData.MatchInThePastWithFullDetails.Teams + .Where(t => t.Team != null).Select(t => t.Team!.TeamId) + .Contains( pi.Team?.TeamId) ), + Award = _databaseFixture.TestData.MatchInThePastWithFullDetails.Awards[0].Award, + Reason = reason + } + } + }; + } + + [Fact] + public async Task UpdateCloseOfPlay_deletes_removed_award() + { + var repository = CreateRepository(); + + var matchToUpdate = _databaseFixture.TestData.Matches.First(m => m.Awards.Count > 1); + var copyOfMatch = new Stoolball.Matches.Match + { + MatchId = matchToUpdate.MatchId + }; + var awardToRemove = matchToUpdate.Awards[0]; + for (var i = 1; i < matchToUpdate.Awards.Count; i++) + { + copyOfMatch.Awards.Add(matchToUpdate.Awards[i]); + } + + var result = await repository.UpdateCloseOfPlay(copyOfMatch, _memberKey, _memberName).ConfigureAwait(false); + + Assert.Equal(copyOfMatch.Awards.Count, result.Awards.Count); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var totalAwardsForMatch = await connection.QuerySingleAsync( + $"SELECT COUNT(ma.AwardedToId) FROM {Tables.AwardedTo} ma WHERE ma.MatchId = @MatchId", + new { matchToUpdate.MatchId } + ).ConfigureAwait(false); + + Assert.Equal(copyOfMatch.Awards.Count, totalAwardsForMatch); + + var removedAward = await connection.QuerySingleOrDefaultAsync( + $"SELECT ma.AwardedToId FROM {Tables.AwardedTo} ma WHERE ma.MatchId = @MatchId AND ma.PlayerIdentityId = @PlayerIdentityId AND ma.Reason = @Reason", + new + { + matchToUpdate.MatchId, + awardToRemove.PlayerIdentity?.PlayerIdentityId, + awardToRemove.Reason + } + ).ConfigureAwait(false); + + Assert.Null(removedAward); + } + } + + [Fact] + public async Task UpdateCloseOfPlay_updates_player_statistics() + { + var repository = CreateRepository(); + + var matchToUpdate = SetupMatchWithNewAward(string.Empty); + + _ = await repository.UpdateCloseOfPlay(matchToUpdate, _memberKey, _memberName).ConfigureAwait(false); + + _statisticsRepository.Verify(x => x.UpdatePlayerStatistics(It.IsAny>(), It.IsAny()), Times.Once()); + } var memberKey = Guid.NewGuid(); var memberName = "Dee Leeter"; - var repo = new SqlServerMatchRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - Mock.Of(), - Mock.Of(), - sanitizer.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of()); - - await repo.DeleteMatch(_databaseFixture.TestData.MatchInThePastWithFullDetails!, memberKey, memberName).ConfigureAwait(false); + [Fact] + public async Task Delete_match_succeeds() + { + var repo = CreateRepository(); + + await repo.DeleteMatch(_databaseFixture.TestData.MatchInThePastWithFullDetails!, _memberKey, _memberName).ConfigureAwait(false); using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) { @@ -69,6 +1078,7 @@ public async Task Delete_match_succeeds() Assert.Null(result); } } + public void Dispose() => _scope.Dispose(); } } diff --git a/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac b/Stoolball.Data.SqlServer.IntegrationTests/StoolballIntegrationTests.dacpac index fa458c9f75e6f931c715ebffc8cd4faa5bed49c2..1ef1dd8113ec4a62d9a1c7afa6c6003ec5d8a856 100644 GIT binary patch delta 26724 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 YzS!HT$Ri>Dd;RL=Jbbz8IsLo)FOjQ*K>z>% delta 26261 zcmX_nQ*||<5Nri$FmQAL02&%_U`VMpa>5pLWDEeTTgGD(0~5-_J2qIdrmm(BG@BUEdiI<7 zQg{>T)TJO2YbR4lFmx}TeBZDDb^wO(s=l82I> zOor*PqMiLcO1vHD*T{m3*vNv0A?GLCyY5eRv)d$<2iP_hnLkTYcT#R&XMb2_YQ2C* z2?<`$1Tjx9r;4~k2F_8}if5E8q26zagj>WtoqfDg9G|}F6ua?+36QX^pJTjH0W^(+ z2Z7)`jZ3cd*KDrBB(qr5b$6769K7eOkDpv$>6zds(<4{5qL~He;fZ^iPIkdH(7XnZ z=K?;ZzFyHRV5VjW1&qMo$Fs=o0c4(I+p~IG`(e&fc=B@H6r8Ye0&okX z#BMK{p1zJ^_7m6cUM@aQCZ3-HK!%+6DWvPRj?bHH2C zELCrGNSN0x(YI%}4D2qVOI){z&LUX}BuRCX?=lY20%sPxrYDD|0@+~RRsM`So=2jPXbPZ6)*JdYjV~)>>Ol zrtqTs7d4l)7g=^i8C`B3e~W_hu`{+2@M0m?@vPQHh06X~&zL}yJB9M30r{K*_zDml z^(-j#Ghl>QkYVt$M4!|3zCv4tak2$A=BkbIN@gBP!}H-Zjm`n&cF zC>C%t4ZED%qAu1z6UP0(rKVvwP=L;tg+9XOOvLn%Ig?wwAR?!0) zbieZ|f&zl%N%v}Ddu*fLq|AL<_^Z$swxwuiu+#5G7~gZL7ySSW1k!JJ#}!j-%;X?| z;Wr%iB%WO?U$a$6Fk(4Rt7B0RRkN7Yk%Ttk5QV~?e;0XqViJ%`xXaNfCxE(Hov!aM z9;DrMtxR@oD_&j_64xqT@!cn7m=e=u>zRf=GGKZct6VzLGN|5RshmmUybZB$W5&^8 z554e_x2_qGy9K$JtS^L9>H>>~BDi)&4xQy(?vs6i5%J{mC4se`k^e z05pTzV;*xkHv-f6*YWDAUqxz-K6u6Zm_?;!7R1YVePIv5<-G_uCnYen_XlUT`|F#7 z`)71ii9FB$-m@WQpZFC;M>m;Jo~m(G@;|bW)BRr6@hggPv%GE>V1aIe}}f{&fs3zs~y?8Iz6ke4$kRN8L4woc*)rd2H&Fr#Wq$! z5LDwe7%33JYy-{N2E#ksEL3ZyN;-aQB0MX|Tttl8LP4Uz1n6OHX3dLv29OYw_?JKU z)jqr=2u$mUiR1L=Z$Qcv#Y9WGMXH#%YAbD$uYn1q?L)7PfNSNBIdk#SFZzL^;p*0L z@nT{1j#GO}jr04>lN*o=*EPBTbvIQ&a02~KWstpEv#3+8@Bt-_liXx*bTwFjBLRI^ zZ|+abUS=X%b*h-00g0T-rQV1}6slo4T#rGh%-khU0)9gb`F+UHNqYdYUGyl>o?(#%l_@3QqXyw97rTj&x<1k+08;vjB-Z&PzOPs`!`eEJR z8|%WmPRFbMA9P9I>0T3fD(|XQABBl(ZFAW zun>bMNT+>w>F{Vf3g^F{^z89gP$g;dKZr@}%^_}ryk|`EyN4$dYpfuM0Sk84(1OIP z6a7pvS)qLPEanE-bB_tCxnM?MQm_z4uUpa zCfo5d?!RjN$YY`M>Yi#h`%RHiKU8zfz3j0xN1stZ4g}I9XGuTi z3rARbd5g-fQJTZ>gj}HW^;ke-&jx@@$u?j0v8Z_uMj1d9uf=c;FW2-Qc+)nGW4E&L zc*(v;AN*Ey@BSN$d8=3}Ggr^P7Z5A+0|Yg4Ely4jv_3CUdl!9TZt6onV~Xy6zjSbS zJq8TPW79-6`gK=L-p+LSw>WSGIr2Uv&rszViqSPQ)5s9b;dq3KONe0{nyYlf6RZm| z@gX9f$hmK~EOekjU}!eXRKXY(G8=RM@891PY7j=YriWOV`NH*%a?6X;>00MSZHqtT zMqUVtd>uu66GXyJPI0wHO3;f7s38tOUwvPAWvd4mPXnlXPRCYUg7$%_=w`#rP)Qi3 zrJskPzz#m{?CFx0p6|`j?a$q7;P(X5bpz5}J37aut=t_R!%cvR>F=w}Or5GH^SR}O z%#irelXEE{anx!Ta;Y_=>rwR%d10uZJzC?!=TgazzG!g{*L-PK+To~j|9yhJ_&DE3 zy559u^f8FbeW*#U^gzq%c=Cw$wEX?4`Y&h_+c6+>der87dQ{0GckYuD`J-5Z+-K;i zDcus|jMlh|Jx|3t@Sq!r$9-DfyF)t-EaZapE+UiqO$C{zq+Z2SAJ;=f35!MZc8x+p z5I)18t|KNVi@{rwBywsxQxYzBK1HqBuFe_pYft*_3#%jD4QTvmOv_%)D@mAmyN?U? zhO{OXo@%G+QBPh(PU6+NRa*?xzp2+M+%TSaB^x!fijzp}yh+eeJzL3KsQoZ2Gd{#K zUBA*pi6tLjRXyQ&P;X~(Q3s=6B1tc&=EFTU3RDuAIwMgY)&xPcy+2AKow&C@g?KQE zGt;{}NLPuG0oEws34|&F7f8-K%$M?&$kG`E+2pR5T#~8~$y2I)Cm^V*Z4Sd*AMIU{ z9p#0qo2q%4TC)2Q4X-38o0jo4C%vZ=MenhpWs5$>X%~IYFAz$sN70@30i1D;u}Vi; z>cw%gn!IWaG)`4C%A*_1@s~9F00FP7>u-SLRvfJ!Fd#J==3B9WMH&WR8Ie|$seO~C zjG(agel+((Y0=P8>oh@bN@a^$s*{}M5LzjRuluiPnOF30hJHCzh8TxTcMz{>i=dP) zF|*k1h4ZCE@_R9^RsWQ!5H-<6k zd;7xztom%po9tl*f2Sy)O1D~#D%1IQAKj?H4No*_lxJR<0zYqTGSjoSa^HD~84S_p z2GX^ooP`23q=dwXOYnjF5;u=}n^>Lh-Q*WdBFL71!<3Hu&cwnTFC zI^;p53AS2ZrV{H2XhQ;-NpCP|w0oB`BPTD`sw~v5bPSRB>?sjj>uWXHgL*FyDVYHX zpf8C)iAk#@p3{MB1%>uvB|KD%A((U=%f^_GSWgP~0}5P?rw@8DUv;vSeYwk`V*SOB zA+A;%_I7ymQT0;orCvq8nsn>F|-1^-ysx?tpJJm@;p+)f?RHN$q^WLtIq@J zECzVKad~UoR{*aGQ1<)|mW8OX6&{cZ1a+*5yAMm@l%BHAx7k*cU!!frkZkYLGu||P zPmpB=34)ap6QqrFb`qqT=Jg&t0_(LbLxs@vFD$rW@ij;o8v$LxKE=XFq`j}l4(=!9 z(iYT>+G{NhRwpz3NVXk;Mt$=KLG#P^O(^<}8AyR-Ze^a9&tdqhy#6kwk+_KpDAB4j z%S0wOYiSdONZFJ?r>_J$Kdm|;6?Z9mg4=c{c22I@SqVLF!$_fjm-QlaUDz}})?}@$ zDLmyi*w${xuOe5kIv|B?<8ThKlminU$1X;W<(5o)P>&7OqKYPs?woCLiCtsid(g|& z7jSf59M(F~XyyG&8u=YweDHSv-Ov-(-a0S!rc@ClpSOri@9Uon7VdT>D9We z3y}sXlv*3fHDq=xnmSpKG%2_l(0wL7RP}xZJ()Gw5#Cjf1n=j3rKSjTmYpeQH_ZJ^ z;RSosSz_k{O@Y+$;!MB`1lo(dmMkEhvb0H4=h2{JFfK#vvytS>iXCx!QTf5Yx!c8Q zBhu6$T7jx8Y2%rj#cuTW&mC3eI(E!^qQpe&;PUD z;ZNbTgeFxWTu*Q$MW%TQYeVY7{Jd^@D!WM#D%WngQtjfVUE}Nsj4e_xFHxG%ZnwmU zfWi#W!W70|Xd&%U1YR+auI*~kMa42+ZwtCJsH6qAJ^AaD>+|0Q z2t6v?>B6+osFP?AD_8~lkvg+Nz?-L+Sb2dZ2z+{q6paZ0)pSgftNDWS{l8SCfy)jy zcw78Q)KwrI)dEhpiC=?r>?{aU8$kgRiget7v+ZVo8H=V9^R$7pr6jM+H#Z zEr8blK%hznMrn%rk^kAtqtKI8Lu&*V`4Npy<3~Xgf=9MrXIOVvtVRK8HN#G5tq{6e z0XstSpm%eP*k`!yaA{}a@ZIaGA&oCVHw+UIf$(m2wL>FEcbYzkIx6Dim#KzX^Y}b^ zjCJ8DL6Y@)?=P#LXy%j>lm`s;2EIj+V0GNrx?1!BzwrCt*SuNVH27t+VQqf?ks+tX zEa{=}3}mjtkR&&a{dlky`o@V~-7^4KcO{!oJS+?P?2N+}qE`L)lhCgA6x?d@BluNu zpjoAVrI#WZH;|MKUL?or+B+X5YqZgt2Jn~dX++yY||FgQjXVClqt={~zie6=a3vuu+R9~ta zBVK(YvD4x&y~fYSp~gU~9-JulV3tHSxU^_N3`ETFd2KNNd=yf|DX>{T1ivz^)xN7U=v)}GXp`=dEbv>h7fF5DP(v2~=8k^XHm+H|- zpEptE(*%*#qbWcw&$cVx(yrnFZhsZ*OVm!>f3o?huMR4rc-J~S_2yKgeMXRax^J%1 zME)&U($YSFD+ZsdZKq=XQJo59^3XK&(HBugi&>-us;Z&>SL*zQzZV#xeaP!4)d%<0 zW3w#Ws%2#8n=ovLi?Cz{cl9`{zcT)d4(4{(Qc#zn7`4;s@T_CgGu&V=g3`v326T}di9)p}0B*lnm8^@tfKLexmE317E!^OWe^aBI@*Jbr~LtDYi6R&fs zG4qse=Z_>egRMaUMG#>BGr}aERtDD80?_I4Xqi5;6n%wO?Tmu~+TPhiptGaUo1$&i zy_{O-8ixeD8qEVf7OPOp+^7gt1w&~7IkP=$`F7JAqG_D#6`cf2$GhX?Z+`>R8?5xX z^>5;^WZ0v?rWC#%lV&@DIdxs^#KUGYux0IH1iof9J13sD3|L?@Nw(<0s8HwOnja_8 z7w#a=q~isW?sI7nV=MLICf`>71t$-hhoS01+Zh0k2Mt1tA;+G~izy>TXiC&tjhVbQ zc<5YxM`n9!l-l*n4L+<*9Hgh|i1>2e4DOLBnkJ$My7n%g*3lbdOFut`KBP2)52S=! z1LOzkf6DJ2fZs%hg|Jw7 zr<(-jXlJ2=K)bvrD(}pncY0n7%rJPFLq!A|lWTnkVU>6hm=ebxi#8L^qudi_Cq8zf zN~47#+=n5IzxrcHHQ;SI(G*h=s6JP^e*v~S54U)~LG>3*|9HZxjBvAStG^?{*Okox zD)jvv=MM@^YnDe5C={DMVNAvOh?%B0F&sVm*4QPARPc|pS>o}mLOu@t*bh)E3OtHom{H+x$x`&(|uNv#*S|nKgDq=_sTd( z<)p1l6m0qbof~TEfQU0grg*wPyP}%KSsB?VS9b&a%E8M*&ez# zwVKQRT=+|SNV>D54I z2VovL;e7A>Vuh!?Hs(YP3ARR%ncEZ+9OQ^9!E&&lw7jn7N=!@<58gkcNq(mPEYg%C zXAH&r$yYnW%K2&L44jV)1>M zBW%pNEwASj+L2{wxwjjdHfgZ4*+WivQo55{o(PMaotXvO1CPu|eVR2G5{FkNMc&f5 zw=%Osx}}RB{#MY4NM+O(JiQgSdP93m@re;Ms`?unj&n#OA>2Vy=q(U@#$#8xBR2E| zI!F+R<>>H<&<8hi_U2kc*3_YiU9q}!6?$VagkaAGo6YI7KgrPtdxszVJ1JfdI+w6& zSQ5A)qomv!AYN1n!4ph8n1$$W+9$l253(k@1Ox>O2o<9O+)#Wt<*Pup7Y#3W!BF55 zXcH~QY&l7lnNYwVPF(!uk2cFXEp%7~Rg|TBMvdu&uu%_ZtqSo92wHV|eKk>2^4oHf zir%t(F)Kt9wjPF~tzRfw(>RN12iJ6-!rJ_HD-hQAL>1*a5WDP-%wTSTI_U3H&-36p0MPWiA(CKRI7voH<$m=tEyV6JWP-GWU-W*Q=qB{X4{nye6U zmmR-)d@i$C1XZ~sL$t||4$+wR# zI3Vx2H?fcM$=oUX1@~@yM{}p__-dqC<8Of#QC+=|*VMQZrm&2nI7;)F$lBkNKWa2{ z0i3-rIDPW;7=6yJmsbhu($#RGO|dLhF(7hpGNAPTK%tD=oUGu?QNi|1)$Im-aEN}w z9GyeE^Nt{=gNf_zkgN((`eU)VzzH<%h;7VN;*}@2lhmYuKxYg}Ga-S=N+Ku6T`5rK zRr|>u$^280vNCIN$&4N^jon@yN2S4+2RKm%Sk-^N3Vz&C6Xuv7KQ+N{fTR&dR^0U_ z^&R*Jmn9Q9yqA8V=(NQqe2hu;RrJFWQ(|BQFu&|k^I{JX`wfuWfrdkLt}JtPV!nFyr2 zxb!1hlBBUpKaZj@#X$%Renznk{09ivlOlob)c16GGO&KI#9UcYNwPI!>6VB0p=>mdILAu)1|mwQlfpOAq*nj(4)GE zKkb6D4k+$Kt=MnA(&~c?{ZkJKbS`BRGqD#l{Ror0D*^8|L?+0wkOTb|(xul< zZ%pm>$QI_n11kfx=tGkN+2#ZLBO8H|iEQ@8W=~WTs-jn=)b8$M{!c(x_wX2Y<^05c zeYp%?KtN^xo+yw!?G8IlQIRgE9Is4u450ETG({gJOXGzw>dLM$r$&tpR&zSjH825d z&-puc#`e{ie6C8WB&;67QF{!Z1t8;HZrMOzZ465UmspLE8&hc%nM^90TuD6|kaF3G zR2o)o-*{)pFXgS{qs^!Hduk$C5hPa#>N=BLN|SPE(-%YrEMai2iCz_X19mM2lGMkqO94Vog} zDLSV^zP2IvlU@f7;T2$XExS|?YVsl&UXwh2zSMK81q=%8L{mz8BX0gN>Kyi$wmjR# zA5gX9-?P~6FTOMp1FGw%_ z_1;J|F{({D#4w|FegUUxtbW@N5>Y5W>Z|E>dJt%32g!#OFIXLn5^kFv0O5nFvqUzfw^nH7PddeERIQTd) zpY}N(8C$#9JNo!|UOWB^?3wA|>FG@O&P)>E?CSntwn!W~Xz9t08!-c=e4a`kF!prd zeFc7AM^5v;`g`~5&Yl+mD1l=P-v*8zd~0u)4QoM|Z%7XtBa|zNGv5O~?%vEiZv!pS zZ>;5m-)^i~S+lS@H=^T&bRQfg+ou&4a|I3JKu>rrFTy)enR?@~iuOM0v-K(ar&0J4o zdR}P6=wt4(N*!Jc3eFQxKZ0QQkM_)cT|JmAaG)gh^ry5&i8uQ9lgla3`EkC1^skh+ z5{ci$fBGwnHH%rgGX*+p(i;}D19o4f$bEuI?*3r=-cD0BSX->t?^BVg_n=9|0&macA_zuuy*i|L16w7{c_h{B%TvSD{SobjZ(w{W3X zlh_64|B?M#tH^0hy$5bjs?5vk~Zzk(V&^toD6Q`tm*7b)gk%U(WmZ8}2WRGN65 zzEslcd1`%mvlkB#_q~eppjI!{OmmIsE^t74qWY`FgKp-6un2ABf(x>7+aVjTWgF8& zVe0ZD&y@RIRZ(Q0^bQEYrD%@%Lo?YjUQ0_T9!?Y$Ay{uikb1n_LSCGl3?^x33IDeY zD{O3~T;S{Zh#@IfG%eM?8!?<`vRXrB-+0t)-e9Bsgf-ZbygtrTu*OsmuRG+=2|BWZi! z{r>3C6#~`51UJ#`z(*6Zbmc0c&^S1Te935d<}cQD;GCD%0C+oLu%Q!`U zy*;ufXjdd>hX_9@1iC;}P^epI@D-Scs)*tW_fv}zRAb^`?6{_;Mp5SFXXyQU@HG$f1 z;%+L;C}ZG-GS?6hlGbtXwI5k9nKqIK?4Y)q%cO$QcJKk3Pf}iku%}XkR?%p&{QaYf zu@wY?#qMSR&g8%7eo$hAuDkYNM*I0f@KS^nW1TZSo!?VV1WZxMQhudd}*;Lk= zqdr36_dtNF<`Ozo%SFwV^PyvM#FD091xR*{X(W1_iCwgQ6?(JE!Sui+6;$-Hbpl@d zUKh%u>Rn0%{?r%jX!un8{_^NR4UJ`5l!<{W$~^cu&PBrmy=mHq(437`R^Uc;tg~7D z4`|IWU494cyDHs7V>$F<*mO$wDXiK$*J5+J)6B<6sJ2Xd{Z0Gb9NY^!j4hd;R=#W;1N~sCfq+m5_oACoEyH(oUz5U*xQ!heGor4tic@DO) zNh0K$dw^YQfh0A0^bO+50Ubg|oHv2|CNS<6(=eV}JD64LGU4fi4V(e1?|gCFKBi9f z38=qB0O9%&jnivlm=e4`=|NN@10OPBC01RF)jpsEEk#d-{IZNJiE&TS$q~qL8gI)I zWvEevwH^Q+-Zh(DtJCN>H=bhss*|jiq4g;CQ=g_LRS2KbrR_yXP(s=ZeKrjlwLel5 z(-M$2iwf03;YUvR6S~kELy9l8-eC&-4u}p@UMT9%Wg*MH6qV#<2I@R-R&dW44SGj> zpocWJUT?iD^Qk@ktp%rP60Z^)B2K<*Ltgurh#Kd>0f&KE% zCd0k)1!`>3yQOdz&QjI?*i&_?!aBIfi>Jk8C=r3Ay#m~EL%cgW+X6wHk_aja@%M<6 zoyoGqf)XF8epwKCd};$xpLBs7iCsk&N@r4jLfC`{Hm#YYh@}w~lMY0*UkFyJh;5YV zFqtk{gX5XOHrs>;iOR)J3Tx0ZFb*9x0X!_++WbMu)E2B3f3~6tzuKAt|+WaYc8);B~Hoyk7ln-3A|C`c!(j zd^`PU2^(WsRq(gZA|L1h0(L~dBCjUO-hv}x`Z#&Tx^&GD4^(b7N?X3 z9-{H@?#auuTJFWegvL#bFbdvv@=^$rkYmX!f)4B@{0nJpXi{n6$FBION3Yp~RoSl` zLjtmB+_PzEW#fcdV!d5An)F{Xx=fcsKf%eOV+U3EHHHvv180}mEM$L6DB+}`L?7W= z3@_7|`^lj|P$Xw~dO|J$Ys>2Cu!W*4@!+b8IHf+xqHk)b?p9f3LrWF)8geZolu?aQ zD42GyFdPZyP?h=Ggdy{n)TA%{Eitn2qRjD1;<)CqoyXJD^!e2d6M`g93LlOk4V4xr zp=G3)#)byHGfEnY+mYlpNs=Oy>Xd6eA!I=$?#M1Ub>e4a2ZNV@(m*NZ+fo1h1FL`n z!0YkuyolJ~*=dZFtOhJH3ld@7>8h~{YebfgwIhJrX>ETkzue;lx@mzOu|`$J(VlqQ z@Yp#uR7$fUa6??Sz~cvV!ocW%9PgGWeMCZ&WpgA)N+bapCQBUbHn(Y>0D%HmQzJD= zFR&u%OCc&pz5wX?P($E5Jjd`eMrPf4MOOe!0%edKDxG2VF|Mv!Gtadhj5Iqsn=N6fGK$He!MLl)A;k{hufJXo~+FX`@J|?U!>zOLIPPlRk6~6-7>>RjcpF zfR@r_`TY4)gbWLXWnd?0JuM(&QlS+mCOb+hfOc8h4JZQpZ}&!Dc-bVBFr>do;Vd5) z@Kf4b4+MWH58%zeu85|(KSQ=}onep>t|m^EHXuO4ax9l4NF1Zl30$g36x@$55Ysy3 zq^*-yq+bmnP?C)$F&ZL>kyq%kPp>AWHCEt1Y_BVcs|O=G2Nxt`WwbplX)2$nOkvh7 zB%Svf0jC~el^+nc7Y;25er3U`E+N_)O=@X=*GVOa`yVEhe5}k zuQx}ml1@sEb|ViO>%E#4;W4lEPxE3l>Uefo8MurkPKL{$cl+h7RXtozk{%LJHZ=y~ z31c-2!LQY=;{@v4sYHfU!3JS$QlF5`fTaW2(^oz5?s^KOBmA zakEgORf8|72QE+LNDv1x0I_k;B~4;D2F41QvYo}-=e!LVR)d;l&$V%dgtAuoVWWZw zU{vi>IkX2vQ4}_BX6kAmLf`f)JOLYmn|?6fDSxE_wK9U!CU$OhG^Rf3R+$Wu5&Q{wKenPsa^9=q1TerXgrfW0AYkF{e!;<7fvHz+xH|Iw^I{-Dop0& zdVnk-0KEV(sP7qR(k**Pwfij3I?6_CmbdzmVg}R>Tfsz7Kv)%MF5{3|7ffmCKim`u zQ+J&~Fb2AO|FJg4J+e@)!+*7qIT^X?j^Xsd+1G-=-dM_$#`>q&ozxqmvI&0&{8~{v zCX@%^{3Z&pW6(E6RJ2`}fz+Sds&`qLD%MA=*N2_AilkT^2geGLh8_%ra}p8d(640bp9x`91S~q5>OanEk+s9NrGV!F=Tr74HZwP~Ok}g}puwOyaz1z&rs6^*{Kw4VO}~#T;GR zFJ9=J^t_|i36P#oq1R<^9W9mw;j4iz1yGCFI7o)X+?8q{?#J!+CJ{p-;0dv!4R$ro zIlKlH%%kHrm8>kzs)}@r%Cj;w&6Zpr05fA8uS2QoMqP6u6Y;b0gdc7zB3)=xMFbP`2BrnUqWM%@a1?iO_w#P!K2 zl%BNT0c;r$bqlc%p|0hF8zKbwnu(E8IyTWyV(wxm_$BFs)-eUwcj*MSi4S&Y{G0sj z%qXV~Ta^tW4iZqt2)}j0dkc`{wKorPW{PzV{PH-Um%i|zpXowK+6 zarEf`n(5k}U^Xf1z1}K*KP28kLj&AMK|1dB-L@*!{fMxl)*i44X#3@B2ZKNM5=F5t zFe$^<^5I#Lz)F`7Rr_l#-ZLW6_0;_=wl8@_f1M?Umui{{RS5!h^PlqO`}Gsm_=#X) zdd(rgOq<1KWJ6TxoI>q7Ge_iqp zFb<+HxsDg8-&i<<7EU#T3bJDwn^KCe8wLYRYi_F&7;(Sv_<8D-d}_)Sv~Y#Ax*$T+ ziVz!c(Jr9~s0o3}kZX;Q#(OOJJP7p|tt`vL30V4I52AgLa~SFdwAMOM!3J;DJLxgG z$F+esbQr!9;g|Y!RMf!4f@e#Oy9J*wQqCo>YYV^WEf9wo{^>fq%FjhNXyYMk*>pJA zzqnX}l0w-HBOmDFmWWfIZWdv#0Sd{#{o6Db1fx(hEYg8NH2pQdy6-^20(1GVczG5_ zJW{LVaQHA+*~kBr<()9?AeOU54a^Z<|0`k+`s@=H zUSrQXl5nT&PnlP;yGNFrJw0O(H~eP~gQD5_4s^nY9T)MKO0GUzHsv$?8rKxL86}PZ z=C>F<3=qO{nh;+33KKf%vuoJljPel&ge-`~E5phkkXdbBsKJ7duQJc3p{U)EjYWYy zZ91!y@E-$FW~y-#-uh)9cPR%i-TcPl%jB-ItZ%Y(V7}@&JssRvZ>9M`V_bcqvdb;r zaTH%7MPzDeIi3=%B6u`4nwmA;XhVmQ>%qbbj5epJkiW4FzXctBTC0z*u`{oCDxjjX z-dOP=V8%nMEgONtwuEc(ujQ*!omuqq?>9L!9>jW3u99{)R$(ZKs{ zgu9+i>u|)tahd6Iw{gO&0k%W=hl-TmmlcZYHuP2Bp*t%g) z;IFT_{3Go^(NN5mf+rfJL&M%Mu7Zdpxb#ZFq@m4JCeY4E*D9eKl^d?2%27(g_Kg5E z`pocUO3q@!3iT4RkSs=f#Rq1WP3RFO{2vX99ofw?@mc2o)UVQTQ?g{kA2{mqEui#% zks-qpBvFY*cs%hf)g>8F^K6w8SqDS$MElpU9f~ zaok9O=DvIP*vcT5>X`wiXp`W(Wm5i^*V70<}y&BVPz?{Ur4oic2=9T@hBqd!ddH4>%` zCWs&;x+x|T1d;`Bk)_oZNmdk$TIeU;FVb((S#J!Ihp%_p`3jk_s2^-M5?c<4X7LPk zYcq!*y7K$tc18$(P5pu;RDd}IuG-_jz2U9Htp9prywk>>99TU-s4pg*Rfe921I7Ej z)dCL{0;2+EHRFElfcSerRlJ6Soj>Eqrs9Y0ddCX0{2%Ibjp2zk6)W#wC*r*&DD)I}0Xj zpOlsDK1`j}2|#mY5&G5v7A~^S4hw*QG<=5phM+-*FqUvMBr2z-b8z(!?Pxsd#Mnc| zd@%>1l{x<*;yXH`BI6!|aDmmGA?V8H((4RO)YWQ~U#@bsaBXFBeljrSAH4lY9vog7 zFp-2?XQK0mDN~IJj|#HgE;2yFeHY=OltqcV2H){zl&<}EjR)j`l=U?&M>W0j);TQY zpn|L5Fi)xY0cMDYGJXJ=2<7TqoF zsz^Hz;Bp)At1|9`>7ar2l|Nf0=_EHTVv+QIy10rXnt{fM8(5L z?2!$EcHAr_j%OtNcyOIF3m1X`Vd{-?$F+6?VM!MDr2r7+%Gm3VfOrv|Ua*-4!Xj-# z2~`k96@5bv=vO1!ftR!oaB~!@78(1yFq-UP<0W>-i0kOSj zE`q?5KMEFK!%rT>8`e&lc1H;iwB8}B^~^EjF(~OxFdPo-tG2fqdgp?le_?L#FLX)3T2C&S zp$b6TjY4P7d@*x1o1Re({)Inq-DsqLh;Osoa(EaZ8 zNlAKXN(rG!yxraR1(!cIB*Q*arL#WHw@akfFgV`1O|(cDg-Ts1m0l709kEA$D~w3Y zvq|#`3T^H{I`o$DhxY)HHLg|@m7d>VLOTsO121A-8%FHB@q$51a#Z2H59MsqFz9tL zM8gn+;w0#AKZR=hG-gLNT?EfUqtQBzSzY)3I+R~PO&qM)F-yGB!!GV0Jq~633B?%k zp+@zapNvsUqX=O=7_k5%3e(2B*BnG&C9?6)&nQgSXv|CD&^b`CNOpexMgU`L9DxK7 z#%M3Dx1U-bCH#nKwZhzF^a7=eL`+>cEG3J|c%M5Lpyh9?XX@?O%ux|(mZP&ZLumi2 zJNF4AdeY`yGDDJ;6zmnY%L>Y(#>N1=q#UO?S(Mf*akZg$Nbgzv@xm_BW?QTA;?mkG zwX)@)*(4>G^~${XY2K*i5HkZ|w=)!ITkppe-)W&tgLDzrdPwKw@r-mqwo$fG6Y;h~ zIT=Bl&rrkBONr3EEGVZ9U8zdIm=wI5a>63*eqjv(P$o&WdG)>kQL5zgqkP~nGZn2tVm*&u1&WZh(5JP4=y9+RW+t-h0^_SH z>%zDS9ziNJVNc9`y28fOL`4YxnRiv#nl-r(nt?5 zKum%?ZOWN6DjFaFp`1mA`)SnW<7C|?F-ligVB=m0~=O~w!{q_GkVUNye7 zpc92G&s7REy|CTM1I_o73UuB46i9+zaW0PyGFQd(6#m75Kg8)FjBY}0^}nFQ4gS+= z9k+?ES`qfYuhBcL}`jl85+noo*@(32>{=)wp)nr_j!>R-Ct6v6eQQRE; z>*U<%HCh9zw7Z4L1g>^S_y}~vKW&oMvzWYR4klodRN5r2)0u}23kq!!<@-rE0=6HU& z(I?CHh3%S_xQI%BDCB>{DAF-n-S^ke1nEr18JSSJMZsLH|JCD8NMt8tsdYd5B8^6S zV?p?6d_+Z4x4A@p(Y1D&WLraiR%uK?*z$^WAEgIDT6W(}ht_GxvRx+v=<=xcmf69p z0txVMF4h?HtOKYge_9c8v~Xx=H99FXta!4orLLAq7W8>#cTkc7maX_OR?1>s{Cp0sP4c$$y$ZBf_G*$MZxVmK{zW zc!v!yb~C4b?QgY+dk$|bFICVk#Ukhi$F}+$0War!tkEsuwHy+t?ciNf=xyQML?L=1 zV{ASD2fC!M@1RrOu0Y~#u{Gd-^3LZ!%=c#Ykw5i@01k#A3-(#e&e=OCRE-H7(}hl3 z@yFR`kIT4tsF5+4-p%=~(>S}S63Q`oZRQ(i;FYtowxZ|0HS%!(9Z`@fotxj@(L-fR z{P@Ag`CaAuwNeJa13)1{r3wjh=lNiaDYj}AgvI0tBF#21@RcaGa*N?k35_@ywG~RD z#CTmawM_MOhxieW3 z7CkM{8`+JmmQ%7rAmeWhsN*;jPro%sA}^W|KNiQDSy_B7@W9Y}qCTMB6qadS6WK!Q=qtHM$_#&1<);&Cog(&0|uQv7z9;Agg&ToTx@&qi;5NPa; z<-EAqxk({tu0EnLBYFIJ-#cUx2aP-)YVa6gL;W^@e^c`yS45(9`V9`baUOJ|zYCEu zgT8v&npn_W$G8*&86i-mkDgu&tK