From 0febd3e8b4e6534d5fc8c0948d360c96ab3a838d Mon Sep 17 00:00:00 2001 From: Rick Mason Date: Thu, 22 Sep 2022 20:02:55 +0100 Subject: [PATCH] Complete tests for CreateTournament method #484 --- .../SqlServerTournamentRepositoryTests.cs | 316 +++++++++++------- .../SqlServerPlayerRepositoryUnitTests.cs | 12 +- .../SqlServerTournamentRepositoryUnitTests.cs | 112 +++++++ Stoolball.Data.SqlServer/DapperWrapper.cs | 15 +- Stoolball.Data.SqlServer/IDapperWrapper.cs | 24 +- .../SqlServerPlayerRepository.cs | 2 +- .../SqlServerTournamentRepository.cs | 59 ++-- 7 files changed, 386 insertions(+), 154 deletions(-) create mode 100644 Stoolball.Data.SqlServer.UnitTests/SqlServerTournamentRepositoryUnitTests.cs diff --git a/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerTournamentRepositoryTests.cs b/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerTournamentRepositoryTests.cs index dae2e7cb..db91f98f 100644 --- a/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerTournamentRepositoryTests.cs +++ b/Stoolball.Data.SqlServer.IntegrationTests/Matches/SqlServerTournamentRepositoryTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Transactions; @@ -7,9 +8,11 @@ using Dapper; using Ganss.XSS; using Moq; +using Stoolball.Competitions; using Stoolball.Data.SqlServer.IntegrationTests.Fixtures; using Stoolball.Logging; using Stoolball.Matches; +using Stoolball.MatchLocations; using Stoolball.Routing; using Stoolball.Teams; using Xunit; @@ -22,11 +25,167 @@ public class SqlServerTournamentRepositoryTests : IDisposable { private readonly SqlServerTestDataFixture _databaseFixture; private readonly TransactionScope _scope; + private readonly Mock _auditRepository = new(); + private readonly Mock> _logger = new(); + private readonly Mock _routeGenerator = new(); + private readonly Mock _redirectsRepository = new(); + private readonly Mock _teamRepository = new(); + private readonly Mock _matchRepository = new(); + private readonly Mock _htmlSanitizer = new(); + private readonly Mock _copier = new(); public SqlServerTournamentRepositoryTests(SqlServerTestDataFixture databaseFixture) { _databaseFixture = databaseFixture ?? throw new ArgumentNullException(nameof(databaseFixture)); _scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled); + + _htmlSanitizer.Setup(x => x.AllowedTags).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedAttributes).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedCssProperties).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedAtRules).Returns(new HashSet()); + } + + private SqlServerTournamentRepository CreateRepository() + { + return new SqlServerTournamentRepository( + _databaseFixture.ConnectionFactory, + new DapperWrapper(), + _auditRepository.Object, + _logger.Object, + _routeGenerator.Object, + _redirectsRepository.Object, + _teamRepository.Object, + _matchRepository.Object, + _htmlSanitizer.Object, + _copier.Object); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task Create_tournament_inserts_basic_fields(bool hasLocation) + { + var tournament = new Tournament + { + TournamentName = "Tournament name", + TournamentLocation = hasLocation ? new MatchLocation { MatchLocationId = _databaseFixture.TestData.MatchLocations.First().MatchLocationId } : null, + PlayerType = PlayerType.JuniorBoys, + PlayersPerTeam = 10, + QualificationType = TournamentQualificationType.OpenTournament, + StartTime = new DateTimeOffset(2022, 8, 1, 12, 30, 0, TimeSpan.FromHours(1)), // deliberately British Summer Time + StartTimeIsKnown = true, + TournamentNotes = "

h1 not allowed but is allowed

", + MemberKey = Guid.NewGuid() + }; + + var expectedRoute = "/tournaments/example-tournament"; + _routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", tournament.TournamentName + " " + tournament.StartTime.Date.ToString("dMMMyyyy", CultureInfo.CurrentCulture), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(expectedRoute)); + + SetupEntityCopierMock(tournament, tournament, tournament); + var sanitizedNotes = tournament.TournamentNotes.Replace("

", string.Empty).Replace("

", string.Empty); + _htmlSanitizer.Setup(x => x.Sanitize(tournament.TournamentNotes, string.Empty, null)).Returns(sanitizedNotes); + + var repo = CreateRepository(); + + var createdTournament = await repo.CreateTournament(tournament, tournament.MemberKey.Value, "Member name").ConfigureAwait(false); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var result = await connection.QuerySingleOrDefaultAsync(@$" + SELECT TournamentName, PlayerType, PlayersPerTeam, QualificationType, StartTime, StartTimeIsKnown, TournamentNotes, TournamentRoute, MemberKey + FROM {Tables.Tournament} WHERE TournamentId = @TournamentId", new { createdTournament.TournamentId }); + + Assert.NotNull(result); + Assert.Equal(tournament.TournamentName, result.TournamentName); + Assert.Equal(tournament.PlayerType.ToString(), result.PlayerType.ToString()); + Assert.Equal(tournament.PlayersPerTeam, result.PlayersPerTeam); + Assert.Equal(tournament.QualificationType.ToString(), result.QualificationType.ToString()); + Assert.Equal(tournament.StartTime.UtcDateTime, result.StartTime.UtcDateTime); + Assert.Equal(tournament.StartTimeIsKnown, result.StartTimeIsKnown); + Assert.Equal(sanitizedNotes, result.TournamentNotes); + Assert.Equal(expectedRoute, result.TournamentRoute); + Assert.Equal(tournament.MemberKey, result.MemberKey); + + var matchLocationId = await connection.ExecuteScalarAsync(@$"SELECT MatchLocationId FROM {Tables.Tournament} WHERE TournamentId = @TournamentId", new { createdTournament.TournamentId }); + if (hasLocation) + { + Assert.Equal(tournament.TournamentLocation!.MatchLocationId, matchLocationId); + } + else + { + Assert.Null(result.TournamentLocation); + } + } + + } + + [Fact] + public async Task Create_tournament_inserts_teams() + { + var tournament = new Tournament + { + TournamentName = "Example tournament", + StartTime = DateTime.Now.AddDays(1), + TournamentRoute = "/tournaments/example-tournament", + Teams = _databaseFixture.TestData.Teams.Take(2).Select(x => new TeamInTournament { Team = new Team { TeamId = x.TeamId }, TeamRole = TournamentTeamRole.Confirmed, PlayingAsTeamName = x.TeamName + "xxx" }).ToList() + }; + + _routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + + SetupEntityCopierMock(tournament, tournament, tournament); + + var repo = CreateRepository(); + + var createdTournament = await repo.CreateTournament(tournament, Guid.NewGuid(), "Member name"); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var results = await connection.QueryAsync<(Guid teamId, string teamRole, string playingAs)?>(@$" + SELECT TeamId, TeamRole, PlayingAsTeamName + FROM {Tables.TournamentTeam} WHERE TournamentId = @TournamentId", new { createdTournament.TournamentId }); + + Assert.Equal(tournament.Teams.Count, results.Count()); + foreach (var team in tournament.Teams) + { + var result = results.SingleOrDefault(x => x?.teamId == team.Team.TeamId); + Assert.NotNull(result); + + Assert.Equal(team.TeamRole.ToString(), result?.teamRole); + Assert.Equal(team.PlayingAsTeamName, result?.playingAs); + } + } + } + + + [Fact] + public async Task Create_tournament_inserts_seasons() + { + var tournament = new Tournament + { + TournamentName = "Example tournament", + StartTime = DateTime.Now.AddDays(1), + TournamentRoute = "/tournaments/example-tournament", + Seasons = _databaseFixture.TestData.Seasons.Take(2).Select(x => new Season { SeasonId = x.SeasonId }).ToList() + }; + + _routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + + SetupEntityCopierMock(tournament, tournament, tournament); + + var repo = CreateRepository(); + + var createdTournament = await repo.CreateTournament(tournament, Guid.NewGuid(), "Member name"); + + using (var connection = _databaseFixture.ConnectionFactory.CreateDatabaseConnection()) + { + var results = await connection.QueryAsync(@$"SELECT SeasonId FROM {Tables.TournamentSeason} WHERE TournamentId = @TournamentId", new { createdTournament.TournamentId }); + + Assert.Equal(tournament.Seasons.Count, results.Count()); + foreach (var season in tournament.Seasons) + { + Assert.Contains(season.SeasonId!.Value, results); + } + } } [Fact] @@ -42,19 +201,11 @@ public async Task Create_tournament_should_insert_default_overset_if_overs_speci TournamentRoute = "/tournaments/example-tournament" }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, tournament, tournament).Object); + SetupEntityCopierMock(tournament, tournament, tournament); + + var repo = CreateRepository(); var createdTournament = await repo.CreateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -71,6 +222,7 @@ public async Task Create_tournament_should_insert_default_overset_if_overs_speci } } + [Fact] public async Task Create_tournament_should_not_insert_default_overset_if_overs_not_specified() { @@ -82,19 +234,11 @@ public async Task Create_tournament_should_not_insert_default_overset_if_overs_n TournamentRoute = "/tournaments/example-tournament" }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute("/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, tournament, tournament).Object); + SetupEntityCopierMock(tournament, tournament, tournament); + + var repo = CreateRepository(); var createdTournament = await repo.CreateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -111,7 +255,7 @@ public async Task Create_tournament_should_not_insert_default_overset_if_overs_n [Fact] public async Task Update_tournament_should_update_existing_default_overset_if_overs_specified() { - var tournament = _databaseFixture.TestData.TournamentWithFullDetails; + var tournament = _databaseFixture.TestData.TournamentWithFullDetails!; var auditable = new Tournament { @@ -130,19 +274,11 @@ public async Task Update_tournament_should_update_existing_default_overset_if_ov } }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, auditable, auditable).Object); + SetupEntityCopierMock(tournament, auditable, auditable); + + var repo = CreateRepository(); _ = await repo.UpdateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -163,7 +299,7 @@ public async Task Update_tournament_should_update_existing_default_overset_if_ov [Fact] public async Task Update_tournament_in_the_future_should_update_existing_match_overset_if_overs_specified() { - var tournament = _databaseFixture.TestData.TournamentWithFullDetails; + var tournament = _databaseFixture.TestData.TournamentWithFullDetails!; var auditable = new Tournament { @@ -182,19 +318,11 @@ public async Task Update_tournament_in_the_future_should_update_existing_match_o } }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, auditable, auditable).Object); + SetupEntityCopierMock(tournament, auditable, auditable); + + var repo = CreateRepository(); _ = await repo.UpdateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -222,7 +350,7 @@ public async Task Update_tournament_in_the_future_should_update_existing_match_o [Fact] public async Task Update_tournament_in_the_past_should_not_update_match_overset_if_overs_specified() { - var tournament = _databaseFixture.TestData.TournamentWithFullDetails; + var tournament = _databaseFixture.TestData.TournamentWithFullDetails!; var auditable = new Tournament { @@ -241,19 +369,11 @@ public async Task Update_tournament_in_the_past_should_not_update_match_overset_ } }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, auditable, auditable).Object); + SetupEntityCopierMock(tournament, auditable, auditable); + + var repo = CreateRepository(); _ = await repo.UpdateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -281,7 +401,7 @@ public async Task Update_tournament_in_the_past_should_not_update_match_overset_ [Fact] public async Task Update_tournament_should_clear_default_overset_if_overs_not_specified() { - var tournament = _databaseFixture.TestData.TournamentWithFullDetails; + var tournament = _databaseFixture.TestData.TournamentWithFullDetails!; var auditable = new Tournament { @@ -292,19 +412,11 @@ public async Task Update_tournament_should_clear_default_overset_if_overs_not_sp DefaultOverSets = new List() }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, auditable, auditable).Object); + SetupEntityCopierMock(tournament, auditable, auditable); + + var repo = CreateRepository(); _ = await repo.UpdateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -322,7 +434,7 @@ public async Task Update_tournament_should_clear_default_overset_if_overs_not_sp [Fact] public async Task Update_tournament_in_the_future_should_not_clear_match_overset_if_overs_not_specified() { - var tournament = _databaseFixture.TestData.TournamentWithFullDetails; + var tournament = _databaseFixture.TestData.TournamentWithFullDetails!; var auditable = new Tournament { @@ -333,19 +445,11 @@ public async Task Update_tournament_in_the_future_should_not_clear_match_overset DefaultOverSets = new List() }; - var routeGenerator = new Mock(); - routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); + _routeGenerator.Setup(x => x.GenerateUniqueRoute(tournament.TournamentRoute, "/tournaments", It.IsAny(), NoiseWords.TournamentRoute, It.IsAny>>())).Returns(Task.FromResult(tournament.TournamentRoute)); - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - routeGenerator.Object, - Mock.Of(), - Mock.Of(), - Mock.Of(), - CreateHtmlSanitizerMock().Object, - CreateEntityCopierMock(tournament, auditable, auditable).Object); + SetupEntityCopierMock(tournament, auditable, auditable); + + var repo = CreateRepository(); _ = await repo.UpdateTournament(tournament, Guid.NewGuid(), "Member name"); @@ -373,25 +477,15 @@ public async Task Update_tournament_in_the_future_should_not_clear_match_overset [Fact] public async Task Delete_tournament_succeeds() { - var auditable = new Tournament { TournamentId = _databaseFixture.TestData.TournamentWithFullDetails.TournamentId }; + var auditable = new Tournament { TournamentId = _databaseFixture.TestData.TournamentWithFullDetails!.TournamentId }; var redacted = new Tournament { TournamentId = _databaseFixture.TestData.TournamentWithFullDetails.TournamentId }; - var sanitizer = CreateHtmlSanitizerMock(); - var copier = CreateEntityCopierMock(_databaseFixture.TestData.TournamentWithFullDetails, auditable, redacted); + SetupEntityCopierMock(_databaseFixture.TestData.TournamentWithFullDetails, auditable, redacted); var memberKey = Guid.NewGuid(); var memberName = "Dee Leeter"; - var repo = new SqlServerTournamentRepository( - _databaseFixture.ConnectionFactory, - Mock.Of(), - Mock.Of>(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - Mock.Of(), - sanitizer.Object, - copier.Object); + var repo = CreateRepository(); await repo.DeleteTournament(_databaseFixture.TestData.TournamentWithFullDetails, memberKey, memberName); @@ -402,22 +496,10 @@ public async Task Delete_tournament_succeeds() } } - private Mock CreateEntityCopierMock(Tournament original, Tournament auditable, Tournament redacted) - { - var copier = new Mock(); - copier.Setup(x => x.CreateAuditableCopy(original)).Returns(auditable); - copier.Setup(x => x.CreateAuditableCopy(auditable)).Returns(redacted); - return copier; - } - - private static Mock CreateHtmlSanitizerMock() + private void SetupEntityCopierMock(Tournament original, Tournament auditable, Tournament redacted) { - 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()); - return sanitizer; + _copier.Setup(x => x.CreateAuditableCopy(original)).Returns(auditable); + _copier.Setup(x => x.CreateRedactedCopy(auditable)).Returns(redacted); } public void Dispose() => _scope.Dispose(); diff --git a/Stoolball.Data.SqlServer.UnitTests/SqlServerPlayerRepositoryUnitTests.cs b/Stoolball.Data.SqlServer.UnitTests/SqlServerPlayerRepositoryUnitTests.cs index 0032c555..491732bd 100644 --- a/Stoolball.Data.SqlServer.UnitTests/SqlServerPlayerRepositoryUnitTests.cs +++ b/Stoolball.Data.SqlServer.UnitTests/SqlServerPlayerRepositoryUnitTests.cs @@ -47,13 +47,13 @@ private SqlServerPlayerRepository CreateRepository() [Fact] public async Task ProcessAsyncUpdates_allows_3_retries_for_SQL_timeouts_and_logs_warnings() { - _dapperWrapper.Setup(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure)).Throws(SqlExceptionFactory.Create(SqlExceptionType.Timeout)); + _dapperWrapper.Setup(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object)).Throws(SqlExceptionFactory.Create(SqlExceptionType.Timeout)); var repo = CreateRepository(); await repo.ProcessAsyncUpdatesForLinkingAndUnlinkingPlayersToMemberAccounts(); - _dapperWrapper.Verify(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure), Times.Exactly(4)); + _dapperWrapper.Verify(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object), Times.Exactly(4)); _logger.Verify(x => x.Warn(SqlServerPlayerRepository.LOG_TEMPLATE_WARN_SQL_TIMEOUT, It.IsAny()), Times.Exactly(4)); } @@ -63,7 +63,7 @@ public async Task ProcessAsyncUpdates_clears_cache_for_affected_routes() var affectedRoutesFirstIteration = new[] { "/players/one", "/players/two" }; var affectedRoutesSecondIteration = new[] { "/players/three", "/players/four" }; - _dapperWrapper.SetupSequence(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure)) + _dapperWrapper.SetupSequence(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object)) .Returns(Task.FromResult(affectedRoutesFirstIteration as IEnumerable)) .Returns(Task.FromResult(affectedRoutesSecondIteration as IEnumerable)) .Returns(Task.FromResult(Array.Empty() as IEnumerable)); @@ -88,7 +88,7 @@ public async Task ProcessAsyncUpdates_logs_affected_routes() { var affectedRoutes = new[] { "/players/one", "/players/two" }; - _dapperWrapper.SetupSequence(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure)) + _dapperWrapper.SetupSequence(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object)) .Returns(Task.FromResult(affectedRoutes as IEnumerable)) .Returns(Task.FromResult(Array.Empty() as IEnumerable)); @@ -103,13 +103,13 @@ public async Task ProcessAsyncUpdates_logs_affected_routes() [Fact] public async Task ProcessAsyncUpdates_logs_SQL_connection_error_and_exits() { - _dapperWrapper.Setup(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure)).Throws(SqlExceptionFactory.Create(SqlExceptionType.Connection)); + _dapperWrapper.Setup(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object)).Throws(SqlExceptionFactory.Create(SqlExceptionType.Connection)); var repo = CreateRepository(); await repo.ProcessAsyncUpdatesForLinkingAndUnlinkingPlayersToMemberAccounts(); - _dapperWrapper.Verify(x => x.QueryAsync(_databaseConnection.Object, SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure), Times.Exactly(1)); + _dapperWrapper.Verify(x => x.QueryAsync(SqlServerPlayerRepository.PROCESS_ASYNC_STORED_PROCEDURE, CommandType.StoredProcedure, _databaseConnection.Object), Times.Exactly(1)); _logger.Verify(x => x.Error( SqlServerPlayerRepository.LOG_TEMPLATE_ERROR_SQL_EXCEPTION, SqlExceptionFactory.ERROR_ESTABLISHING_CONNECTION diff --git a/Stoolball.Data.SqlServer.UnitTests/SqlServerTournamentRepositoryUnitTests.cs b/Stoolball.Data.SqlServer.UnitTests/SqlServerTournamentRepositoryUnitTests.cs new file mode 100644 index 00000000..68ee8be0 --- /dev/null +++ b/Stoolball.Data.SqlServer.UnitTests/SqlServerTournamentRepositoryUnitTests.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using AngleSharp.Css.Dom; +using Ganss.XSS; +using Moq; +using Stoolball.Logging; +using Stoolball.Matches; +using Stoolball.Routing; +using Stoolball.Teams; +using Xunit; +using static Stoolball.Constants; + +namespace Stoolball.Data.SqlServer.UnitTests +{ + public class SqlServerTournamentRepositoryUnitTests + { + private readonly Mock _connectionFactory = new(); + private readonly Mock _databaseConnection = new(); + private readonly Mock _databaseTransaction = new(); + private readonly Mock _dapperWrapper = new(); + private readonly Mock _auditRepository = new(); + private readonly Mock> _logger = new(); + private readonly Mock _routeGenerator = new(); + private readonly Mock _redirectsRepository = new(); + private readonly Mock _teamRepository = new(); + private readonly Mock _matchRepository = new(); + private readonly Mock _htmlSanitizer = new(); + private readonly Mock _copier = new(); + + public SqlServerTournamentRepositoryUnitTests() + { + _connectionFactory.Setup(x => x.CreateDatabaseConnection()).Returns(_databaseConnection.Object); + _databaseConnection.Setup(x => x.BeginTransaction()).Returns(_databaseTransaction.Object); + + _htmlSanitizer.Setup(x => x.AllowedTags).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedAttributes).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedCssProperties).Returns(new HashSet()); + _htmlSanitizer.Setup(x => x.AllowedAtRules).Returns(new HashSet()); + } + + private SqlServerTournamentRepository CreateRepository() + { + return new SqlServerTournamentRepository( + _connectionFactory.Object, + _dapperWrapper.Object, + _auditRepository.Object, + _logger.Object, + _routeGenerator.Object, + _redirectsRepository.Object, + _teamRepository.Object, + _matchRepository.Object, + _htmlSanitizer.Object, + _copier.Object); + } + + [Fact] + public async Task Create_tournament_audits_and_logs() + { + var memberKey = Guid.NewGuid(); + var memberName = "Member name"; + + var original = new Tournament + { + TournamentId = Guid.NewGuid(), + TournamentName = "Example tournament", + StartTime = DateTime.Now.AddDays(1), + TournamentRoute = "/tournaments/example-tournament", + MemberKey = memberKey + }; + + var auditable = new Tournament + { + TournamentId = Guid.NewGuid(), + TournamentName = original.TournamentName, + StartTime = original.StartTime, + TournamentRoute = original.TournamentRoute, + MemberKey = memberKey + }; + + var redacted = new Tournament + { + TournamentId = Guid.NewGuid(), + TournamentName = original.TournamentName, + StartTime = original.StartTime, + TournamentRoute = original.TournamentRoute, + MemberKey = memberKey + }; + + _copier.Setup(x => x.CreateAuditableCopy(original)).Returns(auditable); + _copier.Setup(x => x.CreateRedactedCopy(auditable)).Returns(redacted); + + var repo = CreateRepository(); + + var createdTournament = await repo.CreateTournament(original, memberKey, memberName); + + _auditRepository.Verify(x => x.CreateAudit(It.Is(audit => + audit.Action == AuditAction.Create && + audit.MemberKey == memberKey && + audit.ActorName == memberName && + audit.EntityUri == auditable.EntityUri && + audit.State.Contains(auditable.TournamentId.Value.ToString()) && + audit.RedactedState.Contains(redacted.TournamentId.Value.ToString()) && + audit.AuditDate.Date == DateTime.UtcNow.Date + ), _databaseTransaction.Object), Times.Once); + + _logger.Verify(x => x.Info(LoggingTemplates.Created, redacted, memberName, memberKey, typeof(SqlServerTournamentRepository), nameof(SqlServerTournamentRepository.CreateTournament)), Times.Once); + + } + } +} diff --git a/Stoolball.Data.SqlServer/DapperWrapper.cs b/Stoolball.Data.SqlServer/DapperWrapper.cs index 55b56b5a..ae20ce31 100644 --- a/Stoolball.Data.SqlServer/DapperWrapper.cs +++ b/Stoolball.Data.SqlServer/DapperWrapper.cs @@ -9,9 +9,22 @@ namespace Stoolball.Data.SqlServer public class DapperWrapper : IDapperWrapper { /// - public async Task> QueryAsync(IDbConnection connection, string sql, CommandType commandType) + public async Task> QueryAsync(string sql, CommandType commandType, IDbConnection connection) { return await connection.QueryAsync(sql, commandType); } + + /// + public async Task> QueryAsync(string sql, object param, IDbTransaction transaction) + { + return await transaction.Connection.QueryAsync(sql, param, transaction); + } + + /// + public async Task ExecuteAsync(string sql, object param, IDbTransaction transaction) + { + return await transaction.Connection.ExecuteAsync(sql, param, transaction); + } + } } diff --git a/Stoolball.Data.SqlServer/IDapperWrapper.cs b/Stoolball.Data.SqlServer/IDapperWrapper.cs index 4f908bdd..fc60e47a 100644 --- a/Stoolball.Data.SqlServer/IDapperWrapper.cs +++ b/Stoolball.Data.SqlServer/IDapperWrapper.cs @@ -13,10 +13,30 @@ public interface IDapperWrapper /// Execute a query asynchronously using Task /// /// - /// /// /// + /// + /// + Task> QueryAsync(string sql, CommandType commandType, IDbConnection connection); + + /// + /// Execute a query asynchronously using Task + /// + /// + /// + /// + /// /// - Task> QueryAsync(IDbConnection connection, string sql, CommandType commandType); + Task> QueryAsync(string sql, object param, IDbTransaction transaction); + + /// + /// Execute a command asynchronously using Task + /// + /// + /// + /// + /// + /// The number of rows affected + Task ExecuteAsync(string sql, object param, IDbTransaction transaction); } } \ No newline at end of file diff --git a/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs b/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs index 8d2a76b8..2eb87b7d 100644 --- a/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs +++ b/Stoolball.Data.SqlServer/SqlServerPlayerRepository.cs @@ -387,7 +387,7 @@ public async Task ProcessAsyncUpdatesForLinkingAndUnlinkingPlayersToMemberAccoun try { retry = false; - affectedRoutes = await _dapperWrapper.QueryAsync(connection, "usp_Link_Player_To_Member_Async_Update", commandType: CommandType.StoredProcedure).ConfigureAwait(false); + affectedRoutes = await _dapperWrapper.QueryAsync("usp_Link_Player_To_Member_Async_Update", commandType: CommandType.StoredProcedure, connection: connection).ConfigureAwait(false); foreach (var route in affectedRoutes) { await _playerCacheClearer.ClearCacheFor(new Player { PlayerRoute = route }); diff --git a/Stoolball.Data.SqlServer/SqlServerTournamentRepository.cs b/Stoolball.Data.SqlServer/SqlServerTournamentRepository.cs index 15cede4a..f3cd01c9 100644 --- a/Stoolball.Data.SqlServer/SqlServerTournamentRepository.cs +++ b/Stoolball.Data.SqlServer/SqlServerTournamentRepository.cs @@ -20,6 +20,7 @@ namespace Stoolball.Data.SqlServer public class SqlServerTournamentRepository : ITournamentRepository { private readonly IDatabaseConnectionFactory _databaseConnectionFactory; + private readonly IDapperWrapper _dapperWrapper; private readonly IAuditRepository _auditRepository; private readonly ILogger _logger; private readonly IRouteGenerator _routeGenerator; @@ -29,10 +30,11 @@ public class SqlServerTournamentRepository : ITournamentRepository private readonly IHtmlSanitizer _htmlSanitiser; private readonly IStoolballEntityCopier _stoolballEntityCopier; - public SqlServerTournamentRepository(IDatabaseConnectionFactory databaseConnectionFactory, IAuditRepository auditRepository, ILogger logger, IRouteGenerator routeGenerator, - IRedirectsRepository redirectsRepository, ITeamRepository teamRepository, IMatchRepository matchRepository, IHtmlSanitizer htmlSanitiser, IStoolballEntityCopier stoolballEntityCopier) + public SqlServerTournamentRepository(IDatabaseConnectionFactory databaseConnectionFactory, IDapperWrapper dapperWrapper, IAuditRepository auditRepository, ILogger logger, + IRouteGenerator routeGenerator, IRedirectsRepository redirectsRepository, ITeamRepository teamRepository, IMatchRepository matchRepository, IHtmlSanitizer htmlSanitiser, IStoolballEntityCopier stoolballEntityCopier) { _databaseConnectionFactory = databaseConnectionFactory ?? throw new ArgumentNullException(nameof(databaseConnectionFactory)); + _dapperWrapper = dapperWrapper ?? throw new ArgumentNullException(nameof(dapperWrapper)); _auditRepository = auditRepository ?? throw new ArgumentNullException(nameof(auditRepository)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _routeGenerator = routeGenerator ?? throw new ArgumentNullException(nameof(routeGenerator)); @@ -89,7 +91,7 @@ public async Task CreateTournament(Tournament tournament, Guid membe async route => await transaction.Connection.ExecuteScalarAsync($"SELECT COUNT(*) FROM {Tables.Tournament} WHERE TournamentRoute = @TournamentRoute", new { TournamentRoute = route }, transaction).ConfigureAwait(false) ).ConfigureAwait(false); - await connection.ExecuteAsync($@"INSERT INTO {Tables.Tournament} + await _dapperWrapper.ExecuteAsync($@"INSERT INTO {Tables.Tournament} (TournamentId, TournamentName, MatchLocationId, PlayerType, PlayersPerTeam, QualificationType, StartTime, StartTimeIsKnown, TournamentNotes, TournamentRoute, MemberKey) VALUES (@TournamentId, @TournamentName, @MatchLocationId, @PlayerType, @PlayersPerTeam, @@ -113,22 +115,23 @@ await connection.ExecuteAsync($@"INSERT INTO {Tables.Tournament} foreach (var team in auditableTournament.Teams) { - await connection.ExecuteAsync($@"INSERT INTO {Tables.TournamentTeam} - (TournamentTeamId, TournamentId, TeamId, TeamRole) - VALUES (@TournamentTeamId, @TournamentId, @TeamId, @TeamRole)", + await _dapperWrapper.ExecuteAsync($@"INSERT INTO {Tables.TournamentTeam} + (TournamentTeamId, TournamentId, TeamId, TeamRole, PlayingAsTeamName) + VALUES (@TournamentTeamId, @TournamentId, @TeamId, @TeamRole, @PlayingAsTeamName)", new { TournamentTeamId = Guid.NewGuid(), auditableTournament.TournamentId, team.Team.TeamId, - TeamRole = team.TeamRole.ToString() + TeamRole = team.TeamRole.ToString(), + team.PlayingAsTeamName }, transaction).ConfigureAwait(false); } foreach (var season in auditableTournament.Seasons) { - await connection.ExecuteAsync($@"INSERT INTO {Tables.TournamentSeason} + await _dapperWrapper.ExecuteAsync($@"INSERT INTO {Tables.TournamentSeason} (TournamentSeasonId, TournamentId, SeasonId) VALUES (@TournamentSeasonId, @TournamentId, @SeasonId)", new @@ -154,21 +157,21 @@ await _auditRepository.CreateAudit(new AuditRecord transaction.Commit(); - _logger.Info(LoggingTemplates.Created, redacted, memberName, memberKey, GetType(), nameof(SqlServerTournamentRepository.CreateTournament)); + _logger.Info(LoggingTemplates.Created, redacted, memberName, memberKey, GetType(), nameof(CreateTournament)); } } return auditableTournament; } - private static async Task InsertOverSets(Tournament auditableTournament, System.Data.IDbTransaction transaction) + private async Task InsertOverSets(Tournament auditableTournament, IDbTransaction transaction) { - var matchInningsIds = await transaction.Connection.QueryAsync($"SELECT MatchInningsId FROM {Tables.MatchInnings} mi INNER JOIN {Tables.Match} m ON mi.MatchId = m.MatchId WHERE m.TournamentId = @TournamentId", new { auditableTournament.TournamentId }, transaction).ConfigureAwait(false); + var matchInningsIds = await _dapperWrapper.QueryAsync($"SELECT MatchInningsId FROM {Tables.MatchInnings} mi INNER JOIN {Tables.Match} m ON mi.MatchId = m.MatchId WHERE m.TournamentId = @TournamentId", new { auditableTournament.TournamentId }, transaction).ConfigureAwait(false); for (var i = 0; i < auditableTournament.DefaultOverSets.Count; i++) { auditableTournament.DefaultOverSets[i].OverSetId = Guid.NewGuid(); - await transaction.Connection.ExecuteAsync($"INSERT INTO {Tables.OverSet} (OverSetId, TournamentId, OverSetNumber, Overs, BallsPerOver) VALUES (@OverSetId, @TournamentId, @OverSetNumber, @Overs, @BallsPerOver)", + await _dapperWrapper.ExecuteAsync($"INSERT INTO {Tables.OverSet} (OverSetId, TournamentId, OverSetNumber, Overs, BallsPerOver) VALUES (@OverSetId, @TournamentId, @OverSetNumber, @Overs, @BallsPerOver)", new { auditableTournament.DefaultOverSets[i].OverSetId, @@ -179,20 +182,22 @@ await transaction.Connection.ExecuteAsync($"INSERT INTO {Tables.OverSet} (OverSe }, transaction).ConfigureAwait(false); - foreach (var matchInningsId in matchInningsIds) + if (matchInningsIds != null) { - await transaction.Connection.ExecuteAsync($"INSERT INTO {Tables.OverSet} (OverSetId, MatchInningsId, OverSetNumber, Overs, BallsPerOver) VALUES (@OverSetId, @MatchInningsId, @OverSetNumber, @Overs, @BallsPerOver)", - new - { - OverSetId = Guid.NewGuid(), - MatchInningsId = matchInningsId, - OverSetNumber = i + 1, - auditableTournament.DefaultOverSets[i].Overs, - auditableTournament.DefaultOverSets[i].BallsPerOver - }, - transaction).ConfigureAwait(false); + foreach (var matchInningsId in matchInningsIds) + { + await _dapperWrapper.ExecuteAsync($"INSERT INTO {Tables.OverSet} (OverSetId, MatchInningsId, OverSetNumber, Overs, BallsPerOver) VALUES (@OverSetId, @MatchInningsId, @OverSetNumber, @Overs, @BallsPerOver)", + new + { + OverSetId = Guid.NewGuid(), + MatchInningsId = matchInningsId, + OverSetNumber = i + 1, + auditableTournament.DefaultOverSets[i].Overs, + auditableTournament.DefaultOverSets[i].BallsPerOver + }, + transaction).ConfigureAwait(false); + } } - } } @@ -350,7 +355,7 @@ await _auditRepository.CreateAudit(new AuditRecord transaction.Commit(); - _logger.Info(LoggingTemplates.Updated, redacted, memberName, memberKey, GetType(), nameof(SqlServerTournamentRepository.UpdateTournament)); + _logger.Info(LoggingTemplates.Updated, redacted, memberName, memberKey, GetType(), nameof(UpdateTournament)); } } @@ -562,7 +567,7 @@ await _auditRepository.CreateAudit(new AuditRecord AuditDate = DateTime.UtcNow }, transaction).ConfigureAwait(false); - _logger.Info(LoggingTemplates.Created, team, memberName, memberKey, GetType(), nameof(SqlServerTournamentRepository.UpdateTeams)); + _logger.Info(LoggingTemplates.Created, team, memberName, memberKey, GetType(), nameof(UpdateTeams)); } await connection.ExecuteAsync($@"INSERT INTO {Tables.TournamentTeam} @@ -876,7 +881,7 @@ await _auditRepository.CreateAudit(new AuditRecord transaction.Commit(); - _logger.Info(LoggingTemplates.Deleted, redacted, memberName, memberKey, GetType(), nameof(SqlServerTournamentRepository.DeleteTournament)); + _logger.Info(LoggingTemplates.Deleted, redacted, memberName, memberKey, GetType(), nameof(DeleteTournament)); } } }