Cold review of 572737a29. Findings taken; two are corrections to my own claims.
CORRECTION: "a regression this branch INTRODUCED" was wrong, and I verified the
reviewer's counter-claim against origin/main before accepting it. That handler
already included SongMetadata AND already routed Song there, so
GET /api/v1/playlists/{id}/items was ALREADY a live 500 for a null-Artists song.
This branch only made the same throw reachable on a second path. The record said
so twice; both are fixed, because a wrong explanation outlives a wrong line.
SWEEP: fixing one site left the mirror standing -- Playouts/Mapper.GetDisplayTitle
had the identical unguarded join on a path that also eager-loads SongMetadata, so
it too was live, feeding the playout guide, troubleshooting, media-item info and
channel states. Guarded, with a unit test; reverting it reproduces
ArgumentNullException. LibraryBrowseItemMapper already wrote `Artists ?? []`, so
the nullability was known in-tree and these sites were simply unswept. Filed #691
for the remaining SongVideoGenerator dereferences on the playback path.
Also: documented that the shared matrix is the RERUN predicate used as a superset
for playlists (the playlist write path rejects RemoteStream today); noted the
third, inert consumer ReplacePlaylistItemsHandler; added the new mechanisms to the
record's `mechanics:` field; and trimmed the record under the 60-line prose
ceiling -- it was tipping the corpus p90 above the ceiling and reddening the
calibration test in scripts/tests, which passes again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
213 lines
9.1 KiB
C#
213 lines
9.1 KiB
C#
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using NUnit.Framework;
|
|
|
|
namespace ErsatzTV.Tests.Support;
|
|
|
|
/// <summary>
|
|
/// One selection-type matrix, shared by every fixture that exercises a tagged-union selection
|
|
/// (rerun collections and playlist items). Both consumers of
|
|
/// <c>MediaCollections.Mapper.ProjectMediaItemToViewModel</c> are proved against the SAME data, so
|
|
/// widening the shared switch cannot be discharged for the second consumer by inspection alone —
|
|
/// which is the method that produced #671 in the first place.
|
|
/// </summary>
|
|
internal static class SelectionSeedData
|
|
{
|
|
public const int SelectedId = 42;
|
|
|
|
/// <summary>
|
|
/// Derived from production rather than hand-listed, so a newly-supported type joins the matrix
|
|
/// automatically and trips the <c>default:</c> arms below until someone teaches them about it.
|
|
/// Note this is the RERUN-COLLECTION predicate, used for playlist items as a deliberate
|
|
/// SUPERSET: <c>ReplacePlaylistItemsHandler.CollectionTypeMustBeValid</c> has no
|
|
/// <c>RemoteStream</c> case, so a RemoteStream playlist item cannot be created through the write
|
|
/// API today and the playlist fixture seeds that row directly. Covering it is forward-looking,
|
|
/// not a claim that the two sets are equivalent — split this if they ever legitimately diverge.
|
|
/// </summary>
|
|
public static IEnumerable<CollectionType> SupportedSelectionTypes =>
|
|
Enum.GetValues<CollectionType>().Where(RerunCollectionRequestMapping.IsSupportedSelectionType);
|
|
|
|
/// <summary>
|
|
/// The exact projected name per type. Pinning the whole string — rather than merely asserting
|
|
/// "not a placeholder" — is what makes a missing NESTED include leg visible: dropping
|
|
/// Episode → Season → Show still yields the placeholder-free "s??e04 - Selected episode", and
|
|
/// dropping MusicVideo → Artist still yields "Selected music video". Both would sail past a
|
|
/// looser assertion while having lost real data.
|
|
/// </summary>
|
|
public static string ExpectedName(CollectionType collectionType) =>
|
|
collectionType switch
|
|
{
|
|
CollectionType.Collection => "Selected collection",
|
|
CollectionType.MultiCollection => "Selected multi collection",
|
|
CollectionType.SmartCollection => "Selected smart collection",
|
|
CollectionType.TelevisionShow => "Selected show (2020)",
|
|
CollectionType.TelevisionSeason => "Parent show (2020) - Season 3",
|
|
CollectionType.Artist => "Selected artist",
|
|
CollectionType.Movie => "Selected movie (2019)",
|
|
CollectionType.Episode => "Episode's show - s02e04 - Selected episode",
|
|
CollectionType.MusicVideo => "Video's artist - Selected music video",
|
|
CollectionType.OtherVideo => "Selected other video",
|
|
CollectionType.Song => "Song artist - Selected song",
|
|
CollectionType.Image => "Selected image",
|
|
CollectionType.RemoteStream => "Selected remote stream",
|
|
_ => throw new AssertionException($"No expected name pinned for {collectionType}")
|
|
};
|
|
|
|
public static async Task SeedSelection(TvContext context, CollectionType collectionType)
|
|
{
|
|
switch (collectionType)
|
|
{
|
|
case CollectionType.Collection:
|
|
context.Collections.Add(new Collection
|
|
{
|
|
Id = SelectedId,
|
|
Name = "Selected collection",
|
|
MediaItems = []
|
|
});
|
|
break;
|
|
case CollectionType.MultiCollection:
|
|
context.MultiCollections.Add(new MultiCollection
|
|
{
|
|
Id = SelectedId,
|
|
Name = "Selected multi collection"
|
|
});
|
|
break;
|
|
case CollectionType.SmartCollection:
|
|
context.SmartCollections.Add(new SmartCollection
|
|
{
|
|
Id = SelectedId,
|
|
Name = "Selected smart collection",
|
|
Query = "tag:family"
|
|
});
|
|
break;
|
|
case CollectionType.TelevisionShow:
|
|
context.Shows.Add(new Show
|
|
{
|
|
Id = SelectedId,
|
|
ShowMetadata = [new ShowMetadata { Title = "Selected show", Year = 2020 }]
|
|
});
|
|
break;
|
|
case CollectionType.TelevisionSeason:
|
|
context.Seasons.Add(new Season
|
|
{
|
|
Id = SelectedId,
|
|
SeasonNumber = 3,
|
|
Show = new Show
|
|
{
|
|
Id = 900,
|
|
ShowMetadata = [new ShowMetadata { Title = "Parent show", Year = 2020 }]
|
|
}
|
|
});
|
|
break;
|
|
case CollectionType.Artist:
|
|
context.Artists.Add(new Artist
|
|
{
|
|
Id = SelectedId,
|
|
ArtistMetadata = [new ArtistMetadata { Title = "Selected artist" }]
|
|
});
|
|
break;
|
|
case CollectionType.Movie:
|
|
context.Movies.Add(new Movie
|
|
{
|
|
Id = SelectedId,
|
|
MovieMetadata = [new MovieMetadata { Title = "Selected movie", Year = 2019 }]
|
|
});
|
|
break;
|
|
case CollectionType.Episode:
|
|
context.Episodes.Add(new Episode
|
|
{
|
|
Id = SelectedId,
|
|
EpisodeMetadata = [new EpisodeMetadata { Title = "Selected episode", EpisodeNumber = 4 }],
|
|
Season = new Season
|
|
{
|
|
Id = 901,
|
|
SeasonNumber = 2,
|
|
Show = new Show
|
|
{
|
|
Id = 902,
|
|
ShowMetadata = [new ShowMetadata { Title = "Episode's show", Year = 2018 }]
|
|
}
|
|
}
|
|
});
|
|
break;
|
|
case CollectionType.MusicVideo:
|
|
context.MusicVideos.Add(new MusicVideo
|
|
{
|
|
Id = SelectedId,
|
|
MusicVideoMetadata = [new MusicVideoMetadata { Title = "Selected music video" }],
|
|
Artist = new Artist
|
|
{
|
|
Id = 903,
|
|
ArtistMetadata = [new ArtistMetadata { Title = "Video's artist" }]
|
|
}
|
|
});
|
|
break;
|
|
case CollectionType.OtherVideo:
|
|
context.OtherVideos.Add(new OtherVideo
|
|
{
|
|
Id = SelectedId,
|
|
OtherVideoMetadata = [new OtherVideoMetadata { Title = "Selected other video" }]
|
|
});
|
|
break;
|
|
case CollectionType.Song:
|
|
context.Songs.Add(new Song
|
|
{
|
|
Id = SelectedId,
|
|
SongMetadata =
|
|
[new SongMetadata { Title = "Selected song", Artists = ["Song artist"] }]
|
|
});
|
|
break;
|
|
case CollectionType.Image:
|
|
context.Images.Add(new Image
|
|
{
|
|
Id = SelectedId,
|
|
ImageMetadata = [new ImageMetadata { Title = "Selected image" }]
|
|
});
|
|
break;
|
|
case CollectionType.RemoteStream:
|
|
context.RemoteStreams.Add(new RemoteStream
|
|
{
|
|
Id = SelectedId,
|
|
Url = "http://example.invalid/stream",
|
|
RemoteStreamMetadata = [new RemoteStreamMetadata { Title = "Selected remote stream" }]
|
|
});
|
|
break;
|
|
default:
|
|
throw new AssertionException(
|
|
$"{collectionType} is a supported selection type but this suite does not know how " +
|
|
"to seed it — teach SeedSelection about it rather than narrowing the matrix.");
|
|
}
|
|
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assigns the one foreign key the tagged union uses for this type. Shared so the rerun and
|
|
/// playlist fixtures cannot disagree about which slot a type occupies.
|
|
/// </summary>
|
|
public static void ApplySelection(
|
|
CollectionType collectionType,
|
|
Action<int> setCollectionId,
|
|
Action<int> setMultiCollectionId,
|
|
Action<int> setSmartCollectionId,
|
|
Action<int> setMediaItemId)
|
|
{
|
|
switch (collectionType)
|
|
{
|
|
case CollectionType.Collection:
|
|
setCollectionId(SelectedId);
|
|
break;
|
|
case CollectionType.MultiCollection:
|
|
setMultiCollectionId(SelectedId);
|
|
break;
|
|
case CollectionType.SmartCollection:
|
|
setSmartCollectionId(SelectedId);
|
|
break;
|
|
default:
|
|
setMediaItemId(SelectedId);
|
|
break;
|
|
}
|
|
}
|
|
}
|