Added fail ingestion tests.

This commit is contained in:
2025-10-29 02:01:35 -04:00
parent 512da985fa
commit 4121bd94d9
5 changed files with 223 additions and 43 deletions

View File

@@ -26,7 +26,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
continue; continue;
} }
Upsert(ingest, upsertContext); result.Status = Upsert(ingest, upsertContext);
} }
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
@@ -119,20 +119,29 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
if (voiceWork.SalesDate is not null && ingest.SalesDate is null) if (voiceWork.SalesDate is not null && ingest.SalesDate is null)
{ {
string message = $"Voice work has a sales date of {voiceWork.SalesDate.Value.ToShortDateString()}, but parsed ingest does not"; string message = $"Voice work has a sales date of {voiceWork.SalesDate.Value.ToShortDateString()}, but ingest does not";
result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error)); result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error));
} }
} }
private void Upsert(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) private VoiceWorkUpsertStatus Upsert(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{ {
UpsertCircle(ingest, upsertContext); UpsertCircle(ingest, upsertContext);
UpsertVoiceWork(ingest, upsertContext);
VoiceWork voiceWork = UpsertVoiceWork(ingest, upsertContext);
UpsertTags(ingest, upsertContext); UpsertTags(ingest, upsertContext);
UpsertVoiceWorkTags(ingest, upsertContext); UpsertVoiceWorkTags(ingest, upsertContext);
UpsertCreators(ingest, upsertContext); UpsertCreators(ingest, upsertContext);
UpsertVoiceWorkCreators(ingest, upsertContext); UpsertVoiceWorkCreators(ingest, upsertContext);
UpsertVoiceWorkSupportedLanguages(ingest, upsertContext); UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
return dbContext.Entry(voiceWork).State switch
{
EntityState.Added => VoiceWorkUpsertStatus.Inserted,
EntityState.Modified => VoiceWorkUpsertStatus.Updated,
_ => VoiceWorkUpsertStatus.Unchanged,
};
} }
private void UpsertCircle(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) private void UpsertCircle(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
@@ -158,19 +167,11 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
return circle; return circle;
} }
private void UpsertVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) private VoiceWork UpsertVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
{ {
VoiceWork voiceWork = GetOrAddVoiceWork(ingest, upsertContext); VoiceWork voiceWork = GetOrAddVoiceWork(ingest, upsertContext);
VoiceWorkUpsertState state = ComputeVoiceWorkUpsertState(voiceWork, ingest, upsertContext); VoiceWorkUpsertState state = ComputeVoiceWorkUpsertState(voiceWork, ingest, upsertContext);
//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 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.Circle = upsertContext.Circles[ingest.MakerId];
voiceWork.ProductName = ingest.Title; voiceWork.ProductName = ingest.Title;
voiceWork.Description = ingest.Description; voiceWork.Description = ingest.Description;
@@ -199,6 +200,8 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
voiceWork.PlannedReleaseDate = ingest.RegistrationDate > upsertContext.CurrentScanAnchor ? ingest.RegistrationDate : null; voiceWork.PlannedReleaseDate = ingest.RegistrationDate > upsertContext.CurrentScanAnchor ? ingest.RegistrationDate : null;
voiceWork.Status = state.IsNewUpcoming ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming; voiceWork.Status = state.IsNewUpcoming ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming;
} }
return voiceWork;
} }
private VoiceWork GetOrAddVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext) private VoiceWork GetOrAddVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
@@ -218,11 +221,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
} }
private sealed record VoiceWorkUpsertState( private sealed record VoiceWorkUpsertState(
bool IsAdded,
bool ScannedThisAnchor,
bool ScannedPrevAt4pm,
bool WentOnSale, bool WentOnSale,
bool HasSalesDate,
bool IsNewUpcoming, bool IsNewUpcoming,
bool IsNewOnSale bool IsNewOnSale
); );
@@ -243,11 +242,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider
bool isNewOnSale = hasSales && (isAdded || wentOnSale || scannedThis); bool isNewOnSale = hasSales && (isAdded || wentOnSale || scannedThis);
return new VoiceWorkUpsertState( return new VoiceWorkUpsertState(
IsAdded: isAdded,
ScannedThisAnchor: scannedThis,
ScannedPrevAt4pm: scannedPrevAt4pm,
WentOnSale: wentOnSale, WentOnSale: wentOnSale,
HasSalesDate: hasSales,
IsNewUpcoming: isNewUpcoming, IsNewUpcoming: isNewUpcoming,
IsNewOnSale: isNewOnSale IsNewOnSale: isNewOnSale
); );

View File

@@ -0,0 +1,55 @@
using JSMR.Application.Common;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
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 Fail_Attempted_Insert_With_Spam_Circle_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container)
{
[Fact]
public async Task Fail_Attempted_Insert_With_Spam_Circle()
{
await using AppDbContext dbContext = await GetAppDbContextAsync();
VoiceWorkIngest ingest = new()
{
MakerId = "RG00004",
MakerName = "Never Again",
ProductId = "RJ3000002",
Title = "Probably Something with AI",
Description = "Some description.",
Tags = [],
Creators = [],
WishlistCount = 100,
Downloads = 0,
HasTrial = false,
HasDLPlay = false,
AgeRating = AgeRating.R18,
HasImage = false,
SupportedLanguages = [new JapaneseLanguage()],
SalesDate = null,
ExpectedDate = new DateOnly(2025, 2, 1)
};
VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 09, 16, 00, 00), [ingest]);
VoiceWork? voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
voiceWork.ShouldBeNull();
results.Length.ShouldBe(1);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(0);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(1);
results.Sum(r => r.Issues.Count).ShouldBe(1);
VoiceWorkUpsertIssue issue = results[0].Issues.ElementAt(0);
issue.Severity.ShouldBe(VoiceWorkUpsertIssueSeverity.Error);
issue.Message.ShouldBe($"Circle {ingest.MakerName} ({ingest.MakerId}) is a spam circle");
}
}

View File

@@ -0,0 +1,75 @@
using JSMR.Application.Common;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
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 Fail_Attempted_Update_With_Decreased_Downloads_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container)
{
[Fact]
public async Task Fail_Attempted_Update_With_Decreased_Downloads()
{
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
};
VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 15, 00, 00, 00), [ingest]);
VoiceWork? voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
voiceWork.ShouldNotBeNull();
voiceWork.Downloads.ShouldBe(10);
results.Length.ShouldBe(1);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(1);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(0);
results.Sum(r => r.Issues.Count).ShouldBe(0);
VoiceWorkIngest updatedIngest = ingest with
{
Downloads = 9
};
VoiceWorkUpsertResult[] updatedResults = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 16, 00, 00, 00), [updatedIngest]);
voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
voiceWork.ShouldNotBeNull();
voiceWork.Downloads.ShouldBe(10);
updatedResults.Length.ShouldBe(1);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(0);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(1);
updatedResults.Sum(r => r.Issues.Count).ShouldBe(1);
VoiceWorkUpsertIssue issue = updatedResults[0].Issues.ElementAt(0);
issue.Severity.ShouldBe(VoiceWorkUpsertIssueSeverity.Error);
issue.Message.ShouldBe($"Downloads have decreased from {voiceWork.Downloads} to {updatedIngest.Downloads}");
}
}

View File

@@ -0,0 +1,75 @@
using JSMR.Application.Common;
using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
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 Fail_Attempted_Update_With_Sales_Date_Reversal_Tests(MariaDbContainerFixture container) : IngestionTestsBase(container)
{
[Fact]
public async Task Fail_Attempted_Update_With_Decreased_Downloads()
{
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
};
VoiceWorkUpsertResult[] results = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 15, 00, 00, 00), [ingest]);
VoiceWork? voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
voiceWork.ShouldNotBeNull();
voiceWork.SalesDate.ShouldBe(new DateTime(2025, 1, 15));
results.Length.ShouldBe(1);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(1);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0);
results.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(0);
results.Sum(r => r.Issues.Count).ShouldBe(0);
VoiceWorkIngest updatedIngest = ingest with
{
SalesDate = null
};
VoiceWorkUpsertResult[] updatedResults = await UpsertAsync(dbContext, TokyoLocalToUtc(2025, 01, 16, 00, 00, 00), [updatedIngest]);
voiceWork = await dbContext.VoiceWorks.FirstOrDefaultAsync(v => v.ProductId == ingest.ProductId, TestContext.Current.CancellationToken);
voiceWork.ShouldNotBeNull();
voiceWork.SalesDate.ShouldBe(new DateTime(2025, 1, 15));
updatedResults.Length.ShouldBe(1);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(0);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0);
updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(1);
updatedResults.Sum(r => r.Issues.Count).ShouldBe(1);
VoiceWorkUpsertIssue issue = updatedResults[0].Issues.ElementAt(0);
issue.Severity.ShouldBe(VoiceWorkUpsertIssueSeverity.Error);
issue.Message.ShouldBe($"Voice work has a sales date of {voiceWork.SalesDate!.Value.ToShortDateString()}, but ingest does not");
}
}

View File

@@ -14,7 +14,8 @@ public static class VoiceWorkIngestionSeedData
context.Circles.AddRange( context.Circles.AddRange(
new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" }, new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" },
new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" }, new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" },
new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" } new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" },
new() { CircleId = 4, Name = "Never Again", Spam = true, MakerId = "RG00004" }
); );
context.VoiceWorks.AddRange( context.VoiceWorks.AddRange(
@@ -63,27 +64,6 @@ public static class VoiceWorkIngestionSeedData
new() { VoiceWorkId = 3, TagId = 5 }, // Tsundere new() { VoiceWorkId = 3, TagId = 5 }, // Tsundere
new() { VoiceWorkId = 3, TagId = 9 } // Non-Fiction / Narrative new() { VoiceWorkId = 3, TagId = 9 } // Non-Fiction / Narrative
//new() { VoiceWorkId = 3, TagId = 1 },
//new() { VoiceWorkId = 3, TagId = 1 },
//new() { VoiceWorkId = 3, TagId = 1 },
//new() { VoiceWorkId = 3, TagId = 1 },
//new() { VoiceWorkId = 3, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 4, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 5 } // Tsundere
//new() { VoiceWorkId = 5, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 1 },
//new() { VoiceWorkId = 5, TagId = 1 }
); );
context.Creators.AddRange( context.Creators.AddRange(