diff --git a/JSMR.Application/Scanning/Ports/IVoiceWorkUpdater.cs b/JSMR.Application/Scanning/Ports/IVoiceWorkUpdater.cs index 7794ae1..349eef8 100644 --- a/JSMR.Application/Scanning/Ports/IVoiceWorkUpdater.cs +++ b/JSMR.Application/Scanning/Ports/IVoiceWorkUpdater.cs @@ -4,5 +4,32 @@ namespace JSMR.Application.Scanning.Ports; public interface IVoiceWorkUpdater { - Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken); + Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken); +} + +public class VoiceWorkUpsertResult +{ + public int? VoiceWorkId { get; set; } + public ICollection Issues { get; } = []; + public VoiceWorkUpsertStatus Status { get; set; } = VoiceWorkUpsertStatus.Unchanged; +} + +public record VoiceWorkUpsertIssue( + string Message, + VoiceWorkUpsertIssueSeverity Severity +); + +public enum VoiceWorkUpsertIssueSeverity +{ + Information, + Warning, + Error +} + +public enum VoiceWorkUpsertStatus +{ + Unchanged, + Inserted, + Updated, + Skipped } \ No newline at end of file diff --git a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs index 5c90764..c3bcb85 100644 --- a/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs +++ b/JSMR.Application/Scanning/ScanVoiceWorksHandler.cs @@ -43,7 +43,9 @@ public sealed class ScanVoiceWorksHandler( return VoiceWorkIngest.From(work, value); })]; - int[] voiceWorkIds = await updater.UpsertAsync(ingests, cancellationToken); + VoiceWorkUpsertResult[] upsertResults = await updater.UpsertAsync(ingests, cancellationToken); + int[] voiceWorkIds = [.. upsertResults.Where(x => x.VoiceWorkId.HasValue).Select(x => x.VoiceWorkId!.Value)]; + await searchUpdater.UpdateAsync(voiceWorkIds, cancellationToken); return new(); diff --git a/JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs b/JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs index 5a569d4..c814737 100644 --- a/JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs +++ b/JSMR.Infrastructure/Common/Languages/LanguageIdentifier.cs @@ -20,7 +20,7 @@ public class LanguageIdentifier : ILanguageIdentifier { RankedLanguageIdentifierFactory factory = new(); - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("JSMR.Infrastructure.Languages.Language.xml"); + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("JSMR.Infrastructure.Common.Languages.Language.xml"); _identifier = factory.Load(stream); } diff --git a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs index d0c227a..9011622 100644 --- a/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs +++ b/JSMR.Infrastructure/DI/InfrastructureServiceCollectionExtensions.cs @@ -20,8 +20,7 @@ using JSMR.Infrastructure.Data.Repositories.Creators; using JSMR.Infrastructure.Data.Repositories.Tags; using JSMR.Infrastructure.Data.Repositories.VoiceWorks; using JSMR.Infrastructure.Http; -using JSMR.Infrastructure.Ingest; -using JSMR.Infrastructure.Ingestions; +using JSMR.Infrastructure.Ingestion; using JSMR.Infrastructure.Scanning; using Microsoft.Extensions.DependencyInjection; diff --git a/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpdater.cs b/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpdater.cs index 217b424..a917d6c 100644 --- a/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpdater.cs +++ b/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpdater.cs @@ -10,7 +10,7 @@ namespace JSMR.Infrastructure.Ingestion; public class EnglishVoiceWorkUpdater(AppDbContext dbContext, ILanguageIdentifier languageIdentifier) : IVoiceWorkUpdater { - public async Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) + public async Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) { EnglishVoiceWorkUpsertContext upsertContext = await CreateUpsertContextAsync(ingests, cancellationToken); @@ -20,20 +20,23 @@ public class EnglishVoiceWorkUpdater(AppDbContext dbContext, ILanguageIdentifier VoiceWorkUpsertResult result = upsertContext.Results[ingest.ProductId]; + if (upsertContext.VoiceWorks.TryGetValue(ingest.ProductId, out VoiceWork? voiceWork)) + { + result.VoiceWorkId = upsertContext.VoiceWorks[ingest.ProductId].VoiceWorkId; + } + if (result.Issues.Count > 0) { result.Status = VoiceWorkUpsertStatus.Skipped; continue; } - - UpsertEnglishVoiceWork(ingest, upsertContext); - - result.Status = VoiceWorkUpsertStatus.Updated; + + result.Status = UpsertEnglishVoiceWork(ingest, upsertContext); } await dbContext.SaveChangesAsync(cancellationToken); - return [.. upsertContext.VoiceWorks.Select(x => x.Value.VoiceWorkId)]; + return [.. upsertContext.Results.Select(x => x.Value)]; } private async Task CreateUpsertContextAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) @@ -68,31 +71,41 @@ public class EnglishVoiceWorkUpdater(AppDbContext dbContext, ILanguageIdentifier if (!isTitleEnglish && !isDescriptionEnglish) { - string message = $"Prouct title and/or description is not in English"; + string message = $"Product title and/or description is not in English"; result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Information)); return; } - if (upsertContext.Circles.TryGetValue(ingest.MakerId, out Circle? circle) == false) + if (upsertContext.Circles.ContainsKey(ingest.MakerId) == false) { string message = $"Unable to find circle for maker id: {ingest.MakerId}"; result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error)); return; } - if (upsertContext.VoiceWorks.TryGetValue(ingest.ProductId, out VoiceWork? voiceWork) == false) + if (upsertContext.VoiceWorks.ContainsKey(ingest.ProductId) == false) { string message = $"Unable to find voice work for product id: {ingest.ProductId}"; result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error)); } } - private void UpsertEnglishVoiceWork(VoiceWorkIngest ingest, EnglishVoiceWorkUpsertContext upsertContext) + private VoiceWorkUpsertStatus UpsertEnglishVoiceWork(VoiceWorkIngest ingest, EnglishVoiceWorkUpsertContext upsertContext) { EnglishVoiceWork englishVoiceWork = GetOrAddEnglishVoiceWork(ingest, upsertContext); englishVoiceWork.ProductName = ingest.Title; englishVoiceWork.Description = ingest.Description; englishVoiceWork.IsValid = true; + + switch (dbContext.Entry(englishVoiceWork).State) + { + case EntityState.Added: + return VoiceWorkUpsertStatus.Inserted; + case EntityState.Modified: + return VoiceWorkUpsertStatus.Updated; + default: + return VoiceWorkUpsertStatus.Unchanged; + } } private EnglishVoiceWork GetOrAddEnglishVoiceWork(VoiceWorkIngest ingest, EnglishVoiceWorkUpsertContext upsertContext) diff --git a/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpsertContext.cs b/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpsertContext.cs index 4002c96..be7fccf 100644 --- a/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpsertContext.cs +++ b/JSMR.Infrastructure/Ingestion/EnglishVoiceWorkUpsertContext.cs @@ -1,4 +1,5 @@ -using JSMR.Domain.Entities; +using JSMR.Application.Scanning.Ports; +using JSMR.Domain.Entities; namespace JSMR.Infrastructure.Ingestion; diff --git a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs index 20779ed..800c0db 100644 --- a/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs +++ b/JSMR.Infrastructure/Ingestion/VoiceWorkUpdater.cs @@ -10,7 +10,7 @@ namespace JSMR.Infrastructure.Ingestion; public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider) : IVoiceWorkUpdater { - public async Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) + public async Task UpsertAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) { VoiceWorkUpsertContext upsertContext = await CreateUpsertContextAsync(ingests, cancellationToken); @@ -31,7 +31,7 @@ public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider await dbContext.SaveChangesAsync(cancellationToken); - return [.. upsertContext.VoiceWorks.Select(x => x.Value.VoiceWorkId)]; + return [.. upsertContext.Results.Select(x => x.Value)]; } private async Task CreateUpsertContextAsync(IReadOnlyCollection ingests, CancellationToken cancellationToken) diff --git a/JSMR.Infrastructure/Ingestion/VoiceWorkUpsertContext.cs b/JSMR.Infrastructure/Ingestion/VoiceWorkUpsertContext.cs index 0e774c1..28bb975 100644 --- a/JSMR.Infrastructure/Ingestion/VoiceWorkUpsertContext.cs +++ b/JSMR.Infrastructure/Ingestion/VoiceWorkUpsertContext.cs @@ -1,4 +1,5 @@ -using JSMR.Domain.Entities; +using JSMR.Application.Scanning.Ports; +using JSMR.Domain.Entities; namespace JSMR.Infrastructure.Ingestion; @@ -12,29 +13,29 @@ public record VoiceWorkUpsertContext( Dictionary Results ); -public class VoiceWorkUpsertResult -{ - public int? VoiceWorkId { get; set; } - public ICollection Issues { get; } = []; - public VoiceWorkUpsertStatus Status { get; set; } = VoiceWorkUpsertStatus.Unchanged; -} +//public class VoiceWorkUpsertResult +//{ +// public int? VoiceWorkId { get; set; } +// public ICollection Issues { get; } = []; +// public VoiceWorkUpsertStatus Status { get; set; } = VoiceWorkUpsertStatus.Unchanged; +//} -public record VoiceWorkUpsertIssue( - string Message, - VoiceWorkUpsertIssueSeverity Severity -); +//public record VoiceWorkUpsertIssue( +// string Message, +// VoiceWorkUpsertIssueSeverity Severity +//); -public enum VoiceWorkUpsertIssueSeverity -{ - Information, - Warning, - Error -} +//public enum VoiceWorkUpsertIssueSeverity +//{ +// Information, +// Warning, +// Error +//} -public enum VoiceWorkUpsertStatus -{ - Unchanged, - Inserted, - Updated, - Skipped -} \ No newline at end of file +//public enum VoiceWorkUpsertStatus +//{ +// Unchanged, +// Inserted, +// Updated, +// Skipped +//} \ No newline at end of file diff --git a/JSMR.Tests/Fixtures/MariaDbFixture.cs b/JSMR.Tests/Fixtures/MariaDbFixture.cs index 194231e..5c15807 100644 --- a/JSMR.Tests/Fixtures/MariaDbFixture.cs +++ b/JSMR.Tests/Fixtures/MariaDbFixture.cs @@ -1,6 +1,11 @@ -using JSMR.Infrastructure.Data; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using JSMR.Infrastructure.Data; using Microsoft.EntityFrameworkCore; +using MySqlConnector; using Testcontainers.MariaDb; +using Testcontainers.Xunit; +using Xunit.Abstractions; namespace JSMR.Tests.Fixtures; @@ -64,4 +69,113 @@ public class MariaDbFixture : IAsyncLifetime await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); } +} + +[CollectionDefinition("db")] +public sealed class MariaDbCollection : ICollectionFixture { } + +//[UsedImplicitly] +public sealed class MariaDbContainerFixture2(IMessageSink messageSink) + : ContainerFixture(messageSink) +{ + const int MajorVersion = 10; + const int MinorVersion = 11; + const int Build = 6; + + public string RootConnectionString => $"Server={Container.IpAddress};Port=3306;User=root;Password=rootpw;SslMode=none;"; + + protected override MariaDbBuilder Configure(MariaDbBuilder builder) + { + return builder.WithImage($"mariadb:{MajorVersion}.{MinorVersion}.{Build}") + .WithEnvironment("MARIADB_ROOT_PASSWORD", "rootpw") + .WithPortBinding(3307, 3306) + //.WithPortBinding(3306, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(3306)); + } +} + + +public sealed class MariaDbContainerFixture : IAsyncLifetime +{ + const int MajorVersion = 10; + const int MinorVersion = 11; + const int Build = 6; + + private IContainer _container = default!; + public string RootConnectionString { get; private set; } = default!; + + public async Task InitializeAsync() + { + //_container = new ContainerBuilder() + // .WithImage("mariadb:11") + // .WithEnvironment("MARIADB_ROOT_PASSWORD", "rootpw") + // .WithPortBinding(3307, 3306) + // .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(3306)) + // .Build(); + + _container = new ContainerBuilder() + .WithImage($"mariadb:{MajorVersion}.{MinorVersion}.{Build}") + .WithEnvironment("MARIADB_ROOT_PASSWORD", "rootpw") + .WithPortBinding(3307, 3306) + //.WithPortBinding(3306, assignRandomHostPort: true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(3306)) + .Build(); + + await _container.StartAsync(); + + // No database specified: we’ll create per-test DBs + //RootConnectionString = "Server=127.0.0.1;Port=3307;User=root;Password=rootpw;SslMode=none;"; + + //RootConnectionString = _container.GetConnectionString(); + var port = _container.GetMappedPublicPort(3306); + RootConnectionString = $"Server=127.0.0.1;Port={port};User=root;Password=rootpw;SslMode=none;"; + } + + public async Task DisposeAsync() => await _container.DisposeAsync(); +} + +public static class MariaTestDb +{ + public static async Task CreateIsolatedAsync(string rootConnectionString, Func? seed = null) + { + string databaseName = $"t_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}"; + + await CreateDatabaseAsync(rootConnectionString, databaseName); + + MySqlConnectionStringBuilder connectionStringBuilder = new(rootConnectionString) + { + Database = databaseName + }; + + AppDbContext dbContext = CreateDbContext(connectionStringBuilder.ConnectionString); + await dbContext.Database.EnsureCreatedAsync(); + + if (seed != null) + await seed(dbContext); + + return dbContext; + } + + private static async Task CreateDatabaseAsync(string rootConnectionString, string databaseName) + { + await using MySqlConnection connection = new(rootConnectionString); + + await connection.OpenAsync(); + + await using MySqlCommand command = connection.CreateCommand(); + command.CommandText = $"CREATE DATABASE `{databaseName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"; + + await command.ExecuteNonQueryAsync(); + } + + private static AppDbContext CreateDbContext(string connectionString) + { + DbContextOptions options = new DbContextOptionsBuilder() + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), + o => o.EnableRetryOnFailure()) + .EnableSensitiveDataLogging() + .Options; + + return new AppDbContext(options); + } } \ No newline at end of file diff --git a/JSMR.Tests/Fixtures/TagSearchProviderFixture.cs b/JSMR.Tests/Fixtures/TagSearchProviderFixture.cs index 0f62974..00b2ed2 100644 --- a/JSMR.Tests/Fixtures/TagSearchProviderFixture.cs +++ b/JSMR.Tests/Fixtures/TagSearchProviderFixture.cs @@ -1,5 +1,8 @@ using JSMR.Infrastructure.Data; +using JSMR.Tests.Ingestion; using Microsoft.EntityFrameworkCore; +using MySqlConnector; +using Testcontainers.MariaDb; namespace JSMR.Tests.Fixtures; @@ -29,4 +32,44 @@ public class TagSearchProviderFixture : MariaDbFixture await context.SaveChangesAsync(); } +} + +public sealed class TagSearchProviderFixture2(MariaDbContainerFixture container) : IAsyncLifetime +{ + public AppDbContext? DbContext { get; private set; } + + public async Task InitializeAsync() + { + DbContext = await MariaTestDb.CreateIsolatedAsync( + container.RootConnectionString, + seed: SeedAsync); + } + + private static async Task SeedAsync(AppDbContext context) + { + if (await context.Tags.AnyAsync()) + return; + + context.Tags.AddRange( + new() { TagId = 1, Name = "OL" }, + new() { TagId = 2, Name = "ほのぼの", Favorite = true }, + new() { TagId = 3, Name = "ツンデレ", Blacklisted = true } + ); + + context.EnglishTags.AddRange( + new() { EnglishTagId = 1, TagId = 1, Name = "Office Lady" }, + new() { EnglishTagId = 2, TagId = 2, Name = "Heartwarming" }, + new() { EnglishTagId = 3, TagId = 3, Name = "Tsundere" } + ); + + await context.SaveChangesAsync(); + } + + public async Task DisposeAsync() + { + if (DbContext is not null) + { + await DbContext.DisposeAsync(); + } + } } \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/EnglishLocalizationTests.cs b/JSMR.Tests/Ingestion/EnglishLocalizationTests.cs new file mode 100644 index 0000000..1008cce --- /dev/null +++ b/JSMR.Tests/Ingestion/EnglishLocalizationTests.cs @@ -0,0 +1,189 @@ +using JSMR.Application.Scanning.Contracts; +using JSMR.Application.Scanning.Ports; +using JSMR.Domain.Entities; +using JSMR.Infrastructure.Common.Languages; +using JSMR.Infrastructure.Data; +using JSMR.Infrastructure.Ingestion; +using JSMR.Tests.Fixtures; +using Microsoft.EntityFrameworkCore; +using Shouldly; + +namespace JSMR.Tests.Ingestion; + +public class EnglishLocalizationTests(MariaDbContainerFixture container) : IClassFixture +{ + private readonly LanguageIdentifier languageIdentifier = new(); + + [Fact] + public async Task Insert_Then_Update() + { + await using AppDbContext dbContext = await MariaTestDb.CreateIsolatedAsync( + container.RootConnectionString, + seed: VoiceWorkIngestionSeedData.SeedAsync); + + // Part 1 -- Insert + VoiceWorkIngest ingest = new() + { + MakerId = "RG00001", + MakerName = "Good Dreams", + ProductId = "RJ0000001", + Title = "Today Sounds (EN)", + Description = "An average product. (EN)" + }; + + EnglishVoiceWorkUpdater updater = new(dbContext, languageIdentifier); + VoiceWorkUpsertResult[] results = await updater.UpsertAsync([ingest], CancellationToken.None); + + VoiceWork voiceWork = await dbContext.VoiceWorks.SingleAsync(v => v.ProductId == "RJ0000001"); + EnglishVoiceWork? englishVoiceWork = await dbContext.EnglishVoiceWorks.SingleOrDefaultAsync(e => e.VoiceWorkId == voiceWork.VoiceWorkId); + + englishVoiceWork.ShouldNotBeNull(); + englishVoiceWork.ProductName.ShouldBe("Today Sounds (EN)"); + englishVoiceWork.Description.ShouldBe("An average product. (EN)"); + englishVoiceWork.IsValid?.ShouldBeTrue(); + + 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.Count(r => r.Status == VoiceWorkUpsertStatus.Unchanged).ShouldBe(0); + results.Sum(r => r.Issues.Count).ShouldBe(0); + + // Part 2 -- Update + VoiceWorkIngest ingestUpdate = ingest with + { + Title = "Today Sounds (EN v2)", + Description = "Updated English description." + }; + + VoiceWorkUpsertResult[] updatedResults = await updater.UpsertAsync([ingestUpdate], CancellationToken.None); + + EnglishVoiceWork? updatedEnglishVoiceWork = await dbContext.EnglishVoiceWorks.SingleOrDefaultAsync(e => e.VoiceWorkId == voiceWork.VoiceWorkId); + updatedEnglishVoiceWork.ShouldNotBeNull(); + updatedEnglishVoiceWork.ProductName.ShouldBe("Today Sounds (EN v2)"); + updatedEnglishVoiceWork.Description.ShouldBe("Updated English description."); + updatedEnglishVoiceWork.IsValid?.ShouldBeTrue(); + + updatedResults.Length.ShouldBe(1); + updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(0); + updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(1); + updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(0); + updatedResults.Count(r => r.Status == VoiceWorkUpsertStatus.Unchanged).ShouldBe(0); + updatedResults.Sum(r => r.Issues.Count).ShouldBe(0); + + // Part 3 -- Update Again (No Change) + VoiceWorkUpsertResult[] updatedAgainResults = await updater.UpsertAsync([ingestUpdate], CancellationToken.None); + + EnglishVoiceWork? updatedAgainEnglishVoiceWork = await dbContext.EnglishVoiceWorks.SingleOrDefaultAsync(e => e.VoiceWorkId == voiceWork.VoiceWorkId); + updatedAgainEnglishVoiceWork.ShouldNotBeNull(); + updatedAgainEnglishVoiceWork.ProductName.ShouldBe("Today Sounds (EN v2)"); + updatedAgainEnglishVoiceWork.Description.ShouldBe("Updated English description."); + updatedAgainEnglishVoiceWork.IsValid?.ShouldBeTrue(); + + updatedAgainResults.Length.ShouldBe(1); + updatedAgainResults.Count(r => r.Status == VoiceWorkUpsertStatus.Inserted).ShouldBe(0); + updatedAgainResults.Count(r => r.Status == VoiceWorkUpsertStatus.Updated).ShouldBe(0); + updatedAgainResults.Count(r => r.Status == VoiceWorkUpsertStatus.Skipped).ShouldBe(0); + updatedAgainResults.Count(r => r.Status == VoiceWorkUpsertStatus.Unchanged).ShouldBe(1); + updatedAgainResults.Sum(r => r.Issues.Count).ShouldBe(0); + } + + [Fact] + public async Task Fail_Attempted_Insert_With_Missing_Circle() + { + await using AppDbContext dbContext = await MariaTestDb.CreateIsolatedAsync( + container.RootConnectionString, + seed: VoiceWorkIngestionSeedData.SeedAsync); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG99999", + MakerName = "Missing Maker", + ProductId = "RJ9999999", + Title = "EN Title", + Description = "EN Desc" + }; + + EnglishVoiceWorkUpdater updater = new(dbContext, languageIdentifier); + VoiceWorkUpsertResult[] results = await updater.UpsertAsync([ingest], CancellationToken.None); + + int englishVoiceWorkCount = await dbContext.EnglishVoiceWorks.CountAsync(CancellationToken.None); + englishVoiceWorkCount.ShouldBe(0); + + 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($"Unable to find circle for maker id: {ingest.MakerId}"); + } + + [Fact] + public async Task Fail_Attempted_Insert_With_Missing_Product() + { + await using AppDbContext dbContext = await MariaTestDb.CreateIsolatedAsync( + container.RootConnectionString, + seed: VoiceWorkIngestionSeedData.SeedAsync); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG00001", + MakerName = "Good Dreams", + ProductId = "RJ9999999", + Title = "EN Title", + Description = "EN Desc" + }; + + EnglishVoiceWorkUpdater updater = new(dbContext, languageIdentifier); + VoiceWorkUpsertResult[] results = await updater.UpsertAsync([ingest], CancellationToken.None); + + int englishVoiceWorkCount = await dbContext.EnglishVoiceWorks.CountAsync(CancellationToken.None); + englishVoiceWorkCount.ShouldBe(0); + + 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($"Unable to find voice work for product id: {ingest.ProductId}"); + } + + [Fact] + public async Task Fail_Attempted_Insert_When_Not_English() + { + await using AppDbContext dbContext = await MariaTestDb.CreateIsolatedAsync( + container.RootConnectionString, + seed: VoiceWorkIngestionSeedData.SeedAsync); + + VoiceWorkIngest ingest = new() + { + MakerId = "RG00001", + MakerName = "Good Dreams", + ProductId = "RJ0000001", + Title = "すごく快適なASMR", + Description = "最高の製品です!" + }; + + EnglishVoiceWorkUpdater updater = new(dbContext, languageIdentifier); + VoiceWorkUpsertResult[] results = await updater.UpsertAsync([ingest], CancellationToken.None); + + int englishVoiceWorkCount = await dbContext.EnglishVoiceWorks.CountAsync(CancellationToken.None); + englishVoiceWorkCount.ShouldBe(0); + + 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.Information); + issue.Message.ShouldBe("Product title and/or description is not in English"); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Ingestion/VoiceWorkIngestionTests.cs b/JSMR.Tests/Ingestion/VoiceWorkIngestionTests.cs new file mode 100644 index 0000000..b020d36 --- /dev/null +++ b/JSMR.Tests/Ingestion/VoiceWorkIngestionTests.cs @@ -0,0 +1,49 @@ +using JSMR.Application.Scanning.Contracts; +using JSMR.Infrastructure.Common.Time; +using JSMR.Infrastructure.Data; +using JSMR.Infrastructure.Ingestion; +using NSubstitute; +using Shouldly; + +namespace JSMR.Tests.Ingestion; + +//public class VoiceWorkIngestionTests(VoiceWorkIngestionFixture fixture) +//{ +// [Fact] +// public async Task Simple_Upsert() +// { +// await using AppDbContext context = fixture.CreateDbContext(); + +// VoiceWorkIngest[] ingests = +// [ +// // TODO +// //new() +// //{ +// // MakerId = "RG00001", +// // MakerName = "Good Dreams", +// // ProductId = "A Newly Announced Work", +// // Title = "", +// // Description = "" +// //}, +// //new() +// //{ +// // MakerId = "RG00002", +// // MakerName = "Sweet Dreams", +// // ProductId = "", +// // Title = "", +// // Description = "" +// //} +// ]; + +// IClock clock = Substitute.For(); +// clock.UtcNow.Returns(new DateTimeOffset(2025, 1, 3, 0, 0, 0, 0, TimeSpan.FromSeconds(0))); + +// TokyoTimeProvider timeProvider = new(clock); + +// VoiceWorkUpdater updater = new(context, timeProvider); +// await updater.UpsertAsync(ingests, CancellationToken.None); + +// // TODO +// //context.VoiceWorks.Count().ShouldBe(2); +// } +//} \ No newline at end of file diff --git a/JSMR.Tests/Fixtures/VoiceWorkUpsertFixture.cs b/JSMR.Tests/Ingestion/VoiceWorkUpsertFixture.cs similarity index 74% rename from JSMR.Tests/Fixtures/VoiceWorkUpsertFixture.cs rename to JSMR.Tests/Ingestion/VoiceWorkUpsertFixture.cs index 1eab7e1..9d8ab2c 100644 --- a/JSMR.Tests/Fixtures/VoiceWorkUpsertFixture.cs +++ b/JSMR.Tests/Ingestion/VoiceWorkUpsertFixture.cs @@ -1,19 +1,12 @@ using JSMR.Application.Common; -using JSMR.Application.Integrations.DLSite.Models; -using JSMR.Application.Scanning.Contracts; using JSMR.Infrastructure.Data; using Microsoft.EntityFrameworkCore; -namespace JSMR.Tests.Fixtures; +namespace JSMR.Tests.Ingestion; -public class VoiceWorkUpsertFixture : MariaDbFixture +public static class VoiceWorkIngestionSeedData { - protected override async Task OnInitializedAsync(AppDbContext context) - { - await SeedAsync(context); - } - - private static async Task SeedAsync(AppDbContext context) + public static async Task SeedAsync(AppDbContext context) { if (await context.VoiceWorks.AnyAsync()) return; @@ -126,61 +119,4 @@ public class VoiceWorkUpsertFixture : MariaDbFixture await context.SaveChangesAsync(); } - - public VoiceWorkIngest[] GetFirstRoundIngests() - { - VoiceWorkIngest[] ingests = - [ - new() - { - MakerId = "RG00001", - MakerName = "Good Dreams", - ProductId = "RJ0000001", - Title = "Today Sounds", - Description = "An average product.", - Tags = ["ASMR", "Office Lady"], - AgeRating = AgeRating.R15, - SalesDate = new DateOnly(2025, 1, 1), - ExpectedDate = null, - WishlistCount = 750, - Downloads = 500, - HasTrial = true, - HasDLPlay = true - }, - new() - { - MakerId = "RG00002", - MakerName = "Sweet Dreams", - ProductId = "RJ0000002", - Title = "Super Comfy ASMR", - Description = "An amazing product!", - AgeRating = AgeRating.AllAges, - Tags = ["ASMR", "Heartwarming", "Elf / Fairy", "Tsundere", "All Happy", "Gal", "Maid"], - SalesDate = new DateOnly(2025, 3, 1), - ExpectedDate = null, - WishlistCount = 12000, - Downloads = 5000, - HasTrial = true, - HasDLPlay = true - }, - new() - { - MakerId = "RG00003", - MakerName = "Nightmare Fuel", - ProductId = "RJ0000003", - Title = "Low Effort", - Description = "A bad product.", - Tags = ["Tsundere", "Non-Fiction / Narrative"], - AgeRating = AgeRating.R18, - SalesDate = new DateOnly(2025, 1, 1), - ExpectedDate = null, - WishlistCount = 100, - Downloads = 50, - HasTrial = true, - HasDLPlay = false - } - ]; - - return ingests; - } } \ No newline at end of file diff --git a/JSMR.Tests/Integration/TagSearchProviderTests.cs b/JSMR.Tests/Integration/TagSearchProviderTests.cs index 48d857d..cc62f72 100644 --- a/JSMR.Tests/Integration/TagSearchProviderTests.cs +++ b/JSMR.Tests/Integration/TagSearchProviderTests.cs @@ -3,16 +3,20 @@ using JSMR.Application.Tags.Queries.Search.Contracts; using JSMR.Infrastructure.Data; using JSMR.Infrastructure.Data.Repositories.Tags; using JSMR.Tests.Fixtures; +using JSMR.Tests.Ingestion; using Shouldly; +using System.ComponentModel; namespace JSMR.Tests.Integration; +//[Collection("db")] public class TagSearchProviderTests(TagSearchProviderFixture fixture) : IClassFixture { [Fact] public async Task Filter_None_Sort_Name() { await using AppDbContext context = fixture.CreateDbContext(); + //AppDbContext context = fixture.DbContext!; TagSearchProvider provider = new(context); var options = new SearchOptions() @@ -32,6 +36,7 @@ public class TagSearchProviderTests(TagSearchProviderFixture fixture) : IClassFi public async Task Filter_None_Sort_EnglishName() { await using AppDbContext context = fixture.CreateDbContext(); + //AppDbContext context = fixture.DbContext!; TagSearchProvider provider = new(context); var options = new SearchOptions() @@ -51,6 +56,7 @@ public class TagSearchProviderTests(TagSearchProviderFixture fixture) : IClassFi public async Task Filter_By_Name_Tag_Name() { await using AppDbContext context = fixture.CreateDbContext(); + //await using AppDbContext context = fixture.CreateDbContext(); TagSearchProvider provider = new(context); var options = new SearchOptions() @@ -72,6 +78,7 @@ public class TagSearchProviderTests(TagSearchProviderFixture fixture) : IClassFi public async Task Filter_By_Name_English_Tag_Name() { await using AppDbContext context = fixture.CreateDbContext(); + //AppDbContext context = fixture.DbContext!; TagSearchProvider provider = new(context); var options = new SearchOptions() diff --git a/JSMR.Tests/Integration/VoiceWorkUpsertTests.cs b/JSMR.Tests/Integration/VoiceWorkUpsertTests.cs deleted file mode 100644 index 6518270..0000000 --- a/JSMR.Tests/Integration/VoiceWorkUpsertTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using JSMR.Application.Scanning.Contracts; -using JSMR.Infrastructure.Common.Time; -using JSMR.Infrastructure.Data; -using JSMR.Infrastructure.Ingestion; -using JSMR.Tests.Fixtures; -using NSubstitute; -using Shouldly; - -namespace JSMR.Tests.Integration; - -public class VoiceWorkUpsertTests(VoiceWorkUpsertFixture fixture) : IClassFixture -{ - [Fact] - public async Task Simple_Upsert() - { - await using AppDbContext context = fixture.CreateDbContext(); - - VoiceWorkIngest[] ingests = - [ - new() - { - MakerId = "RG00001", - MakerName = "Good Dreams", - ProductId = "A Newly Announced Work", - Title = "", - Description = "" - }, - new() - { - MakerId = "RG00001", - MakerName = "Sweet Dreams", - ProductId = "", - Title = "", - Description = "" - } - ]; - - IClock clock = Substitute.For(); - clock.UtcNow.Returns(new DateTimeOffset(2025, 10, 1, 0, 0, 0, 0, TimeSpan.FromSeconds(0))); - - TokyoTimeProvider timeProvider = new(clock); - - VoiceWorkUpdater updater = new(context, timeProvider); - await updater.UpsertAsync(ingests, CancellationToken.None); - - context.VoiceWorks.Count().ShouldBe(2); - } -} \ No newline at end of file diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj index e48e47d..2a63195 100644 --- a/JSMR.Tests/JSMR.Tests.csproj +++ b/JSMR.Tests/JSMR.Tests.csproj @@ -30,6 +30,7 @@ + all