Auto-Tune DetailPanel backend (#385), additive half. The create request `AutoTunedChannelRequest` gains three optional per-channel fields, all backward-compatible (omit = PR1 behavior): - `templateId` — overrides the batch template per channel - `advanced` — reuses the manual Channel Builder's `CreateChannelFromLineupAdvancedOptionsRequest` verbatim (24-field override set, `advanced.X ?? template.X` stamp contract). Axis default fills `PlaybackOrder` only when the caller leaves it null. - `logo` — uploaded channel image, `Sanitized()` at the request boundary (#283 stored-XSS defense), forwarded to `CreateChannelFromLineup.Logo` Resolved per channel inside `CreateAutoTunedChannelsHandler.CreateOne`, so one channel's bad override still yields a per-channel Failed/Skipped without aborting the batch. Per-source rotation weights + query corrections are split out to #425 (they need a MultiCollection-of-per-source-SmartCollections redesign — #70's WeightedShuffle reads weights only off MultiCollection join rows, and an auto-tuned channel is one SmartCollection). Bug-initials/colour generated logo also deferred (needs persisted Channel state + FFmpeg-pipeline wiring). Tests: handler override-threading (per-channel wins, axis default preserved, no-override baseline) + request `ToCommand()` logo sanitization. OpenAPI trio regenerated. Docs: decisions.md, api-conventions.md, domain-model.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
53 lines
1.9 KiB
C#
53 lines
1.9 KiB
C#
using ErsatzTV.Application.Artworks;
|
|
using ErsatzTV.Application.Channels;
|
|
using ErsatzTV.Controllers.Api.Requests;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Scheduling;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Controllers;
|
|
|
|
[TestFixture]
|
|
public class AutoTuneRequestsTests
|
|
{
|
|
[Test]
|
|
public void ToCommand_Maps_Overrides_And_Sanitizes_A_Bad_Logo_Content_Type()
|
|
{
|
|
var request = new AutoTunedChannelRequest(
|
|
AutoTuneAxis.TvGenre,
|
|
"Comedy",
|
|
"Comedy",
|
|
"500",
|
|
TemplateId: 11,
|
|
// A hostile content type must not survive to the command (stored-XSS defense, #283) —
|
|
// the request mirrors CreateChannelFromLineupRequest's Sanitized() call.
|
|
Logo: new ArtworkContentTypeModel("iptv/logos/comedy.png", "text/html"),
|
|
Advanced: new CreateChannelFromLineupAdvancedOptionsRequest(
|
|
PlaybackOrder: PlaybackOrder.Chronological,
|
|
FFmpegProfileId: 42));
|
|
|
|
AutoTuneChannelSelection command = request.ToCommand();
|
|
|
|
command.TemplateId.ShouldBe(11);
|
|
command.Logo.Path.ShouldBe("iptv/logos/comedy.png");
|
|
command.Logo.ContentType.ShouldBe(string.Empty); // blanked
|
|
command.Advanced.ShouldNotBeNull();
|
|
command.Advanced.PlaybackOrder.ShouldBe(PlaybackOrder.Chronological);
|
|
command.Advanced.FFmpegProfileId.ShouldBe(42);
|
|
}
|
|
|
|
[Test]
|
|
public void ToCommand_Without_Overrides_Leaves_Command_Fields_At_Defaults()
|
|
{
|
|
var request = new AutoTunedChannelRequest(AutoTuneAxis.TvShow, "The Office", "The Office", "500");
|
|
|
|
AutoTuneChannelSelection command = request.ToCommand();
|
|
|
|
command.TemplateId.ShouldBeNull();
|
|
command.Advanced.ShouldBeNull();
|
|
// An omitted logo maps to None (empty path), not null.
|
|
command.Logo.ShouldBe(ArtworkContentTypeModel.None);
|
|
}
|
|
}
|