Added English localization integration tests.

This commit is contained in:
2025-10-25 01:03:59 -04:00
parent 36fcd5379a
commit 99c397b3bc
16 changed files with 492 additions and 158 deletions

View File

@@ -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<MariaDbContainerFixture> { }
//[UsedImplicitly]
public sealed class MariaDbContainerFixture2(IMessageSink messageSink)
: ContainerFixture<MariaDbBuilder, MariaDbContainer>(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: well 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<AppDbContext> CreateIsolatedAsync(string rootConnectionString, Func<AppDbContext, Task>? 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<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>()
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
o => o.EnableRetryOnFailure())
.EnableSensitiveDataLogging()
.Options;
return new AppDbContext(options);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<MariaDbContainerFixture>
{
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");
}
}

View File

@@ -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<IClock>();
// 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);
// }
//}

View File

@@ -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;
}
}

View File

@@ -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<TagSearchProviderFixture>
{
[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<TagSearchCriteria, TagSortField>()
@@ -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<TagSearchCriteria, TagSortField>()
@@ -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<TagSearchCriteria, TagSortField>()
@@ -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<TagSearchCriteria, TagSortField>()

View File

@@ -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<VoiceWorkUpsertFixture>
{
[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<IClock>();
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);
}
}

View File

@@ -30,6 +30,7 @@
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testcontainers" Version="4.8.1" />
<PackageReference Include="Testcontainers.MariaDb" Version="4.8.1" />
<PackageReference Include="Testcontainers.Xunit" Version="4.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>