From 512da985faca663aef6fefaa05530449a89b5e23 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Wed, 29 Oct 2025 00:49:53 -0400 Subject: [PATCH] Fixed date time assignments when testing ingestions. Added more ingestion tests. Fixed voice work updater bugs. --- .../Ingestion/VoiceWorkUpdater.cs | 62 +++++++++++++++++-- .../Ingestion/Japanese/IngestionTestsBase.cs | 16 ++++- ..._New_Release_And_Scan_Again_Later_Tests.cs | 54 ++++++++++++++++ ...elease_With_New_Tags_And_Creators_Tests.cs | 2 +- ...New_Upcoming_And_Scan_Again_Later_Tests.cs | 53 ++++++++++++++++ ...ert_New_Upcoming_Release_Same_Day_Tests.cs | 62 +++++++++++++++++++ ...g_With_Existing_Tags_And_Creators_Tests.cs | 2 +- ...ert_Upcoming_And_Scan_Again_Later_Tests.cs | 49 --------------- 8 files changed, 242 insertions(+), 58 deletions(-) create mode 100644 JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs create mode 100644 JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs create mode 100644 JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs delete mode 100644 JSMR.Tests/Ingestion/Japanese/Insert_Upcoming_And_Scan_Again_Later_Tests.cs diff --git a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs index 6b95aab..5e5511a 100644 --- a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs +++ b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs @@ -161,12 +161,15 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider private void UpsertVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) { VoiceWork voiceWork = GetOrAddVoiceWork(ingest, upsertContext); + VoiceWorkUpsertState state = ComputeVoiceWorkUpsertState(voiceWork, ingest, upsertContext); - bool isAdded = dbContext.Entry(voiceWork).State == EntityState.Added; - bool isWithinCurrentScanAnchor = voiceWork.LastScannedDate == upsertContext.CurrentScanAnchor; - bool isNewOnSale = voiceWork.SalesDate is null && ingest.SalesDate is not null; + //bool isAdded = dbContext.Entry(voiceWork).State == EntityState.Added; + //bool isWithinCurrentScanAnchor = voiceWork.LastScannedDate == upsertContext.CurrentScanAnchor.DateTime; + //bool isWithinPreviousScanAnchor = voiceWork.LastScannedDate == upsertContext.PreviousScanAnchor.DateTime; + //bool hasGoneOnSale = voiceWork.SalesDate is null && ingest.SalesDate is not null; - bool isNew = isAdded || isWithinCurrentScanAnchor || isNewOnSale; + //bool isNewUpcoming = ingest.SalesDate is null && (isAdded || isWithinCurrentScanAnchor || (isWithinPreviousScanAnchor && upsertContext.PreviousScanAnchor.DateTime.Hour == 16)); + //bool isNewOnSale = ingest.SalesDate is not null && (isAdded || hasGoneOnSale || isWithinCurrentScanAnchor); voiceWork.Circle = upsertContext.Circles[ingest.MakerId]; voiceWork.ProductName = ingest.Title; @@ -180,20 +183,21 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider voiceWork.StarRating = ingest.StarRating; voiceWork.Votes = ingest.Votes; voiceWork.IsValid = true; + voiceWork.LastScannedDate = ComputeLastScannedDate(voiceWork.LastScannedDate, state, upsertContext); if (ingest.SalesDate.HasValue) { voiceWork.SalesDate = ingest.SalesDate.Value.ToDateTime(new TimeOnly(0, 0)); voiceWork.ExpectedDate = null; voiceWork.PlannedReleaseDate = null; - voiceWork.Status = isNew ? (byte)VoiceWorkStatus.NewRelease : (byte)VoiceWorkStatus.Available; + voiceWork.Status = state.IsNewOnSale ? (byte)VoiceWorkStatus.NewRelease : (byte)VoiceWorkStatus.Available; } else { voiceWork.SalesDate = null; voiceWork.ExpectedDate = ingest.ExpectedDate?.ToDateTime(new TimeOnly(0, 0)); voiceWork.PlannedReleaseDate = ingest.RegistrationDate > upsertContext.CurrentScanAnchor ? ingest.RegistrationDate : null; - voiceWork.Status = isNew ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming; + voiceWork.Status = state.IsNewUpcoming ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming; } } @@ -213,6 +217,52 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider return voiceWork; } + private sealed record VoiceWorkUpsertState( + bool IsAdded, + bool ScannedThisAnchor, + bool ScannedPrevAt4pm, + bool WentOnSale, + bool HasSalesDate, + bool IsNewUpcoming, + bool IsNewOnSale + ); + + private VoiceWorkUpsertState ComputeVoiceWorkUpsertState(VoiceWork voiceWork, VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) + { + bool isAdded = dbContext.Entry(voiceWork).State == EntityState.Added; + + DateTime currentScanAnchor = upsertContext.CurrentScanAnchor.DateTime; + DateTime previousScanAnchor = upsertContext.PreviousScanAnchor.DateTime; + + bool scannedThis = voiceWork.LastScannedDate == currentScanAnchor; + bool scannedPrevAt4pm = voiceWork.LastScannedDate == previousScanAnchor && previousScanAnchor.Hour == 16; + bool hasSales = ingest.SalesDate is not null; + bool wentOnSale = voiceWork.SalesDate is null && hasSales; + + bool isNewUpcoming = !hasSales && (isAdded || scannedThis || scannedPrevAt4pm); + bool isNewOnSale = hasSales && (isAdded || wentOnSale || scannedThis); + + return new VoiceWorkUpsertState( + IsAdded: isAdded, + ScannedThisAnchor: scannedThis, + ScannedPrevAt4pm: scannedPrevAt4pm, + WentOnSale: wentOnSale, + HasSalesDate: hasSales, + IsNewUpcoming: isNewUpcoming, + IsNewOnSale: isNewOnSale + ); + } + + private static DateTime? ComputeLastScannedDate(DateTime? existing, VoiceWorkUpsertState state, VoiceWorkUpsertContext upsertContext) + { + if ((state.IsNewUpcoming || state.IsNewOnSale) == false) + return null; + + var current = upsertContext.CurrentScanAnchor.DateTime; + + return state.WentOnSale ? current : existing ?? current; + } + private void UpsertTags(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) { foreach (string tagName in ingest.Tags) diff --git a/JSMR.Tests/Ingestion/Japanese/IngestionTestsBase.cs b/JSMR.Tests/Ingestion/Japanese/IngestionTestsBase.cs index 6fd8321..bb412fa 100644 --- a/JSMR.Tests/Ingestion/Japanese/IngestionTestsBase.cs +++ b/JSMR.Tests/Ingestion/Japanese/IngestionTestsBase.cs @@ -5,6 +5,7 @@ using JSMR.Infrastructure.Data; using JSMR.Infrastructure.Ingestion; using JSMR.Tests.Fixtures; using NSubstitute; +using System.Runtime.InteropServices; namespace JSMR.Tests.Ingestion.Japanese; @@ -19,8 +20,10 @@ public abstract class IngestionTestsBase(MariaDbContainerFixture container) protected static async Task UpsertAsync(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest[] ingests) { + DateTime utcDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + IClock clock = Substitute.For(); - clock.UtcNow.Returns(new DateTimeOffset(dateTime)); + clock.UtcNow.Returns(new DateTimeOffset(utcDateTime)); TokyoTimeProvider timeProvider = new(clock); @@ -28,4 +31,15 @@ public abstract class IngestionTestsBase(MariaDbContainerFixture container) return await updater.UpsertAsync(ingests, TestContext.Current.CancellationToken); } + + protected static DateTime TokyoLocalToUtc(int year, int month, int day, int hour, int minute, int second) + { + var tokyo = TimeZoneInfo.FindSystemTimeZoneById( + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Tokyo Standard Time" : "Asia/Tokyo"); + + var localUnspec = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Unspecified); + var utc = TimeZoneInfo.ConvertTimeToUtc(localUnspec, tokyo); + + return utc; + } } \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs new file mode 100644 index 0000000..b6ed659 --- /dev/null +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_And_Scan_Again_Later_Tests.cs @@ -0,0 +1,54 @@ +using JSMR.Application.Common; +using JSMR.Application.Scanning.Contracts; +using JSMR.Domain.Entities; +using JSMR.Infrastructure.Common.SupportedLanguages; +using JSMR.Infrastructure.Data; +using JSMR.Tests.Fixtures; +using Microsoft.EntityFrameworkCore; +using Shouldly; + +namespace JSMR.Tests.Ingestion.Japanese; + +public class Insert_New_Release_And_Scan_Again_Later_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container) +{ + [Fact] + public async Task Insert_New_Release_And_Scan_Again_Later() + { + await using AppDbContext dbContext = await GetAppDbContextAsync(); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG10001", + MakerName = "New Dreams", + ProductId = "RJ2000001", + Title = "Day One Release", + Description = "Releasing now.", + Tags = ["アイドル", "メガネ"], + Creators = ["かの仔"], + WishlistCount = 50, + Downloads = 10, + HasTrial = false, + HasDLPlay = false, + StarRating = null, + Votes = null, + AgeRating = AgeRating.AllAges, + HasImage = true, + SupportedLanguages = [new JapaneseLanguage()], + SalesDate = new DateOnly(2025, 1, 15), + ExpectedDate = null + }; + + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 15, 00, 00, 00), ingest, VoiceWorkStatus.NewRelease); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 15, 15, 59, 59), ingest, VoiceWorkStatus.NewRelease); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 15, 16, 00, 00), ingest, VoiceWorkStatus.Available); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 16, 00, 00, 00), ingest, VoiceWorkStatus.Available); + } + + private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, VoiceWorkStatus status) + { + await UpsertAsync(dbContext, dateTime, [ingest]); + + VoiceWork? voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken); + voiceWork.Status.ShouldBe((byte)status); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs index 76c9e9c..a3a868a 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Release_With_New_Tags_And_Creators_Tests.cs @@ -39,7 +39,7 @@ public class Insert_New_Release_With_New_Tags_And_Creators_Tests(MariaDbContaine ExpectedDate = null }; - VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, new DateTime(2025, 01, 15, 9, 0, 0), [ingest]); + VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 15, 9, 0, 0), [ingest]); VoiceWork? voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == "RJ2000001", TestContext.Current.CancellationToken); voiceWork.ShouldNotBeNull(); diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs new file mode 100644 index 0000000..cc54d7f --- /dev/null +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_And_Scan_Again_Later_Tests.cs @@ -0,0 +1,53 @@ +using JSMR.Application.Common; +using JSMR.Application.Scanning.Contracts; +using JSMR.Domain.Entities; +using JSMR.Infrastructure.Common.SupportedLanguages; +using JSMR.Infrastructure.Data; +using JSMR.Tests.Fixtures; +using Microsoft.EntityFrameworkCore; +using Shouldly; + +namespace JSMR.Tests.Ingestion.Japanese; + +public class Insert_New_Upcoming_And_Scan_Again_Later_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container) +{ + [Fact] + public async Task Insert_New_Upcoming_And_Scan_Again_Later() + { + await using AppDbContext dbContext = await GetAppDbContextAsync(); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG00001", + MakerName = "Good Dreams", + ProductId = "RJ1000002", + Title = "Preview Only", + Description = "Still upcoming.", + Tags = [], + Creators = [], + WishlistCount = 100, + Downloads = 0, + HasTrial = false, + HasDLPlay = false, + AgeRating = AgeRating.AllAges, + HasImage = false, + SupportedLanguages = [new JapaneseLanguage()], + SalesDate = null, + ExpectedDate = new DateOnly(2025, 2, 1) + }; + + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 09, 16, 00, 00), ingest, VoiceWorkStatus.NewAndUpcoming); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 00, 00, 00), ingest, VoiceWorkStatus.NewAndUpcoming); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 15, 59, 59), ingest, VoiceWorkStatus.NewAndUpcoming); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 16, 00, 00), ingest, VoiceWorkStatus.Upcoming); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 11, 00, 00, 00), ingest, VoiceWorkStatus.Upcoming); + } + + private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, VoiceWorkStatus status) + { + await UpsertAsync(dbContext, dateTime, [ingest]); + + VoiceWork? voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken); + voiceWork.Status.ShouldBe((byte)status); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs new file mode 100644 index 0000000..cbbfcaa --- /dev/null +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_Release_Same_Day_Tests.cs @@ -0,0 +1,62 @@ +using JSMR.Application.Common; +using JSMR.Application.Scanning.Contracts; +using JSMR.Domain.Entities; +using JSMR.Infrastructure.Common.SupportedLanguages; +using JSMR.Infrastructure.Data; +using JSMR.Tests.Fixtures; +using Microsoft.EntityFrameworkCore; +using Shouldly; + +namespace JSMR.Tests.Ingestion.Japanese; + +public class Insert_New_Upcoming_Release_Same_Day_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container) +{ + [Fact] + public async Task Insert_New_Upcoming_Release_Same_Day() + { + await using AppDbContext dbContext = await GetAppDbContextAsync(); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG00001", + MakerName = "Good Dreams", + ProductId = "RJ1000002", + Title = "Preview Only", + Description = "Still upcoming.", + Tags = [], + Creators = [], + WishlistCount = 100, + Downloads = 0, + HasTrial = false, + HasDLPlay = false, + AgeRating = AgeRating.AllAges, + HasImage = false, + SupportedLanguages = [new JapaneseLanguage()], + SalesDate = null, + ExpectedDate = new DateOnly(2025, 2, 1) + }; + + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 09, 16, 00, 00), ingest, VoiceWorkStatus.NewAndUpcoming); + + VoiceWorkIngest updatedIngest = ingest with + { + Title = "Released on the Same Day", + Description = "Should be indicated as a new release", + SalesDate = new DateOnly(2025, 1, 10), + ExpectedDate = null + }; + + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 00, 00, 00), updatedIngest, VoiceWorkStatus.NewRelease); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 15, 59, 59), updatedIngest, VoiceWorkStatus.NewRelease); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 10, 16, 00, 00), updatedIngest, VoiceWorkStatus.Available); + await UpsertAndVerify(dbContext, TokyoLocalToUtc(2025, 01, 11, 00, 00, 00), updatedIngest, VoiceWorkStatus.Available); + } + + private static async Task UpsertAndVerify(AppDbContext dbContext, DateTime dateTime, VoiceWorkIngest ingest, VoiceWorkStatus status) + { + await UpsertAsync(dbContext, dateTime, [ingest]); + + VoiceWork? voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken); + voiceWork.Status.ShouldBe((byte)status); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs index 7f8b206..9966a2c 100644 --- a/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs +++ b/JSMR.Tests/Ingestion/Japanese/Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests.cs @@ -42,7 +42,7 @@ public class Insert_New_Upcoming_With_Existing_Tags_And_Creators_Tests(MariaDbCo ]; await using AppDbContext dbContext = await GetAppDbContextAsync(); - DateTime currentDateTime = new(2025, 01, 05, 10, 0, 0); + DateTime currentDateTime = TokyoLocalToUtc(2025, 01, 05, 10, 0, 0); VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, currentDateTime, insertNewUpcomingIngests); diff --git a/JSMR.Tests/Ingestion/Japanese/Insert_Upcoming_And_Scan_Again_Later_Tests.cs b/JSMR.Tests/Ingestion/Japanese/Insert_Upcoming_And_Scan_Again_Later_Tests.cs deleted file mode 100644 index 6e074be..0000000 --- a/JSMR.Tests/Ingestion/Japanese/Insert_Upcoming_And_Scan_Again_Later_Tests.cs +++ /dev/null @@ -1,49 +0,0 @@ -using JSMR.Application.Common; -using JSMR.Application.Scanning.Contracts; -using JSMR.Domain.Entities; -using JSMR.Infrastructure.Common.SupportedLanguages; -using JSMR.Infrastructure.Data; -using JSMR.Tests.Fixtures; -using Microsoft.EntityFrameworkCore; -using Shouldly; - -namespace JSMR.Tests.Ingestion.Japanese; - -public class Insert_Upcoming_And_Scan_Again_Later_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container) -{ - [Fact] - public async Task Insert_Upcoming_And_Scan_Again_Later() - { - await using AppDbContext dbContext = await GetAppDbContextAsync(); - - VoiceWorkIngest ingest = new() - { - MakerId = "RG00001", - MakerName = "Good Dreams", - ProductId = "RJ1000002", - Title = "Preview Only", - Description = "Still upcoming.", - Tags = Array.Empty(), - Creators = Array.Empty(), - WishlistCount = 100, - Downloads = 0, - HasTrial = false, - HasDLPlay = false, - AgeRating = AgeRating.AllAges, - HasImage = false, - SupportedLanguages = [new JapaneseLanguage()], - SalesDate = null, - ExpectedDate = new DateOnly(2025, 2, 1) - }; - - await UpsertAsync(dbContext, new DateTime(2025, 01, 10, 1, 0, 0), [ingest]); - - VoiceWork? voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == "RJ1000002", TestContext.Current.CancellationToken); - voiceWork.Status.ShouldBe((byte)VoiceWorkStatus.NewAndUpcoming); - - await UpsertAsync(dbContext, new DateTime(2025, 01, 12, 10, 0, 0), [ingest]); - - VoiceWork? updatedVoiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == "RJ1000002", TestContext.Current.CancellationToken); - updatedVoiceWork.Status.ShouldBe((byte)VoiceWorkStatus.Upcoming); - } -} \ No newline at end of file