From cb15940d34eefa242d803f0c2e02285eb1188a92 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Mon, 1 Sep 2025 21:13:52 -0400 Subject: [PATCH] Finished voice work search provider implementation, and added several more tests. --- .../VoiceWorks/VoiceWorkSearchProvider.cs | 50 +- .../VoiceWorkSearchProviderFixture.cs | 64 ++- .../VoiceWorkSearchProviderTests.cs | 540 ++++++++++++++++++ 3 files changed, 629 insertions(+), 25 deletions(-) diff --git a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs index a1fb0cf..9344d18 100644 --- a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs +++ b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkSearchProvider.cs @@ -47,7 +47,7 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea filteredQuery = ApplyKeywordsFilter(filteredQuery, criteria); filteredQuery = ApplyCircleStatusFilter(filteredQuery, criteria); filteredQuery = ApplyTagStatusFilter(filteredQuery, criteria); - //filteredQuery = FilterCreatorStatus(filteredQuery, searchProperties.CreatorStatus, _voiceWorkContext); + filteredQuery = ApplyCreatorStatusFilter(filteredQuery, criteria); filteredQuery = ApplyTagIdsFilter(filteredQuery, criteria); filteredQuery = ApplyCreatorIdsFilter(filteredQuery, criteria); @@ -175,6 +175,54 @@ public class VoiceWorkSearchProvider(AppDbContext context, IVoiceWorkFullTextSea }; } + private IQueryable ApplyCreatorStatusFilter(IQueryable query, VoiceWorkSearchCriteria criteria) + { + if (criteria.CreatorStatus is null) + return query; + + // Handy local predicates that translate to EXISTS subqueries + bool HasFav(int voiceWorkId) => + context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Favorite); + + bool HasBlk(int voiceWorkId) => + context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == voiceWorkId && x.c.Blacklisted); + + return criteria.CreatorStatus switch + { + CreatorStatus.NotBlacklisted => + query.Where(q => !context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)), + + CreatorStatus.Blacklisted => + query.Where(q => context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted)), + + CreatorStatus.FavoriteIncludeBlacklist => + query.Where(q => context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite)), + + CreatorStatus.FavoriteExcludeBlacklist => + query.Where(q => + context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Favorite) + && + !context.VoiceWorkCreators + .Join(context.Creators, vwc => vwc.CreatorId, c => c.CreatorId, (vwc, c) => new { vwc, c }) + .Any(x => x.vwc.VoiceWorkId == q.VoiceWork.VoiceWorkId && x.c.Blacklisted) + ), + + _ => query + }; + } + private IQueryable ApplyTagIdsFilter(IQueryable filteredQuery, VoiceWorkSearchCriteria criteria) { if (criteria.TagIds.Length == 0) diff --git a/JSMR.Tests/Fixtures/VoiceWorkSearchProviderFixture.cs b/JSMR.Tests/Fixtures/VoiceWorkSearchProviderFixture.cs index 6435500..8b8ab3d 100644 --- a/JSMR.Tests/Fixtures/VoiceWorkSearchProviderFixture.cs +++ b/JSMR.Tests/Fixtures/VoiceWorkSearchProviderFixture.cs @@ -23,11 +23,11 @@ public class VoiceWorkSearchProviderFixture : MariaDbFixture ); context.VoiceWorks.AddRange( - new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1) }, - new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3) }, - new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2) }, - new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 4) }, - new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 5) } + new() { VoiceWorkId = 1, CircleId = 1, ProductId = "RJ0000001", ProductName = "Today Sounds", Description = "An average product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 1), Downloads = 500, WishlistCount = 750, StarRating = 35 }, + new() { VoiceWorkId = 2, CircleId = 2, ProductId = "RJ0000002", ProductName = "Super Comfy ASMR", Description = "An amazing product!", Status = (byte)VoiceWorkStatus.NewRelease, SalesDate = new(2025, 1, 3), Downloads = 5000, WishlistCount = 12000, StarRating = 50, Favorite = true }, + new() { VoiceWorkId = 3, CircleId = 3, ProductId = "RJ0000003", ProductName = "Low Effort", Description = "A bad product.", Status = (byte)VoiceWorkStatus.Available, SalesDate = new(2025, 1, 2), Downloads = 50, WishlistCount = 100, StarRating = 20 }, + new() { VoiceWorkId = 4, CircleId = 1, ProductId = "RJ0000004", ProductName = "Tomorrow Sounds", Description = "A average upcoming product.", Status = (byte)VoiceWorkStatus.Upcoming, ExpectedDate = new(2025, 1, 1), WishlistCount = 300 }, + new() { VoiceWorkId = 5, CircleId = 2, ProductId = "RJ0000005", ProductName = "Super Comfy ASMR+", Description = "All your favorite sounds, plus more!", Status = (byte)VoiceWorkStatus.NewAndUpcoming, ExpectedDate = new(2025, 1, 11), WishlistCount = 10000 } ); context.Tags.AddRange( @@ -38,7 +38,8 @@ public class VoiceWorkSearchProviderFixture : MariaDbFixture new() { TagId = 5, Name = "ツンデレ", Favorite = true }, new() { TagId = 6, Name = "オールハッピー" }, new() { TagId = 7, Name = "ギャル" }, - new() { TagId = 8, Name = "メイド" } + new() { TagId = 8, Name = "メイド" }, + new() { TagId = 9, Name = "ノンフィクション/体験談", Blacklisted = true } ); context.EnglishTags.AddRange( @@ -49,7 +50,8 @@ public class VoiceWorkSearchProviderFixture : MariaDbFixture 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" } + new() { EnglishTagId = 8, TagId = 8, Name = "Maid" }, + new() { EnglishTagId = 9, TagId = 9, Name = "Non-Fiction / Narrative" } ); context.VoiceWorkTags.AddRange( @@ -62,31 +64,31 @@ public class VoiceWorkSearchProviderFixture : MariaDbFixture new() { VoiceWorkId = 2, TagId = 5 }, // Tsundere new() { VoiceWorkId = 2, TagId = 6 }, // All Happy new() { VoiceWorkId = 2, TagId = 7 }, // Gal - new() { VoiceWorkId = 2, TagId = 8 } // Maid + new() { VoiceWorkId = 2, TagId = 8 }, // Maid - //new() { VoiceWorkId = 3, TagId = 1 }, - //new() { VoiceWorkId = 3, TagId = 1 }, + new() { VoiceWorkId = 3, TagId = 5 }, // Tsundere + 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 = 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 = 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 }, - //new() { VoiceWorkId = 5, 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( @@ -97,6 +99,20 @@ public class VoiceWorkSearchProviderFixture : MariaDbFixture new() { CreatorId = 5, Name = "山田じぇみ子", Blacklisted = true } ); + context.VoiceWorkCreators.AddRange( + new() { VoiceWorkId = 1, CreatorId = 2 }, // 秋野かえで + + new() { VoiceWorkId = 2, CreatorId = 1 }, // 陽向葵ゅか + + new() { VoiceWorkId = 3, CreatorId = 5 }, // 山田じぇみ子 + new() { VoiceWorkId = 3, CreatorId = 1 }, // 陽向葵ゅか + + new() { VoiceWorkId = 4, CreatorId = 3 }, // 柚木つばめ + + new() { VoiceWorkId = 5, CreatorId = 1 }, // 陽向葵ゅか + new() { VoiceWorkId = 5, CreatorId = 4 } // 逢坂成美 + ); + // context.VoiceWorkSearches.AddRange( new() { VoiceWorkId = 1, SearchText = "RJ0000001 RG00001 Good Dreams Today Sounds An average product. ASMR Office Lady" }, diff --git a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs index 392717f..242d487 100644 --- a/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs +++ b/JSMR.Tests/Integration/VoiceWorkSearchProviderTests.cs @@ -174,4 +174,544 @@ public class VoiceWorkSearchProviderTests(VoiceWorkSearchProviderFixture fixture result.TotalItems.ShouldBe(1); result.Items.ShouldAllBe(item => (item.Description ?? string.Empty).Contains("All Your Favorite", StringComparison.OrdinalIgnoreCase)); } + + [Fact] + public async Task Filter_Tags_Favorite_Exclude_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagStatus = TagStatus.FavoriteExcludeBlacklist + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002"]); + } + + [Fact] + public async Task Filter_Tags_Favorite_Include_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagStatus = TagStatus.FavoriteIncludeBlacklist + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(2); + result.TotalItems.ShouldBe(2); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000003"]); + } + + [Fact] + public async Task Filter_Tags_Not_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagStatus = TagStatus.NotBlacklisted + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000002", "RJ0000004", "RJ0000005"]); + } + + [Fact] + public async Task Filter_Tags_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagStatus = TagStatus.Blacklisted + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000003"]); + } + + [Fact] + public async Task Filter_TagIds_Or() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagIds = [1,2], + IncludeAllTags = false + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(2); + result.TotalItems.ShouldBe(2); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000002"]); + } + + [Fact] + public async Task Filter_TagIds_And() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + TagIds = [1, 2], + IncludeAllTags = true + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001"]); + } + + [Fact] + public async Task Filter_Creators_Favorite_Exclude_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorStatus = CreatorStatus.FavoriteExcludeBlacklist + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(2); + result.TotalItems.ShouldBe(2); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000005"]); + } + + [Fact] + public async Task Filter_Creators_Favorite_Include_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorStatus = CreatorStatus.FavoriteIncludeBlacklist + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(3); + result.TotalItems.ShouldBe(3); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000003", "RJ0000005"]); + } + + [Fact] + public async Task Filter_Creators_Not_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorStatus = CreatorStatus.NotBlacklisted + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(4); + result.TotalItems.ShouldBe(4); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000002", "RJ0000004", "RJ0000005"]); + } + + [Fact] + public async Task Filter_Creators_Blacklisted() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorStatus = CreatorStatus.Blacklisted + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000003"]); + } + + [Fact] + public async Task Filter_CreatorIds_Or() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorIds = [1, 4], + IncludeAllCreators = false + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(3); + result.TotalItems.ShouldBe(3); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000003", "RJ0000005"]); + } + + [Fact] + public async Task Filter_CreatorIds_And() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + CreatorIds = [1, 4], + IncludeAllCreators = true + } + }; + + var result = await provider.SearchAsync(options); + + result.Items.Length.ShouldBe(1); + result.TotalItems.ShouldBe(1); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000005"]); + } + + [Fact] + public async Task Sort_By_Release_Date_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.ReleaseDate, Application.Common.Search.SortDirection.Ascending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000004", "RJ0000003", "RJ0000002", "RJ0000005"]); + } + + [Fact] + public async Task Sort_By_Release_Date_Descending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.ReleaseDate, Application.Common.Search.SortDirection.Descending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000005", "RJ0000002", "RJ0000003", "RJ0000001", "RJ0000004"]); + } + + [Fact] + public async Task Sort_By_Downloads_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.Downloads, Application.Common.Search.SortDirection.Ascending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000004", "RJ0000005", "RJ0000003", "RJ0000001", "RJ0000002"]); + } + + [Fact] + public async Task Sort_By_Downloads_Descending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.Downloads, Application.Common.Search.SortDirection.Descending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000001", "RJ0000003", "RJ0000004", "RJ0000005"]); + } + + [Fact] + public async Task Sort_By_WishlistCount_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.WishlistCount, Application.Common.Search.SortDirection.Ascending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000003", "RJ0000004", "RJ0000001", "RJ0000005", "RJ0000002"]); + } + + [Fact] + public async Task Sort_By_WishlistCount_Descending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.WishlistCount, Application.Common.Search.SortDirection.Descending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000005", "RJ0000001", "RJ0000004", "RJ0000003"]); + } + + [Fact] + public async Task Sort_By_StarRating_Ascending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.StarRating, Application.Common.Search.SortDirection.Ascending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000004", "RJ0000005", "RJ0000003", "RJ0000001", "RJ0000002"]); + } + + [Fact] + public async Task Sort_By_StarRating_Descending() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + SortOptions = + [ + new(VoiceWorkSortField.StarRating, Application.Common.Search.SortDirection.Descending) + ] + }; + + var result = await provider.SearchAsync(options); + + result.Items + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002", "RJ0000001", "RJ0000003", "RJ0000004", "RJ0000005"]); + } + + [Fact] + public async Task Filter_Release_Date_Range() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + ReleaseDateStart = new DateTime(2025, 1, 1), + ReleaseDateEnd = new DateTime(2025, 1, 2) + } + }; + + var result = await provider.SearchAsync(options); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000003"]); + } + + [Fact] + public async Task Filter_Downloads_Range() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + MinDownloads = 100, + MaxDownloads = 10000 + } + }; + + var result = await provider.SearchAsync(options); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000001", "RJ0000002"]); + } + + [Fact] + public async Task Filter_Favorite() + { + await using AppDbContext context = fixture.CreateDbContext(); + VoiceWorkSearchProvider provider = InitializeVoiceWorkSearchProvider(context); + + var options = new SearchOptions() + { + Criteria = new() + { + ShowFavoriteVoiceWorks = true + } + }; + + var result = await provider.SearchAsync(options); + + result.Items + .OrderBy(item => item.ProductId) + .Select(item => item.ProductId) + .ShouldBe(["RJ0000002"]); + } } \ No newline at end of file