Files
ersatztv/ErsatzTV.Tests/Application/Search/GetSearchFieldValuesHandlerTests.cs
T
timothyandClaude Opus 4.8 7e6861a928
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
fix(434): accurate endpoint description (DB-sourced) + tests for case-insensitivity, tag exclusion, distinct
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:55:32 +02:00

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