Files
deepdrft/DeepDrftTests/CutDetailTrackOrderingTests.cs
T
daniel-c-harvey 07ddc69cee feat(public): add /cuts/{id} album-detail page
Compose ReleaseDetailScaffold via Header + BodyContent slots for the Cut
album view: left meta + Play/Share, right theme-bordered cover, TrackNumber-
ordered track list with per-row play. CutDetailBase carries the multi-track
prerender bridge.
2026-06-15 23:59:19 -04:00

153 lines
6.1 KiB
C#

using Data.Data.Repositories;
using DeepDrftData;
using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Models.Common;
namespace DeepDrftTests;
/// <summary>
/// Backs the public read path that the /cuts/{id} album page consumes (Phase 11 §3a, §3.3).
/// <c>CutDetailViewModel.Load</c> fetches an album's tracks through the releaseId-filtered track page
/// sorted by "TrackNumber"; that maps to <see cref="TrackRepository.GetPagedFilteredAsync"/> with a
/// <see cref="TrackFilter.ReleaseId"/> predicate and an <c>OrderBy(t =&gt; t.TrackNumber)</c>
/// expression. These tests exercise that exact query — the join narrowing, the explicit-ordinal
/// ordering (not insertion order), and the projection of TrackNumber onto the DTO the page renders.
///
/// Provider note: runs on the EF in-memory provider, which executes the ReleaseId equality, the
/// ordinal sort, and the count in process — every predicate this path uses (no ILike branch here).
/// Mirrors <see cref="TrackFilterQueryTests"/>.
/// </summary>
[TestFixture]
public class CutDetailTrackOrderingTests
{
private DeepDrftContext _context = null!;
[SetUp]
public void SetUp()
{
var options = new DbContextOptionsBuilder<DeepDrftContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new DeepDrftContext(options);
}
[TearDown]
public void TearDown() => _context.Dispose();
private TrackRepository CreateRepository()
=> new(_context, NullLogger<Repository<DeepDrftContext, TrackEntity>>.Instance);
private static ReleaseEntity Release(string title, string artist)
=> new() { Title = title, Artist = artist };
// A track linked to the given release with an explicit ordinal.
private static TrackEntity Track(string name, int trackNumber, ReleaseEntity? release = null)
=> new()
{
EntryKey = Guid.NewGuid().ToString("N"),
TrackName = name,
TrackNumber = trackNumber,
Release = release,
};
private async Task SeedAsync(params TrackEntity[] tracks)
{
_context.Tracks.AddRange(tracks);
await _context.SaveChangesAsync();
}
// The album page's query: filter to one release, order by the explicit ordinal.
private static PagingParameters<TrackEntity> OrderedByTrackNumber()
=> new() { Page = 1, PageSize = 100, OrderBy = t => t.TrackNumber, IsDescending = false };
// The release-id filter narrows to that album only — a sibling release's tracks never leak in.
[Test]
public async Task ReleaseIdFilter_ReturnsOnlyThatReleasesTracks()
{
var albumA = Release("Album A", "Artist");
var albumB = Release("Album B", "Artist");
await SeedAsync(
Track("A-one", 1, albumA),
Track("A-two", 2, albumA),
Track("B-one", 1, albumB),
Track("Loose", 1));
var repo = CreateRepository();
var result = await repo.GetPagedFilteredAsync(
OrderedByTrackNumber(), new TrackFilter { ReleaseId = albumA.Id });
Assert.That(result.TotalCount, Is.EqualTo(2));
Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "A-one", "A-two" }));
}
// The ordering is by the explicit ordinal, not insertion order: tracks seeded out of order
// come back ascending by TrackNumber. This is the guarantee /cuts/{id} relies on for its rows.
[Test]
public async Task OrderByTrackNumber_SortsByExplicitOrdinalNotInsertionOrder()
{
var album = Release("Album", "Artist");
// Insert deliberately scrambled relative to the intended track order.
await SeedAsync(
Track("Third", 3, album),
Track("First", 1, album),
Track("Second", 2, album));
var repo = CreateRepository();
var result = await repo.GetPagedFilteredAsync(
OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id });
Assert.That(
result.Items.Select(t => t.TrackName).ToList(),
Is.EqualTo(new[] { "First", "Second", "Third" }),
"rows must order by the explicit TrackNumber ordinal, not the order they were inserted");
Assert.That(
result.Items.Select(t => t.TrackNumber).ToList(),
Is.EqualTo(new[] { 1, 2, 3 }));
}
// The DTO the page renders carries the ordinal — TrackConverter projects TrackNumber onto
// TrackDto, so the row's number label and the saved order survive the entity -> DTO mapping.
[Test]
public async Task TrackConverter_ProjectsTrackNumberOntoDto()
{
var album = Release("Album", "Artist");
await SeedAsync(
Track("First", 1, album),
Track("Second", 2, album));
var repo = CreateRepository();
var result = await repo.GetPagedFilteredAsync(
OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id });
var dtos = result.Items.Select(TrackConverter.Convert).ToList();
Assert.That(dtos.Select(d => d.TrackNumber).ToList(), Is.EqualTo(new[] { 1, 2 }));
Assert.That(dtos.Select(d => d.TrackName).ToList(), Is.EqualTo(new[] { "First", "Second" }));
}
// An album with no streamable tracks yields an empty page (no rows, no error) — the page header
// still renders; the track list is simply empty.
[Test]
public async Task ReleaseIdFilter_WithNoTracks_ReturnsEmptyPage()
{
var empty = Release("Empty Album", "Artist");
var other = Release("Other", "Artist");
await SeedAsync(Track("Other-one", 1, other));
// Persist the empty release with no tracks linked to it.
_context.Releases.Add(empty);
await _context.SaveChangesAsync();
var repo = CreateRepository();
var result = await repo.GetPagedFilteredAsync(
OrderedByTrackNumber(), new TrackFilter { ReleaseId = empty.Id });
Assert.That(result.TotalCount, Is.EqualTo(0));
Assert.That(result.Items, Is.Empty);
}
}