Files
ersatztv/ErsatzTV.Tests/Controllers/AutoTuneRequestsTests.cs
T
timothyandClaude Opus 4.8 8f61ad6530 feat(385): per-channel overrides in auto-tune bulk-create
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>
2026-07-17 23:33:10 +02:00

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