PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 16s
PR Gates / decisions lifecycle (pull_request) Successful in 19s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m24s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m43s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13m40s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 15m38s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 15m50s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
200 lines
6.8 KiB
C#
200 lines
6.8 KiB
C#
using ErsatzTV.Application.Search.Queries;
|
|
using ErsatzTV.Core.Api.Search;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using LanguageExt;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Application.Search;
|
|
|
|
[TestFixture]
|
|
public class GetSearchFieldValuesHandlerTests
|
|
{
|
|
private InMemoryTvContext _db = null!;
|
|
|
|
[SetUp]
|
|
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
|
|
|
[TearDown]
|
|
public async Task TearDown() => await _db.DisposeAsync();
|
|
|
|
[Test]
|
|
public async Task Returns_Distinct_Whole_Values_For_Genre_With_Prefix_And_Limit()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
context.Set<Genre>().AddRange(
|
|
new Genre { Name = "Action" },
|
|
new Genre { Name = "Adventure" },
|
|
new Genre { Name = "Animation" },
|
|
new Genre { Name = "Comedy" });
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("genre", "A", 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
result.IfSome(r => r.Values.ShouldBe(new List<string> { "Action", "Adventure", "Animation" }));
|
|
|
|
Option<SearchFieldValuesResponseModel> limited = await handler.Handle(
|
|
new GetSearchFieldValues("genre", "A", 2),
|
|
CancellationToken.None);
|
|
|
|
limited.IsSome.ShouldBeTrue();
|
|
limited.IfSome(r => r.Values.ShouldBe(new List<string> { "Action", "Adventure" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Splits_Content_Rating_On_Slash()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
context.MovieMetadata.Add(new MovieMetadata
|
|
{
|
|
MetadataKind = MetadataKind.External,
|
|
DateAdded = DateTime.UtcNow,
|
|
DateUpdated = DateTime.UtcNow,
|
|
ContentRating = "PG-13/TV-14"
|
|
});
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("content_rating", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
result.IfSome(r => r.Values.ShouldBe(new List<string> { "PG-13", "TV-14" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Returns_MediaItemState_Enum_Names_Without_Seeding()
|
|
{
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("state", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
result.IfSome(r => r.Values.ShouldBe(
|
|
new List<string> { "FileNotFound", "Normal", "RemoteOnly", "Unavailable" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Returns_NotFound_For_Excluded_Text_Field_Title()
|
|
{
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("title", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsNone.ShouldBeTrue();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Returns_NotFound_For_NonText_Field()
|
|
{
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
// "minutes" is a "number" field in SearchFieldCatalog, not "text"
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("minutes", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsNone.ShouldBeTrue();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Returns_NotFound_For_Unknown_Field()
|
|
{
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("nope", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsNone.ShouldBeTrue();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Matches_Case_Insensitive_Prefix()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
context.Set<Genre>().AddRange(
|
|
new Genre { Name = "Action" },
|
|
new Genre { Name = "Comedy" });
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("genre", "a", 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
result.IfSome(r => r.Values.ShouldBe(new List<string> { "Action" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Excludes_Network_And_Country_Tags_From_Tag_Field_And_Routes_Network_Tags_To_Network_Field()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
context.Set<Tag>().AddRange(
|
|
new Tag { Name = "PlainTag" },
|
|
new Tag { Name = "HBO", ExternalTypeId = Tag.PlexNetworkTypeId },
|
|
new Tag { Name = "USA", ExternalTypeId = Tag.NfoCountryTypeId });
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> tagResult = await handler.Handle(
|
|
new GetSearchFieldValues("tag", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
tagResult.IsSome.ShouldBeTrue();
|
|
tagResult.IfSome(r => r.Values.ShouldBe(new List<string> { "PlainTag" }));
|
|
|
|
Option<SearchFieldValuesResponseModel> networkResult = await handler.Handle(
|
|
new GetSearchFieldValues("network", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
networkResult.IsSome.ShouldBeTrue();
|
|
networkResult.IfSome(r => r.Values.ShouldBe(new List<string> { "HBO" }));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Dedupes_Repeated_Values()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
context.Set<Genre>().AddRange(
|
|
new Genre { Name = "Action" },
|
|
new Genre { Name = "Action" });
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetSearchFieldValuesHandler(_db.Factory);
|
|
|
|
Option<SearchFieldValuesResponseModel> result = await handler.Handle(
|
|
new GetSearchFieldValues("genre", string.Empty, 50),
|
|
CancellationToken.None);
|
|
|
|
result.IsSome.ShouldBeTrue();
|
|
result.IfSome(r => r.Values.ShouldBe(new List<string> { "Action" }));
|
|
}
|
|
}
|