Files
ersatztv/ErsatzTV.Tests/Controllers/LibraryBrowseControllerTests.cs
T
timothyandClaude Fable 5 54b18e73cd fix(api): root library-browse artwork URLs and add season drill-in (#180)
Part of the #180 library picker fixes (API side).

Broken artwork: GetLibraryBrowseItemsHandler.Artwork returned Blazor-convention
values (a bare cache filename, or a RELATIVE jellyfin/emby proxy path). Blazor's
GetPosterUrl prefixes those with "artwork/posters/" against <base href="/">, but
the React SPA renders item.artwork raw as <img src> from under /app/, so every
image 404'd. The handler now returns rooted, directly-usable URLs
(/artwork/posters/... or /artwork/thumbnails/... per ArtworkKind; jellyfin/emby
mapped to their proxy routes with fillHeight/maxHeight; absolute http(s) URLs
passed through; empty stays empty).

Seasons with no poster of their own now fall back to the parent show's poster
(SeasonArtwork + the extra ShowMetadata.Artwork include).

Season drill-in: GET /api/library/browse gains an optional `parentId` query
param that, with mediaType=TelevisionSeason, returns that show's seasons
(season-number order, bypassing Lucene) so the SPA can expand a show into its
seasons. OpenAPI spec regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:10 +02:00

95 lines
2.8 KiB
C#

using System.Reflection;
using ErsatzTV.Application.LibraryBrowse;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class LibraryBrowseControllerTests
{
private LibraryBrowseController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new LibraryBrowseController(_mediator);
}
[Test]
public void Controller_Should_Expose_Browse_Route_With_Stable_Operation_Name()
{
MethodInfo action = typeof(LibraryBrowseController).GetMethod(nameof(LibraryBrowseController.Browse))
?? throw new AssertionException($"Missing action {nameof(LibraryBrowseController.Browse)}");
var attribute = action.GetCustomAttributes<HttpGetAttribute>().Single();
attribute.Template.ShouldBe("/api/library/browse");
attribute.Name.ShouldBe("BrowseLibrary");
}
[Test]
public async Task Browse_Should_Clamp_Paging_And_Send_Query()
{
_mediator.Send(Arg.Any<GetLibraryBrowseItems>(), Arg.Any<CancellationToken>())
.Returns(new PagedLibraryBrowseItemsResponseModel(0, []));
await _controller.Browse(
"star",
123,
LibraryBrowseMediaType.Movie,
-1,
500,
77,
CancellationToken.None);
await _mediator.Received(1).Send(
Arg.Is<GetLibraryBrowseItems>(q =>
q.Query == "star" &&
q.LibraryId == 123 &&
q.MediaType == LibraryBrowseMediaType.Movie &&
q.PageNum == 0 &&
q.PageSize == 100 &&
q.ParentId == 77),
Arg.Any<CancellationToken>());
}
[Test]
public async Task Browse_Should_Return_Paged_Response()
{
var item = new LibraryBrowseItemResponseModel(
10,
LibraryBrowseMediaType.Movie,
"Movie",
3,
"Movies",
"/artwork/posters/movie.jpg",
TimeSpan.FromMinutes(90),
1,
null,
CollectionType.Movie,
null,
null,
null,
null,
10,
null);
_mediator.Send(Arg.Any<GetLibraryBrowseItems>(), Arg.Any<CancellationToken>())
.Returns(new PagedLibraryBrowseItemsResponseModel(1, [item]));
PagedLibraryBrowseItemsResponseModel result = await _controller.Browse(
cancellationToken: CancellationToken.None);
result.TotalCount.ShouldBe(1);
result.Page.ShouldBe([item]);
}
}