Skip to content

Commit

Permalink
Add unit tests for AvailableMatchDates
Browse files Browse the repository at this point in the history
* Change MatchRepository.GetMatches(...) to async
  • Loading branch information
axunonb committed Jan 23, 2024
1 parent d7580a1 commit 64d1ed7
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ namespace TournamentManager.Tests.ModelValidators;
public class FixtureValidatorTests
{
private (ITenantContext TenantConext, Axuno.Tools.DateAndTime.TimeZoneConverter TimeZoneConverter, PlannedMatchRow PlannedMatch) _data;
#pragma warning disable IDE0052 // Remove unread private members
private readonly AppDb _appDb; // mocked in CTOR
#pragma warning restore IDE0052 // Remove unread private members
private readonly CultureInfo _culture = CultureInfo.GetCultureInfo("en-US");
private const string ExcludedDateReason = "Unit-Test";

Expand Down Expand Up @@ -122,14 +119,6 @@ public FixtureValidatorTests()
tenantContextMock.SetupDbContext(dbContextMock);
_data.TenantConext = tenantContextMock.Object;

_appDb = appDbMock.Object;

//var teamIds = orgCtxMock.Object.AppDb.MatchRepository.AreTeamsBusyAsync(new MatchEntity {Id = 1, HomeTeamId = 11, GuestTeamId = 22}, false, CancellationToken.None).Result;
//var matchrow = venueRepoMock.Object.GetOccupyingMatchesAsync(1, new DateTimePeriod(null, default(DateTime?)), 2, CancellationToken.None).Result;
//var venue = appDbMock.Object.VenueRepository.GetVenueById(2);
//var isValid = orgCtxMock.Object.AppDb.VenueRepository.IsValidVenueId(22).Result;
//isValid = orgCtxMock.Object.AppDb.VenueRepository.IsValidVenueId(21).Result;

#endregion
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Globalization;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using SD.LLBLGen.Pro.ORMSupportClasses;
using TournamentManager.DAL.EntityClasses;
using TournamentManager.DAL.HelperClasses;
using TournamentManager.Data;
using TournamentManager.Plan;
using TournamentManager.Tests.TestComponents;
using Moq;

namespace TournamentManager.Tests.Plan;

[TestFixture]
internal class AvailableMatchDatesTests
{
[Test]
public void Generate_Available_Dates_Should_Succeed()
{
var availableDates = GetAvailableMatchDatesInstance();
var tournament = GetTournament();
var tournamentLeg = tournament.Rounds.First().RoundLegs.First();
var matches = new EntityCollection<MatchEntity>();

Assert.Multiple(() =>
{
Assert.That(del: async () => await availableDates.GenerateNewAsync(tournament.Rounds[0], matches, CancellationToken.None), Throws.Nothing);
Assert.That(availableDates.GetGeneratedAndManualAvailableMatchDateDays(tournamentLeg).Count, Is.EqualTo(87));
Assert.That(availableDates.GetGeneratedAndManualAvailableMatchDates(1, new DateTimePeriod(tournamentLeg.StartDateTime, tournamentLeg.EndDateTime), null).Count, Is.EqualTo(17));
});
}

[Test]
public async Task Clearing_Available_Dates_Should_Succeed()
{
var availableDates = GetAvailableMatchDatesInstance();
Assert.That(await availableDates.ClearAsync(MatchDateClearOption.All, CancellationToken.None), Is.EqualTo(0));
}

private AvailableMatchDates GetAvailableMatchDatesInstance()
{
var tenantContextMock = TestMocks.GetTenantContextMock();
var appDbMock = TestMocks.GetAppDbMock();

#region ** AvailableMatchDateRepository mocks setup **

var availableMatchDatesRepoMock = TestMocks.GetRepo<AvailableMatchDateRepository>();
availableMatchDatesRepoMock.Setup(rep =>
rep.GetAvailableMatchDatesAsync(It.IsAny<long>(), It.IsAny<CancellationToken>()))
.Returns((long tournamentId, CancellationToken cancellationToken) =>
{
var availableMatchDates = new EntityCollection<AvailableMatchDateEntity>();
return Task.FromResult(availableMatchDates);
});
availableMatchDatesRepoMock.Setup(rep =>
rep.ClearAsync(It.IsAny<long>(), It.IsAny<MatchDateClearOption>(), It.IsAny<CancellationToken>()))
.Returns((long tournamentId, MatchDateClearOption clear, CancellationToken cancellationToken) => Task.FromResult(0));
appDbMock.Setup(a => a.AvailableMatchDateRepository).Returns(availableMatchDatesRepoMock.Object);

#endregion

#region ** ExcludedMatchDateRepository mocks setup **

var excludedMatchDatesMock = TestMocks.GetRepo<ExcludedMatchDateRepository>();
excludedMatchDatesMock.Setup(rep =>
rep.GetExcludedMatchDatesAsync(It.IsAny<long>(), It.IsAny<CancellationToken>()))
.Returns((long tournamentId, CancellationToken cancellationToken) =>
{
var excludedMatchDates = new EntityCollection<ExcludeMatchDateEntity>();
return Task.FromResult(excludedMatchDates);
});
appDbMock.Setup(a => a.ExcludedMatchDateRepository).Returns(excludedMatchDatesMock.Object);

#endregion

// Build complete TenantContext mock
var dbContextMock = TestMocks.GetDbContextMock();
dbContextMock.SetupAppDb(appDbMock);
tenantContextMock.SetupDbContext(dbContextMock);

// Create AvailableMatchDates instance
var logger = NullLogger<AvailableMatchDates>.Instance;
var tzConverter = new Axuno.Tools.DateAndTime.TimeZoneConverter(
new NodaTime.TimeZones.DateTimeZoneCache(NodaTime.TimeZones.TzdbDateTimeZoneSource.Default), "Europe/Berlin",
CultureInfo.CurrentCulture,
NodaTime.TimeZones.Resolvers.LenientResolver);
var availableMatchDates = new AvailableMatchDates(tenantContextMock.Object, tzConverter, logger);
return availableMatchDates;
}

private TournamentEntity GetTournament()
{
var teams = new EntityCollection<TeamEntity>() {
{ new (1) { Venue = new VenueEntity(1){ IsDirty = false, IsNew = false }, MatchDayOfWeek = 5, MatchTime = new TimeSpan(18, 0, 0) } },
{ new (2) { Venue = new VenueEntity(2){ IsDirty = false, IsNew = false }, MatchDayOfWeek = 4, MatchTime = new TimeSpan(18, 30, 0) } },
{ new (3) { Venue = new VenueEntity(3){ IsDirty = false, IsNew = false }, MatchDayOfWeek = 3, MatchTime = new TimeSpan(19, 0, 0) } },
{ new (4) { Venue = new VenueEntity(4){ IsDirty = false, IsNew = false }, MatchDayOfWeek = 2, MatchTime = new TimeSpan(19, 30, 0) } },
{ new (5) { Venue = new VenueEntity(5){ IsDirty = false, IsNew = false }, MatchDayOfWeek = 1, MatchTime = new TimeSpan(20, 0, 0) } }
};
foreach (var teamEntity in teams)
{
teamEntity.Fields.State = EntityState.Fetched;
teamEntity.IsNew = teamEntity.IsDirty = false;
}

var round = new RoundEntity(1) {
RoundLegs = { new RoundLegEntity {Id = 1, RoundId = 1, StartDateTime = new DateTime(2024, 1, 1), EndDateTime = new DateTime(2024, 4, 30) } }, IsNew = false,IsDirty = false
};

var teamInRounds = new EntityCollection<TeamInRoundEntity> {
new() { Round = round, Team = teams[0], IsNew = false, IsDirty = false },
new() { Round = round, Team = teams[1], IsNew = false, IsDirty = false },
new() { Round = round, Team = teams[2], IsNew = false, IsDirty = false },
new() { Round = round, Team = teams[3], IsNew = false, IsDirty = false },
new() { Round = round, Team = teams[4], IsNew = false, IsDirty = false }
};

round.TeamInRounds.AddRange(teamInRounds);

foreach (var teamInRound in teamInRounds)
{
teamInRound.Fields.State = EntityState.Fetched;
teamInRound.IsNew = teamInRound.IsDirty = false;
}

var tournament = new TournamentEntity(1) {IsNew = false, IsDirty = false };
round.Tournament = tournament;
// Must be set to false, otherwise teams cannot be added to the collection
round.TeamCollectionViaTeamInRound.IsReadOnly = false;
round.TeamCollectionViaTeamInRound.AddRange(teams);
round.Fields.State = EntityState.Fetched;

tournament.Rounds.Add(round);

return tournament;
}
}

32 changes: 21 additions & 11 deletions TournamentManager/TournamentManager/Data/MatchRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public virtual async Task<List<PlannedMatchRow>> GetPlannedMatchesAsync(IPredica
{
using var da = _dbContext.GetNewAdapter();

if (!(await GetPlannedMatchesAsync(new PredicateExpression(PlannedMatchFields.TournamentId == tournamentId & PlannedMatchFields.Id == id), cancellationToken)).Any())
if ((await GetPlannedMatchesAsync(new PredicateExpression(PlannedMatchFields.TournamentId == tournamentId & PlannedMatchFields.Id == id), cancellationToken)).Count == 0)
return null;

return (await da.FetchQueryAsync(
Expand Down Expand Up @@ -105,38 +105,48 @@ public virtual async Task<List<CalendarRow>> GetMatchCalendarAsync(long tourname
new QueryFactory().Calendar.Where(filter), cancellationToken));
}

public virtual EntityCollection<MatchEntity> GetMatches(long tournamentId)
public virtual async Task<EntityCollection<MatchEntity>> GetMatches(long tournamentId, CancellationToken cancellationToken)
{
var rounds = new TournamentRepository(_dbContext).GetTournamentRounds(tournamentId);

var roundId = new List<long>(rounds.Count);
roundId.AddRange(rounds.Select(round => round.Id));

IRelationPredicateBucket bucket = new RelationPredicateBucket();
IPredicateExpression roundFilter =
new PredicateExpression(new FieldCompareRangePredicate(MatchFields.RoundId, null, false,
roundId.ToArray()));
bucket.PredicateExpression.AddWithAnd(roundFilter);


var matches = new EntityCollection<MatchEntity>();
using var da = _dbContext.GetNewAdapter();
da.FetchEntityCollection(matches, bucket);

var qp = new QueryParameters
{
CollectionToFetch = matches,
FilterToUseAsPredicateExpression = { roundFilter }
};

await da.FetchEntityCollectionAsync(qp, cancellationToken);
da.CloseConnection();

return matches;
}

public virtual EntityCollection<MatchEntity> GetMatches(RoundEntity round)
public virtual async Task<EntityCollection<MatchEntity>> GetMatches(RoundEntity round, CancellationToken cancellationToken)
{
IRelationPredicateBucket bucket = new RelationPredicateBucket();
IPredicateExpression roundFilter =
new PredicateExpression(new FieldCompareRangePredicate(MatchFields.RoundId, null, false,
new[] {round.Id}));
bucket.PredicateExpression.AddWithAnd(roundFilter);

var matches = new EntityCollection<MatchEntity>();
using var da = _dbContext.GetNewAdapter();
da.FetchEntityCollection(matches, bucket);

var qp = new QueryParameters
{
CollectionToFetch = matches,
FilterToUseAsPredicateExpression = { roundFilter }
};

await da.FetchEntityCollectionAsync(qp, cancellationToken);
da.CloseConnection();

return matches;
Expand All @@ -149,7 +159,7 @@ public virtual EntityCollection<MatchEntity> GetMatches(RoundEntity round)
if (!match.LegSequenceNo.HasValue)
return null;

if (match.Round != null && match.Round.RoundLegs != null)
if (match.Round is { RoundLegs: not null })
{
return match.Round.RoundLegs.First(l => l.SequenceNo == match.LegSequenceNo);
}
Expand Down
40 changes: 20 additions & 20 deletions TournamentManager/TournamentManager/Plan/AvailableMatchDates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ internal class AvailableMatchDates
private readonly ILogger<AvailableMatchDates> _logger;

// available match dates from database
private readonly EntityCollection<AvailableMatchDateEntity> _availableMatchDateEntities = new();
private readonly EntityCollection<AvailableMatchDateEntity> _availableDatesFromDb = new();

// programmatically generated available match dates
private readonly EntityCollection<AvailableMatchDateEntity> _generatedAvailableMatchDateEntities = new();
private readonly EntityCollection<AvailableMatchDateEntity> _generatedAvailableDates = new();

// excluded dates
private readonly EntityCollection<ExcludeMatchDateEntity> _excludedMatchDateEntities = new();
private readonly EntityCollection<ExcludeMatchDateEntity> _excludedMatchDates = new();

internal AvailableMatchDates(ITenantContext tenantContext,
Axuno.Tools.DateAndTime.TimeZoneConverter timeZoneConverter, ILogger<AvailableMatchDates> logger)
Expand All @@ -42,21 +42,21 @@ internal AvailableMatchDates(ITenantContext tenantContext,
private async Task Initialize(CancellationToken cancellationToken)
{
_logger.LogDebug($"Initializing {nameof(AvailableMatchDates)}");
_excludedMatchDateEntities.Clear();
_excludedMatchDateEntities.AddRange(
_excludedMatchDates.Clear();
_excludedMatchDates.AddRange(
await _appDb.ExcludedMatchDateRepository.GetExcludedMatchDatesAsync(
_tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken));

_logger.LogDebug("{count} excluded match dates loaded from storage", _excludedMatchDateEntities.Count);
_logger.LogDebug("{count} excluded match dates loaded from storage", _excludedMatchDates.Count);

_availableMatchDateEntities.Clear();
_availableMatchDateEntities.AddRange(
_availableDatesFromDb.Clear();
_availableDatesFromDb.AddRange(
await _appDb.AvailableMatchDateRepository.GetAvailableMatchDatesAsync(
_tenantContext.TournamentContext.MatchPlanTournamentId, cancellationToken));

_logger.LogDebug("{count} available match dates loaded from storage", _availableMatchDateEntities.Count);
_logger.LogDebug("{count} available match dates loaded from storage", _availableDatesFromDb.Count);

_generatedAvailableMatchDateEntities.Clear();
_generatedAvailableDates.Clear();
}


Expand All @@ -73,10 +73,10 @@ await _appDb.AvailableMatchDateRepository.ClearAsync(_tenantContext.TournamentCo
clear, cancellationToken);

if((clear & MatchDateClearOption.OnlyAutoGenerated) == MatchDateClearOption.OnlyAutoGenerated)
_generatedAvailableMatchDateEntities.Clear();
_generatedAvailableDates.Clear();

if ((clear & MatchDateClearOption.OnlyManual) == MatchDateClearOption.OnlyManual)
_availableMatchDateEntities.Clear();
_availableDatesFromDb.Clear();

return deleted;
}
Expand Down Expand Up @@ -164,7 +164,7 @@ public async Task GenerateNewAsync(RoundEntity round, EntityCollection<MatchEnti
IsGenerated = true
};

_generatedAvailableMatchDateEntities.Add(av);
_generatedAvailableDates.Add(av);
}

if (teamsWithSameVenue.Count > 1)
Expand All @@ -178,8 +178,8 @@ public async Task GenerateNewAsync(RoundEntity round, EntityCollection<MatchEnti
}
}

_logger.LogDebug("Generated {Count} UTC dates for HomeTeams:", _generatedAvailableMatchDateEntities.Count);
_logger.LogDebug("{Generated}\n", _generatedAvailableMatchDateEntities.Select(gen => (gen.HomeTeamId, gen.MatchStartTime)));
_logger.LogDebug("Generated {Count} UTC dates for HomeTeams:", _generatedAvailableDates.Count);
_logger.LogDebug("{Generated}\n", _generatedAvailableDates.Select(gen => (gen.HomeTeamId, gen.MatchStartTime)).ToList());

// Note: Generated dates are not saved to the database, but only used for the current run.
}
Expand Down Expand Up @@ -247,19 +247,19 @@ private bool IsExcludedDate(DateTime queryDate, long? roundId, long? teamId)
{
return
// Excluded for the whole tournament...
_excludedMatchDateEntities.Any(
_excludedMatchDates.Any(
excl => queryDate >= excl.DateFrom && queryDate <= excl.DateTo &&
excl.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId && !excl.RoundId.HasValue &&
!excl.TeamId.HasValue)
||
// OR excluded for a round...
_excludedMatchDateEntities.Any(
_excludedMatchDates.Any(
excl => queryDate >= excl.DateFrom && queryDate <= excl.DateTo &&
excl.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId && excl.RoundId.HasValue &&
roundId.HasValue && excl.RoundId == roundId)
||
// OR excluded for a team
_excludedMatchDateEntities.Any(
_excludedMatchDates.Any(
excl => queryDate >= excl.DateFrom && queryDate <= excl.DateTo &&
excl.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId && excl.TeamId.HasValue &&
teamId.HasValue && excl.TeamId == teamId)
Expand All @@ -268,7 +268,7 @@ private bool IsExcludedDate(DateTime queryDate, long? roundId, long? teamId)

internal List<DateTime> GetGeneratedAndManualAvailableMatchDateDays(RoundLegEntity leg)
{
var result = _generatedAvailableMatchDateEntities.Union(_availableMatchDateEntities)
var result = _generatedAvailableDates.Union(_availableDatesFromDb)
.Where(gen => gen.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId
&& gen.MatchStartTime >= leg.StartDateTime.Date &&
gen.MatchStartTime <= leg.EndDateTime.AddDays(1).AddSeconds(-1))
Expand All @@ -284,7 +284,7 @@ internal List<AvailableMatchDateEntity> GetGeneratedAndManualAvailableMatchDates
{
if (datePeriod is not { Start: not null, End: not null }) throw new ArgumentNullException(nameof(datePeriod));

var result = _generatedAvailableMatchDateEntities.Union(_availableMatchDateEntities)
var result = _generatedAvailableDates.Union(_availableDatesFromDb)
.Where(gen => gen.TournamentId == _tenantContext.TournamentContext.MatchPlanTournamentId
&& gen.HomeTeamId == homeTeamId && gen.MatchStartTime >= datePeriod.Start.Value.Date &&
gen.MatchStartTime <=
Expand Down
4 changes: 2 additions & 2 deletions TournamentManager/TournamentManager/Plan/MatchScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ private async Task LoadEntitiesAsync(CancellationToken cancellationToken)
_areEntitiesLoaded = true;
}

internal async Task GenerateAvailableMatchDatesAsync(MatchDateClearOption clearMatchDates, RoundEntity round,
private async Task GenerateAvailableMatchDatesAsync(MatchDateClearOption clearMatchDates, RoundEntity round,
EntityCollection<MatchEntity> tournamentMatches, CancellationToken cancellationToken)
{
await LoadEntitiesAsync(cancellationToken);
Expand Down Expand Up @@ -220,7 +220,7 @@ await _appDb.GenericRepository.DeleteEntitiesDirectlyAsync(typeof(MatchEntity),
cancellationToken);
}

var tournamentMatches = _appDb.MatchRepository.GetMatches(round.TournamentId!.Value);
var tournamentMatches = await _appDb.MatchRepository.GetMatches(round.TournamentId!.Value, cancellationToken);
return tournamentMatches;
}

Expand Down

0 comments on commit 64d1ed7

Please sign in to comment.