From 516060963e945fb8b2a5c5b28b045ba2b16e027e Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sat, 30 Aug 2025 16:21:35 -0400 Subject: [PATCH] Update search provider sort logic, and added testing for circle search provider. --- .../Circles/Queries/Search/CircleSortField.cs | 3 +- .../Common/Queries/SearchProvider.cs | 36 ++- .../Circles/CircleSearchProvider.cs | 26 +- .../Creators/CreatorSearchProvider.cs | 8 +- .../Repositories/Tags/TagSearchProvider.cs | 9 +- .../Fixtures/CircleSearchProviderFixture.cs | 28 ++ JSMR.Tests/Fixtures/MariaDbFixture.cs | 67 +++++ JSMR.Tests/Fixtures/SearchProviderFixture.cs | 65 +++++ .../Integration/CircleSearchProviderTests.cs | 242 +++++++++++++++--- JSMR.Tests/Integration/MariaDbFixture.cs | 89 ------- JSMR.Tests/JSMR.Tests.csproj | 5 +- 11 files changed, 435 insertions(+), 143 deletions(-) create mode 100644 JSMR.Tests/Fixtures/CircleSearchProviderFixture.cs create mode 100644 JSMR.Tests/Fixtures/MariaDbFixture.cs create mode 100644 JSMR.Tests/Fixtures/SearchProviderFixture.cs delete mode 100644 JSMR.Tests/Integration/MariaDbFixture.cs diff --git a/JSMR.Application/Circles/Queries/Search/CircleSortField.cs b/JSMR.Application/Circles/Queries/Search/CircleSortField.cs index 036648f..9d9ffa7 100644 --- a/JSMR.Application/Circles/Queries/Search/CircleSortField.cs +++ b/JSMR.Application/Circles/Queries/Search/CircleSortField.cs @@ -5,6 +5,5 @@ public enum CircleSortField Name, Blacklisted, Favorite, - Spam, - VoiceWorkCount + Spam } \ No newline at end of file diff --git a/JSMR.Infrastructure/Common/Queries/SearchProvider.cs b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs index 645e4ac..7040abc 100644 --- a/JSMR.Infrastructure/Common/Queries/SearchProvider.cs +++ b/JSMR.Infrastructure/Common/Queries/SearchProvider.cs @@ -16,7 +16,7 @@ public abstract class SearchProvider : int total = await filteredQuery.CountAsync(cancellationToken); IOrderedQueryable orderedQuery = ApplySorting(filteredQuery, options.SortOptions); - IOrderedQueryable selectQuery = GetSelectQuery(orderedQuery); + IQueryable selectQuery = GetSelectQuery(orderedQuery); TItem[] items = await selectQuery .Skip((options.PageNumber - 1) * options.PageSize) @@ -50,10 +50,40 @@ public abstract class SearchProvider : ordered = (i == 0) ? applyFirst(selector) : applyNext(selector); } - return ordered ?? GetDefaultSortExpression(query); + //return ordered ?? GetDefaultSortExpression(query); + + // Always add the default as the final tiebreaker + var chain = GetDefaultSortChain(); + + if (ordered is null) + { + using var e = chain.GetEnumerator(); + if (!e.MoveNext()) throw new InvalidOperationException("No default sort provided."); + + var (Selector, Dir) = e.Current; + var res = Dir == SortDirection.Descending + ? query.OrderByDescending(Selector) + : query.OrderBy(Selector); + + while (e.MoveNext()) + res = e.Current.Dir == SortDirection.Descending + ? res.ThenByDescending(e.Current.Selector) + : res.ThenBy(e.Current.Selector); + + return res; + } + else + { + var res = ordered; + foreach (var (sel, dir) in chain) + res = dir == SortDirection.Descending ? res.ThenByDescending(sel) : res.ThenBy(sel); + return res; + } } protected abstract Expression> GetSortExpression(TSortField field); protected abstract IOrderedQueryable GetDefaultSortExpression(IQueryable query); - protected abstract IOrderedQueryable GetSelectQuery(IOrderedQueryable query); + //protected abstract (Expression> Selector, SortDirection Direction) GetDefaultSortExpression(); + protected abstract IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain(); + protected abstract IQueryable GetSelectQuery(IOrderedQueryable query); } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs index 6bbcb51..ba9e990 100644 --- a/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Circles/CircleSearchProvider.cs @@ -1,4 +1,5 @@ using JSMR.Application.Circles.Queries.Search; +using JSMR.Application.Common.Search; using JSMR.Infrastructure.Common.Queries; using Microsoft.EntityFrameworkCore; using System.Linq.Expressions; @@ -102,22 +103,22 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider> GetSortExpression(CircleSortField field) => field switch { - //CircleSortField.MakerId => x => x.MakerId, - //CircleSortField.Downloads => x => x.Downloads, - //CircleSortField.Releases => x => x.Releases, - //CircleSortField.Pending => x => x.Pending, - //CircleSortField.FirstReleaseDate => x => x.FirstReleaseDate ?? DateTime.MinValue, - //CircleSortField.LatestReleaseDate => x => x.LatestReleaseDate ?? DateTime.MinValue, - CircleSortField.Favorite => x => x.Favorite, - CircleSortField.Blacklisted => x => x.Blacklisted, - CircleSortField.Spam => x => x.Spam, + CircleSortField.Favorite => x => !x.Favorite, + CircleSortField.Blacklisted => x => !x.Blacklisted, + CircleSortField.Spam => x => !x.Spam, _ => x => x.Name }; protected override IOrderedQueryable GetDefaultSortExpression(IQueryable query) => query.OrderBy(x => x.Name).ThenBy(x => x.CircleId); - protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) + protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() + { + yield return (x => x.Name, SortDirection.Ascending); + yield return (x => x.MakerId, SortDirection.Ascending); + } + + protected override IQueryable GetSelectQuery(IOrderedQueryable query) { // Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate var selected = @@ -142,9 +143,6 @@ public class CircleSearchProvider(AppDbContext context) : SearchProvider 0).ThenBy(x => x.Name).ThenBy(x => x.CircleId); - // NOTE: If your base class re-applies ordering after Select, you can just: - // return selected.OrderBy(x => x.Name).ThenBy(x => x.CircleId); + return selected; } } \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs index b46c0a9..33ddd51 100644 --- a/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Creators/CreatorSearchProvider.cs @@ -1,4 +1,5 @@ -using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; using JSMR.Application.Creators.Queries.Search.Ports; using JSMR.Infrastructure.Common.Queries; using System.Linq.Expressions; @@ -54,6 +55,11 @@ public class CreatorSearchProvider(AppDbContext context) : SearchProvider x.Name); } + protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() + { + yield return (x => x.Name ?? string.Empty, SortDirection.Ascending); + } + protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) { return query; diff --git a/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs index d1be153..3eac752 100644 --- a/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/Tags/TagSearchProvider.cs @@ -1,4 +1,6 @@ -using JSMR.Application.Tags.Queries.Search.Contracts; +using JSMR.Application.Common.Search; +using JSMR.Application.Creators.Queries.Search.Contracts; +using JSMR.Application.Tags.Queries.Search.Contracts; using JSMR.Application.Tags.Queries.Search.Ports; using JSMR.Infrastructure.Common.Queries; using System.Linq.Expressions; @@ -59,6 +61,11 @@ public class TagSearchProvider(AppDbContext context) : SearchProvider x.Name); } + protected override IEnumerable<(Expression> Selector, SortDirection Dir)> GetDefaultSortChain() + { + yield return (x => x.Name ?? string.Empty, SortDirection.Ascending); + } + protected override IOrderedQueryable GetSelectQuery(IOrderedQueryable query) { return query; diff --git a/JSMR.Tests/Fixtures/CircleSearchProviderFixture.cs b/JSMR.Tests/Fixtures/CircleSearchProviderFixture.cs new file mode 100644 index 0000000..100fe2a --- /dev/null +++ b/JSMR.Tests/Fixtures/CircleSearchProviderFixture.cs @@ -0,0 +1,28 @@ +using JSMR.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Tests.Fixtures; + +public class CircleSearchProviderFixture : MariaDbFixture +{ + protected override async Task OnInitializedAsync(AppDbContext context) + { + await SeedAsync(context); + } + + private static async Task SeedAsync(AppDbContext context) + { + // Make seeding idempotent (quick existence check) + if (await context.Circles.AnyAsync()) + return; + + context.Circles.AddRange( + new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" }, + new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" }, + new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" }, + new() { CircleId = 4, Name = "Garbage Studio", Spam = true, MakerId = "RG00004" } + ); + + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Fixtures/MariaDbFixture.cs b/JSMR.Tests/Fixtures/MariaDbFixture.cs new file mode 100644 index 0000000..d68e5c1 --- /dev/null +++ b/JSMR.Tests/Fixtures/MariaDbFixture.cs @@ -0,0 +1,67 @@ +using JSMR.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Testcontainers.MariaDb; + +namespace JSMR.Tests.Fixtures; + +public class MariaDbFixture : IAsyncLifetime +{ + const int MajorVersion = 10; + const int MinorVersion = 11; + const int Build = 6; + + public MariaDbContainer? MariaDbContainer { get; private set; } + + public string ConnectionString { get; private set; } = default!; + + public async Task InitializeAsync() + { + MariaDbContainer = new MariaDbBuilder() + .WithImage($"mariadb:{MajorVersion}.{MinorVersion}.{Build}") + .Build(); + + await MariaDbContainer.StartAsync(); + + ConnectionString = MariaDbContainer.GetConnectionString(); + + await using AppDbContext context = CreateDbContext(); + await context.Database.EnsureCreatedAsync(); + + await OnInitializedAsync(context); + } + + protected virtual Task OnInitializedAsync(AppDbContext context) + { + return Task.FromResult(Task.CompletedTask); + } + + public async Task DisposeAsync() + { + if (MariaDbContainer is not null) + { + await MariaDbContainer.StopAsync(); + await MariaDbContainer.DisposeAsync(); + } + } + + public AppDbContext CreateDbContext() + { + MySqlServerVersion serverVersion = new(new Version(MajorVersion, MinorVersion, Build)); + + DbContextOptions options = new DbContextOptionsBuilder() + .UseMySql(ConnectionString, serverVersion, + o => o.EnableRetryOnFailure()) + .EnableSensitiveDataLogging() + .Options; + + return new AppDbContext(options); + } + + public async Task ResetAsync() + { + await using AppDbContext context = CreateDbContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Fixtures/SearchProviderFixture.cs b/JSMR.Tests/Fixtures/SearchProviderFixture.cs new file mode 100644 index 0000000..660f351 --- /dev/null +++ b/JSMR.Tests/Fixtures/SearchProviderFixture.cs @@ -0,0 +1,65 @@ +using JSMR.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace JSMR.Tests.Fixtures; + +public class SearchProviderFixture : MariaDbFixture +{ + protected override async Task OnInitializedAsync(AppDbContext context) + { + await SeedAsync(context); + } + + private static async Task SeedAsync(AppDbContext context) + { + // Make seeding idempotent (quick existence check) + if (await context.Circles.AnyAsync()) + return; + + context.Circles.AddRange( + new() { CircleId = 1, Name = "Good Dreams", MakerId = "RG00001" }, + new() { CircleId = 2, Name = "Sweet Dreams", Favorite = true, MakerId = "RG00002" }, + new() { CircleId = 3, Name = "Nightmare Fuel", Blacklisted = true, MakerId = "RG00003" } + ); + + //context.VoiceWorks.AddRange( + // new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available }, + // new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease }, + // new() { VoiceWorkId = 4, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available }, + // new() { VoiceWorkId = 5, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming }, + // new() { VoiceWorkId = 6, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming } + //); + + context.Tags.AddRange( + new() { TagId = 1, Name = "ASMR" }, + new() { TagId = 2, Name = "OL" }, + new() { TagId = 3, Name = "ほのぼの" }, + new() { TagId = 4, Name = "エルフ/妖精" }, + new() { TagId = 5, Name = "ツンデレ", Favorite = true }, + new() { TagId = 6, Name = "オールハッピー" }, + new() { TagId = 7, Name = "ギャル" }, + new() { TagId = 8, Name = "メイド" } + ); + + context.EnglishTags.AddRange( + new() { EnglishTagId = 1, TagId = 1, Name = "ASMR" }, + new() { EnglishTagId = 2, TagId = 2, Name = "Office Lady" }, + new() { EnglishTagId = 3, TagId = 3, Name = "Heartwarming" }, + new() { EnglishTagId = 4, TagId = 4, Name = "Elf / Fairy" }, + new() { EnglishTagId = 5, TagId = 5, Name = "Tsundere" }, + new() { EnglishTagId = 6, TagId = 6, Name = "All Happy" }, + new() { EnglishTagId = 7, TagId = 7, Name = "Gal" }, + new() { EnglishTagId = 8, TagId = 8, Name = "Maid" } + ); + + context.Creators.AddRange( + new() { CreatorId = 1, Name = "陽向葵ゅか", Favorite = true }, + new() { CreatorId = 2, Name = "秋野かえで" }, + new() { CreatorId = 3, Name = "柚木つばめ" }, + new() { CreatorId = 4, Name = "逢坂成美" }, + new() { CreatorId = 5, Name = "山田じぇみ子", Blacklisted = true } + ); + + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Integration/CircleSearchProviderTests.cs b/JSMR.Tests/Integration/CircleSearchProviderTests.cs index 0be3c9f..b9e322c 100644 --- a/JSMR.Tests/Integration/CircleSearchProviderTests.cs +++ b/JSMR.Tests/Integration/CircleSearchProviderTests.cs @@ -3,54 +3,234 @@ using JSMR.Application.Circles.Queries.Search; using JSMR.Application.Common.Search; using JSMR.Infrastructure.Data; using JSMR.Infrastructure.Data.Repositories.Circles; +using JSMR.Tests.Fixtures; +using Shouldly; namespace JSMR.Tests.Integration; -public class CircleSearchProviderTests(MariaDbFixture fixture) : IClassFixture +public class CircleSearchProviderTests(CircleSearchProviderFixture fixture) : IClassFixture { [Fact] - public async Task Search_ByName_Filters_And_Sorts() + public async Task Filter_None() { - await fixture.ResetAsync(); await using AppDbContext context = fixture.CreateDbContext(); - await Seed.SeedCirclesWithWorksAsync(context); - CircleSearchProvider provider = new(context); - var options = new SearchOptions + var options = new SearchOptions() { - PageNumber = 1, - PageSize = 50, - SortOptions = [new SortOption(CircleSortField.Name, SortDirection.Ascending)], - Criteria = new CircleSearchCriteria { Name = "Circle" } + Criteria = new() + { + + } }; var result = await provider.SearchAsync(options, CancellationToken.None); - Assert.True(result.TotalItems >= 2); - Assert.Equal("Circle A", result.Items[0].Name); - Assert.Equal("Circle B", result.Items[1].Name); + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); } - //[Fact] - //public async Task Search_Status_Favorited_Only() - //{ - // await fixture.ResetAsync(); - // await using var db = fixture.CreateDbContext(); - // await Seed.SeedCirclesWithWorksAsync(db); + [Fact] + public async Task Filter_By_Status_Not_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); - // var provider = new CircleSearchProvider(db); + var options = new SearchOptions() + { + Criteria = new() + { + Status = Application.Circles.Queries.Search.CircleStatus.NotBlacklisted + } + }; - // var options = new SearchOptions - // { - // PageNumber = 1, - // PageSize = 50, - // SortOptions = Array.Empty>(), - // Criteria = new CircleSearchCriteria { Status = Application.Circles.Queries.Search.CircleStatus.Favorited } - // }; + var result = await provider.SearchAsync(options, CancellationToken.None); - // var result = await provider.SearchAsync(options, CancellationToken.None); + result.Items.Length.ShouldBe(3); + result.TotalItems.ShouldBe(3); + result.Items.ShouldNotContain(item => item.Blacklisted); + } - // Assert.All(result.Items, i => Assert.True(i.Favorite)); - //} -} + [Fact] + public async Task Filter_By_Status_Favorited() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + Criteria = new() + { + Status = Application.Circles.Queries.Search.CircleStatus.Favorited + } + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + result.Items.ShouldAllBe(item => item.Favorite); + } + + [Fact] + public async Task Filter_By_Status_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + Criteria = new() + { + Status = Application.Circles.Queries.Search.CircleStatus.Blacklisted + } + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + result.Items.ShouldAllBe(item => item.Blacklisted); + } + + [Fact] + public async Task Filter_By_Status_Spam() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + Criteria = new() + { + Status = Application.Circles.Queries.Search.CircleStatus.Spam + } + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + result.Items.ShouldAllBe(item => item.Spam); + } + + [Fact] + public async Task Filter_By_Name_Circle_Name() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + Criteria = new() + { + Name = "Dreams" + } + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(2); + result.TotalItems.ShouldBe(2); + result.Items.ShouldAllBe(item => item.Name.Contains("Dreams", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Filter_By_Name_Circle_Id() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + Criteria = new() + { + Name = "003" + } + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + result.Items.ShouldContain(item => item.MakerId.Contains("003", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public async Task Sort_By_Name_Descending() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + SortOptions = [new(CircleSortField.Name, Application.Common.Search.SortDirection.Descending)] + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + result.Items[0].Name.ShouldBe("Sweet Dreams"); + result.Items[1].Name.ShouldBe("Nightmare Fuel"); + result.Items[2].Name.ShouldBe("Good Dreams"); + result.Items[3].Name.ShouldBe("Garbage Studio"); + } + + [Fact] + public async Task Sort_By_Favorite_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + SortOptions = [new(CircleSortField.Favorite, Application.Common.Search.SortDirection.Ascending)] + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + result.Items[0].Name.ShouldBe("Sweet Dreams"); + result.Items[1].Name.ShouldBe("Garbage Studio"); + } + + [Fact] + public async Task Sort_By_Blacklisted_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + SortOptions = [new(CircleSortField.Blacklisted, Application.Common.Search.SortDirection.Ascending)] + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + result.Items[0].Name.ShouldBe("Nightmare Fuel"); + result.Items[1].Name.ShouldBe("Garbage Studio"); + } + + [Fact] + public async Task Sort_By_Spam_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + CircleSearchProvider provider = new(context); + + var options = new SearchOptions() + { + SortOptions = [new(CircleSortField.Spam, Application.Common.Search.SortDirection.Ascending)] + }; + + var result = await provider.SearchAsync(options, CancellationToken.None); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + result.Items[0].Name.ShouldBe("Garbage Studio"); + result.Items[1].Name.ShouldBe("Good Dreams"); + } +} \ No newline at end of file diff --git a/JSMR.Tests/Integration/MariaDbFixture.cs b/JSMR.Tests/Integration/MariaDbFixture.cs deleted file mode 100644 index 9763c22..0000000 --- a/JSMR.Tests/Integration/MariaDbFixture.cs +++ /dev/null @@ -1,89 +0,0 @@ -using DotNet.Testcontainers.Builders; -using JSMR.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Testcontainers.MariaDb; - -namespace JSMR.Tests.Integration; - -public sealed class MariaDbFixture : IAsyncLifetime -{ - public MariaDbContainer? MariaDbContainer { get; private set; } - - public string ConnectionString { get; private set; } = default!; - - public async Task InitializeAsync() - { - MariaDbContainer = new MariaDbBuilder() - .WithImage("mariadb:10.11.6") - .WithEnvironment("MARIADB_ROOT_PASSWORD", "rootpwd") - .WithEnvironment("MARIADB_DATABASE", "appdb") - .WithPortBinding(3307, 3306) // host:container; avoid conflicts - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(3306)) - .Build(); - - await MariaDbContainer.StartAsync(); - - ConnectionString = - "Server=localhost;Port=3307;Database=appdb;User=root;Password=rootpwd;SslMode=None;AllowPublicKeyRetrieval=True;"; - - //ConnectionString = MariaDbContainer.GetConnectionString(); - - // Run migrations here to create schema - await using AppDbContext context = CreateDbContext(); - await context.Database.EnsureCreatedAsync(); - //await context.Database.MigrateAsync(); - } - - public async Task DisposeAsync() - { - if (MariaDbContainer is not null) - { - await MariaDbContainer.StopAsync(); - await MariaDbContainer.DisposeAsync(); - } - } - - public AppDbContext CreateDbContext() - { - MySqlServerVersion serverVersion = new(new Version(10, 11, 6)); - - DbContextOptions options = new DbContextOptionsBuilder() - .UseMySql(ConnectionString, serverVersion, - o => o.EnableRetryOnFailure()) - .EnableSensitiveDataLogging() - .Options; - - return new AppDbContext(options); - } - - /// Clean tables between tests; use Respawn or manual TRUNCATE in correct FK order. - public async Task ResetAsync() - { - await using AppDbContext context = CreateDbContext(); - - await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); - - //await using var connection = context.Database.GetDbConnection(); - //await connection.OpenAsync(); - - //using var cmd = connection.CreateCommand(); - //cmd.CommandText = "SELECT DATABASE()"; - - //var dbName = (string?)await cmd.ExecuteScalarAsync(); - //Console.WriteLine($"[TEST] Connected to DB: {dbName}"); - - // Fast reset (example): disable FK checks, truncate, re-enable - //await context.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 0;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE voice_work_creators;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE voice_work_tags;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE english_tags;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE english_voice_works;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE voice_work_searches;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE voice_works;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE creators;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE tags;"); - //await context.Database.ExecuteSqlRawAsync("TRUNCATE TABLE circles;"); - //await context.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 1;"); - } -} \ No newline at end of file diff --git a/JSMR.Tests/JSMR.Tests.csproj b/JSMR.Tests/JSMR.Tests.csproj index 49cf77a..ad29a4b 100644 --- a/JSMR.Tests/JSMR.Tests.csproj +++ b/JSMR.Tests/JSMR.Tests.csproj @@ -14,8 +14,9 @@ - - + + + all