From 8912686a478afc75b3acb6239220530fad1d60cb Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 2 Jul 2026 21:27:40 +0200 Subject: [PATCH 1/3] wip: partial implementation salvaged from interrupted workflow run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Untrusted draft — no build/test had run yet. Review before building on it. Co-Authored-By: Claude Fable 5 --- ErsatzTV.Application/Filler/Mapper.cs | 6 +- .../Queries/GetAllFillerPresetsForApi.cs | 5 ++ .../GetAllFillerPresetsForApiHandler.cs | 22 +++++++ .../Queries/GetAllGraphicsElementsForApi.cs | 5 ++ .../GetAllGraphicsElementsForApiHandler.cs | 27 ++++++++ ErsatzTV.Application/Watermarks/Mapper.cs | 4 ++ .../Queries/GetAllWatermarksForApi.cs | 5 ++ .../Queries/GetAllWatermarksForApiHandler.cs | 22 +++++++ .../Api/Filler/FillerPresetResponseModel.cs | 3 + .../Graphics/GraphicsElementResponseModel.cs | 3 + .../Api/Watermarks/WatermarkResponseModel.cs | 3 + .../FillerPresetControllerTests.cs | 63 +++++++++++++++++++ .../GraphicsElementControllerTests.cs | 63 +++++++++++++++++++ .../Controllers/WatermarkControllerTests.cs | 63 +++++++++++++++++++ .../Controllers/Api/FillerPresetController.cs | 19 ++++++ .../Api/GraphicsElementController.cs | 19 ++++++ .../Controllers/Api/WatermarkController.cs | 19 ++++++ 17 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApi.cs create mode 100644 ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApiHandler.cs create mode 100644 ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApi.cs create mode 100644 ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApiHandler.cs create mode 100644 ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApi.cs create mode 100644 ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApiHandler.cs create mode 100644 ErsatzTV.Core/Api/Filler/FillerPresetResponseModel.cs create mode 100644 ErsatzTV.Core/Api/Graphics/GraphicsElementResponseModel.cs create mode 100644 ErsatzTV.Core/Api/Watermarks/WatermarkResponseModel.cs create mode 100644 ErsatzTV.Tests/Controllers/FillerPresetControllerTests.cs create mode 100644 ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs create mode 100644 ErsatzTV.Tests/Controllers/WatermarkControllerTests.cs create mode 100644 ErsatzTV/Controllers/Api/FillerPresetController.cs create mode 100644 ErsatzTV/Controllers/Api/GraphicsElementController.cs create mode 100644 ErsatzTV/Controllers/Api/WatermarkController.cs diff --git a/ErsatzTV.Application/Filler/Mapper.cs b/ErsatzTV.Application/Filler/Mapper.cs index 6a4ad35eb..1b72b5200 100644 --- a/ErsatzTV.Application/Filler/Mapper.cs +++ b/ErsatzTV.Application/Filler/Mapper.cs @@ -1,9 +1,13 @@ -using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Api.Filler; +using ErsatzTV.Core.Domain.Filler; namespace ErsatzTV.Application.Filler; internal static class Mapper { + internal static FillerPresetResponseModel ProjectToResponseModel(FillerPreset fillerPreset) => + new(fillerPreset.Id, fillerPreset.Name); + internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) => new( fillerPreset.Id, diff --git a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApi.cs b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApi.cs new file mode 100644 index 000000000..3252aae1f --- /dev/null +++ b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.Filler; + +namespace ErsatzTV.Application.Filler; + +public record GetAllFillerPresetsForApi : IRequest>; diff --git a/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApiHandler.cs b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApiHandler.cs new file mode 100644 index 000000000..b0c6edaf6 --- /dev/null +++ b/ErsatzTV.Application/Filler/Queries/GetAllFillerPresetsForApiHandler.cs @@ -0,0 +1,22 @@ +using ErsatzTV.Core.Api.Filler; +using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.Filler.Mapper; + +namespace ErsatzTV.Application.Filler; + +public class GetAllFillerPresetsForApiHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + GetAllFillerPresetsForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + List fillerPresets = await dbContext.FillerPresets + .AsNoTracking() + .ToListAsync(cancellationToken); + return fillerPresets.Map(ProjectToResponseModel).ToList(); + } +} diff --git a/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApi.cs b/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApi.cs new file mode 100644 index 000000000..534ecf73f --- /dev/null +++ b/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.Graphics; + +namespace ErsatzTV.Application.Graphics; + +public record GetAllGraphicsElementsForApi : IRequest>; diff --git a/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApiHandler.cs b/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApiHandler.cs new file mode 100644 index 000000000..c6131d4ab --- /dev/null +++ b/ErsatzTV.Application/Graphics/Queries/GetAllGraphicsElementsForApiHandler.cs @@ -0,0 +1,27 @@ +using ErsatzTV.Core.Api.Graphics; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.Graphics.Mapper; + +namespace ErsatzTV.Application.Graphics; + +public class GetAllGraphicsElementsForApiHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + GetAllGraphicsElementsForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + List graphicsElements = await dbContext.GraphicsElements + .AsNoTracking() + .ToListAsync(cancellationToken); + return graphicsElements + .Map(ProjectToViewModel) + .OrderBy(e => e.Name == e.FileName) + .ThenBy(e => e.Name) + .Select(vm => new GraphicsElementResponseModel(vm.Id, vm.Name)) + .ToList(); + } +} diff --git a/ErsatzTV.Application/Watermarks/Mapper.cs b/ErsatzTV.Application/Watermarks/Mapper.cs index 537932caa..4869d2ddc 100644 --- a/ErsatzTV.Application/Watermarks/Mapper.cs +++ b/ErsatzTV.Application/Watermarks/Mapper.cs @@ -1,10 +1,14 @@ using ErsatzTV.Application.Artworks; +using ErsatzTV.Core.Api.Watermarks; using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.Watermarks; internal static class Mapper { + internal static WatermarkResponseModel ProjectToResponseModel(ChannelWatermark watermark) => + new(watermark.Id, watermark.Name); + public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) => new( watermark.Id, diff --git a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApi.cs b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApi.cs new file mode 100644 index 000000000..acf3b86a9 --- /dev/null +++ b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.Watermarks; + +namespace ErsatzTV.Application.Watermarks; + +public record GetAllWatermarksForApi : IRequest>; diff --git a/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApiHandler.cs b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApiHandler.cs new file mode 100644 index 000000000..758fe6349 --- /dev/null +++ b/ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksForApiHandler.cs @@ -0,0 +1,22 @@ +using ErsatzTV.Core.Api.Watermarks; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.Watermarks.Mapper; + +namespace ErsatzTV.Application.Watermarks; + +public class GetAllWatermarksForApiHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + GetAllWatermarksForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + List watermarks = await dbContext.ChannelWatermarks + .AsNoTracking() + .ToListAsync(cancellationToken); + return watermarks.Map(ProjectToResponseModel).ToList(); + } +} diff --git a/ErsatzTV.Core/Api/Filler/FillerPresetResponseModel.cs b/ErsatzTV.Core/Api/Filler/FillerPresetResponseModel.cs new file mode 100644 index 000000000..a237a0b3b --- /dev/null +++ b/ErsatzTV.Core/Api/Filler/FillerPresetResponseModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Api.Filler; + +public record FillerPresetResponseModel(int Id, string Name); diff --git a/ErsatzTV.Core/Api/Graphics/GraphicsElementResponseModel.cs b/ErsatzTV.Core/Api/Graphics/GraphicsElementResponseModel.cs new file mode 100644 index 000000000..fef16a496 --- /dev/null +++ b/ErsatzTV.Core/Api/Graphics/GraphicsElementResponseModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Api.Graphics; + +public record GraphicsElementResponseModel(int Id, string Name); diff --git a/ErsatzTV.Core/Api/Watermarks/WatermarkResponseModel.cs b/ErsatzTV.Core/Api/Watermarks/WatermarkResponseModel.cs new file mode 100644 index 000000000..656e5b550 --- /dev/null +++ b/ErsatzTV.Core/Api/Watermarks/WatermarkResponseModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Api.Watermarks; + +public record WatermarkResponseModel(int Id, string Name); diff --git a/ErsatzTV.Tests/Controllers/FillerPresetControllerTests.cs b/ErsatzTV.Tests/Controllers/FillerPresetControllerTests.cs new file mode 100644 index 000000000..5747efc03 --- /dev/null +++ b/ErsatzTV.Tests/Controllers/FillerPresetControllerTests.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using ErsatzTV.Application.Filler; +using ErsatzTV.Controllers.Api; +using ErsatzTV.Core.Api.Filler; +using MediatR; +using Microsoft.AspNetCore.Mvc.Routing; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Controllers; + +[TestFixture] +public class FillerPresetControllerTests +{ + private FillerPresetController _controller = null!; + private IMediator _mediator = null!; + + [SetUp] + public void SetUp() + { + _mediator = Substitute.For(); + _controller = new FillerPresetController(_mediator); + } + + [Test] + public void Controller_Should_Expose_Idiomatic_Rest_Route() + { + MethodInfo action = typeof(FillerPresetController).GetMethod(nameof(FillerPresetController.GetAll)) + ?? throw new AssertionException("Missing action GetAll"); + + HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); + attribute.HttpMethods.ShouldContain("GET"); + attribute.Template.ShouldBe("/api/filler-presets"); + } + + [Test] + public async Task GetAll_Should_Return_FillerPresets() + { + List models = + [ + new FillerPresetResponseModel(1, "Intro"), + new FillerPresetResponseModel(2, "Outro") + ]; + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(models); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBe(models); + } + + [Test] + public async Task GetAll_Should_Return_Empty_List_When_None_Exist() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([]); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBeEmpty(); + } +} diff --git a/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs b/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs new file mode 100644 index 000000000..8a366e9d4 --- /dev/null +++ b/ErsatzTV.Tests/Controllers/GraphicsElementControllerTests.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using ErsatzTV.Application.Graphics; +using ErsatzTV.Controllers.Api; +using ErsatzTV.Core.Api.Graphics; +using MediatR; +using Microsoft.AspNetCore.Mvc.Routing; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Controllers; + +[TestFixture] +public class GraphicsElementControllerTests +{ + private GraphicsElementController _controller = null!; + private IMediator _mediator = null!; + + [SetUp] + public void SetUp() + { + _mediator = Substitute.For(); + _controller = new GraphicsElementController(_mediator); + } + + [Test] + public void Controller_Should_Expose_Idiomatic_Rest_Route() + { + MethodInfo action = typeof(GraphicsElementController).GetMethod(nameof(GraphicsElementController.GetAll)) + ?? throw new AssertionException("Missing action GetAll"); + + HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); + attribute.HttpMethods.ShouldContain("GET"); + attribute.Template.ShouldBe("/api/graphics-elements"); + } + + [Test] + public async Task GetAll_Should_Return_GraphicsElements() + { + List models = + [ + new GraphicsElementResponseModel(1, "Lower Third (lower-third.png)"), + new GraphicsElementResponseModel(2, "bug.png") + ]; + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(models); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBe(models); + } + + [Test] + public async Task GetAll_Should_Return_Empty_List_When_None_Exist() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([]); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBeEmpty(); + } +} diff --git a/ErsatzTV.Tests/Controllers/WatermarkControllerTests.cs b/ErsatzTV.Tests/Controllers/WatermarkControllerTests.cs new file mode 100644 index 000000000..1a2bafa9b --- /dev/null +++ b/ErsatzTV.Tests/Controllers/WatermarkControllerTests.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using ErsatzTV.Application.Watermarks; +using ErsatzTV.Controllers.Api; +using ErsatzTV.Core.Api.Watermarks; +using MediatR; +using Microsoft.AspNetCore.Mvc.Routing; +using NSubstitute; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Controllers; + +[TestFixture] +public class WatermarkControllerTests +{ + private WatermarkController _controller = null!; + private IMediator _mediator = null!; + + [SetUp] + public void SetUp() + { + _mediator = Substitute.For(); + _controller = new WatermarkController(_mediator); + } + + [Test] + public void Controller_Should_Expose_Idiomatic_Rest_Route() + { + MethodInfo action = typeof(WatermarkController).GetMethod(nameof(WatermarkController.GetAll)) + ?? throw new AssertionException("Missing action GetAll"); + + HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); + attribute.HttpMethods.ShouldContain("GET"); + attribute.Template.ShouldBe("/api/watermarks"); + } + + [Test] + public async Task GetAll_Should_Return_Watermarks() + { + List models = + [ + new WatermarkResponseModel(1, "Corner Logo"), + new WatermarkResponseModel(2, "Ticker") + ]; + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(models); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBe(models); + } + + [Test] + public async Task GetAll_Should_Return_Empty_List_When_None_Exist() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([]); + + List result = await _controller.GetAll(CancellationToken.None); + + result.ShouldBeEmpty(); + } +} diff --git a/ErsatzTV/Controllers/Api/FillerPresetController.cs b/ErsatzTV/Controllers/Api/FillerPresetController.cs new file mode 100644 index 000000000..77e0e2e0a --- /dev/null +++ b/ErsatzTV/Controllers/Api/FillerPresetController.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Application.Filler; +using ErsatzTV.Core.Api.Filler; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +public class FillerPresetController(IMediator mediator) : ControllerBase +{ + [HttpGet("/api/filler-presets")] + [Tags("Filler Presets")] + [EndpointSummary("Get all filler presets")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task> GetAll(CancellationToken cancellationToken) => + await mediator.Send(new GetAllFillerPresetsForApi(), cancellationToken); +} diff --git a/ErsatzTV/Controllers/Api/GraphicsElementController.cs b/ErsatzTV/Controllers/Api/GraphicsElementController.cs new file mode 100644 index 000000000..f0b5ef5be --- /dev/null +++ b/ErsatzTV/Controllers/Api/GraphicsElementController.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Application.Graphics; +using ErsatzTV.Core.Api.Graphics; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +public class GraphicsElementController(IMediator mediator) : ControllerBase +{ + [HttpGet("/api/graphics-elements")] + [Tags("Graphics Elements")] + [EndpointSummary("Get all graphics elements")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task> GetAll(CancellationToken cancellationToken) => + await mediator.Send(new GetAllGraphicsElementsForApi(), cancellationToken); +} diff --git a/ErsatzTV/Controllers/Api/WatermarkController.cs b/ErsatzTV/Controllers/Api/WatermarkController.cs new file mode 100644 index 000000000..2ec628fb7 --- /dev/null +++ b/ErsatzTV/Controllers/Api/WatermarkController.cs @@ -0,0 +1,19 @@ +using ErsatzTV.Application.Watermarks; +using ErsatzTV.Core.Api.Watermarks; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +public class WatermarkController(IMediator mediator) : ControllerBase +{ + [HttpGet("/api/watermarks")] + [Tags("Watermarks")] + [EndpointSummary("Get all watermarks")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task> GetAll(CancellationToken cancellationToken) => + await mediator.Send(new GetAllWatermarksForApi(), cancellationToken); +} -- 2.47.3 From 0eaedb9cf60e9aab315aaca5039f0de523c8b07a Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 2 Jul 2026 22:06:19 +0200 Subject: [PATCH 2/3] feat(api): add route names and handler tests for picker list endpoints (#105) The WIP GET /api/filler-presets, /api/watermarks, and /api/graphics-elements endpoints omitted the Name= route attribute used by the other API GET actions (e.g. FFmpegProfileController). Add Name = "GetFillerPresets" / "GetWatermarks" / "GetGraphicsElements" for consistency. Also add handler tests (FillerPresetHandlerTests, WatermarkHandlerTests, GraphicsElementHandlerTests) covering the GetAll*ForApi handlers using the InMemoryTvContext harness: multi-row projection to (Id, Name) DTOs, the empty-DB case, and the graphics-element ordering rule (named elements sort before elements whose projected Name equals FileName). --- .../Filler/FillerPresetHandlerTests.cs | 63 +++++++++++++++ .../Graphics/GraphicsElementHandlerTests.cs | 77 +++++++++++++++++++ .../Watermarks/WatermarkHandlerTests.cs | 71 +++++++++++++++++ .../Controllers/Api/FillerPresetController.cs | 2 +- .../Api/GraphicsElementController.cs | 2 +- .../Controllers/Api/WatermarkController.cs | 2 +- 6 files changed, 214 insertions(+), 3 deletions(-) create mode 100644 ErsatzTV.Tests/Application/Filler/FillerPresetHandlerTests.cs create mode 100644 ErsatzTV.Tests/Application/Graphics/GraphicsElementHandlerTests.cs create mode 100644 ErsatzTV.Tests/Application/Watermarks/WatermarkHandlerTests.cs diff --git a/ErsatzTV.Tests/Application/Filler/FillerPresetHandlerTests.cs b/ErsatzTV.Tests/Application/Filler/FillerPresetHandlerTests.cs new file mode 100644 index 000000000..6c810cffd --- /dev/null +++ b/ErsatzTV.Tests/Application/Filler/FillerPresetHandlerTests.cs @@ -0,0 +1,63 @@ +using ErsatzTV.Application.Filler; +using ErsatzTV.Core.Api.Filler; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Tests.Support; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Application.Filler; + +[TestFixture] +public class FillerPresetHandlerTests +{ + private InMemoryTvContext _db = null!; + + [SetUp] + public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync(); + + [TearDown] + public async Task TearDown() => await _db.DisposeAsync(); + + [Test] + public async Task GetAllFillerPresetsForApi_Should_Return_All_Presets() + { + await SeedPreset(1, "Intro"); + await SeedPreset(2, "Outro"); + + var handler = new GetAllFillerPresetsForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None); + + result.Count.ShouldBe(2); + result.ShouldContain(new FillerPresetResponseModel(1, "Intro")); + result.ShouldContain(new FillerPresetResponseModel(2, "Outro")); + } + + [Test] + public async Task GetAllFillerPresetsForApi_Should_Return_Empty_List_When_None_Exist() + { + var handler = new GetAllFillerPresetsForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllFillerPresetsForApi(), CancellationToken.None); + + result.ShouldBeEmpty(); + } + + private async Task SeedPreset(int id, string name) + { + await using TvContext context = _db.CreateContext(); + context.FillerPresets.Add(new FillerPreset + { + Id = id, + Name = name, + FillerKind = FillerKind.PreRoll, + FillerMode = FillerMode.Duration, + CollectionType = CollectionType.Collection + }); + await context.SaveChangesAsync(); + } +} diff --git a/ErsatzTV.Tests/Application/Graphics/GraphicsElementHandlerTests.cs b/ErsatzTV.Tests/Application/Graphics/GraphicsElementHandlerTests.cs new file mode 100644 index 000000000..d68fa1ca0 --- /dev/null +++ b/ErsatzTV.Tests/Application/Graphics/GraphicsElementHandlerTests.cs @@ -0,0 +1,77 @@ +using ErsatzTV.Application.Graphics; +using ErsatzTV.Core.Api.Graphics; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Tests.Support; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Application.Graphics; + +[TestFixture] +public class GraphicsElementHandlerTests +{ + private InMemoryTvContext _db = null!; + + [SetUp] + public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync(); + + [TearDown] + public async Task TearDown() => await _db.DisposeAsync(); + + [Test] + public async Task GetAllGraphicsElementsForApi_Should_Return_All_Elements() + { + await SeedElement(1, "watermark.png", GraphicsElementKind.Image, "Custom Watermark"); + await SeedElement(2, "clock.png", GraphicsElementKind.Image, string.Empty); + + var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None); + + result.Count.ShouldBe(2); + result.Select(e => e.Id).ShouldBe([1, 2]); + } + + [Test] + public async Task GetAllGraphicsElementsForApi_Should_Order_Named_Elements_Before_Unnamed() + { + // unnamed element's projected Name equals its FileName -> should sort after named elements + await SeedElement(1, "aaa.png", GraphicsElementKind.Image, string.Empty); + await SeedElement(2, "zzz.png", GraphicsElementKind.Image, "A Custom Name"); + + var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None); + + result.Count.ShouldBe(2); + result[0].Id.ShouldBe(2); + result[1].Id.ShouldBe(1); + } + + [Test] + public async Task GetAllGraphicsElementsForApi_Should_Return_Empty_List_When_None_Exist() + { + var handler = new GetAllGraphicsElementsForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllGraphicsElementsForApi(), CancellationToken.None); + + result.ShouldBeEmpty(); + } + + private async Task SeedElement(int id, string path, GraphicsElementKind kind, string name) + { + await using TvContext context = _db.CreateContext(); + context.GraphicsElements.Add(new GraphicsElement + { + Id = id, + Path = path, + Name = name, + Kind = kind + }); + await context.SaveChangesAsync(); + } +} diff --git a/ErsatzTV.Tests/Application/Watermarks/WatermarkHandlerTests.cs b/ErsatzTV.Tests/Application/Watermarks/WatermarkHandlerTests.cs new file mode 100644 index 000000000..173ce4798 --- /dev/null +++ b/ErsatzTV.Tests/Application/Watermarks/WatermarkHandlerTests.cs @@ -0,0 +1,71 @@ +using ErsatzTV.Application.Watermarks; +using ErsatzTV.Core.Api.Watermarks; +using ErsatzTV.Core.Domain; +using ErsatzTV.FFmpeg.State; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Tests.Support; +using NUnit.Framework; +using Shouldly; + +namespace ErsatzTV.Tests.Application.Watermarks; + +[TestFixture] +public class WatermarkHandlerTests +{ + private InMemoryTvContext _db = null!; + + [SetUp] + public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync(); + + [TearDown] + public async Task TearDown() => await _db.DisposeAsync(); + + [Test] + public async Task GetAllWatermarksForApi_Should_Return_All_Watermarks() + { + await SeedWatermark(1, "Bug"); + await SeedWatermark(2, "Logo"); + + var handler = new GetAllWatermarksForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllWatermarksForApi(), CancellationToken.None); + + result.Count.ShouldBe(2); + result.ShouldContain(new WatermarkResponseModel(1, "Bug")); + result.ShouldContain(new WatermarkResponseModel(2, "Logo")); + } + + [Test] + public async Task GetAllWatermarksForApi_Should_Return_Empty_List_When_None_Exist() + { + var handler = new GetAllWatermarksForApiHandler(_db.Factory); + + List result = + await handler.Handle(new GetAllWatermarksForApi(), CancellationToken.None); + + result.ShouldBeEmpty(); + } + + private async Task SeedWatermark(int id, string name) + { + await using TvContext context = _db.CreateContext(); + context.ChannelWatermarks.Add(new ChannelWatermark + { + Id = id, + Name = name, + Mode = ChannelWatermarkMode.Permanent, + ImageSource = ChannelWatermarkImageSource.Custom, + Image = "watermark.png", + Location = WatermarkLocation.BottomRight, + Size = WatermarkSize.Scaled, + WidthPercent = 10, + HorizontalMarginPercent = 2, + VerticalMarginPercent = 2, + FrequencyMinutes = 15, + DurationSeconds = 30, + Opacity = 100 + }); + await context.SaveChangesAsync(); + } +} diff --git a/ErsatzTV/Controllers/Api/FillerPresetController.cs b/ErsatzTV/Controllers/Api/FillerPresetController.cs index 77e0e2e0a..ba4d8de77 100644 --- a/ErsatzTV/Controllers/Api/FillerPresetController.cs +++ b/ErsatzTV/Controllers/Api/FillerPresetController.cs @@ -9,7 +9,7 @@ namespace ErsatzTV.Controllers.Api; [ApiController] public class FillerPresetController(IMediator mediator) : ControllerBase { - [HttpGet("/api/filler-presets")] + [HttpGet("/api/filler-presets", Name = "GetFillerPresets")] [Tags("Filler Presets")] [EndpointSummary("Get all filler presets")] [EndpointGroupName("general")] diff --git a/ErsatzTV/Controllers/Api/GraphicsElementController.cs b/ErsatzTV/Controllers/Api/GraphicsElementController.cs index f0b5ef5be..7196f1f14 100644 --- a/ErsatzTV/Controllers/Api/GraphicsElementController.cs +++ b/ErsatzTV/Controllers/Api/GraphicsElementController.cs @@ -9,7 +9,7 @@ namespace ErsatzTV.Controllers.Api; [ApiController] public class GraphicsElementController(IMediator mediator) : ControllerBase { - [HttpGet("/api/graphics-elements")] + [HttpGet("/api/graphics-elements", Name = "GetGraphicsElements")] [Tags("Graphics Elements")] [EndpointSummary("Get all graphics elements")] [EndpointGroupName("general")] diff --git a/ErsatzTV/Controllers/Api/WatermarkController.cs b/ErsatzTV/Controllers/Api/WatermarkController.cs index 2ec628fb7..4b2259a7f 100644 --- a/ErsatzTV/Controllers/Api/WatermarkController.cs +++ b/ErsatzTV/Controllers/Api/WatermarkController.cs @@ -9,7 +9,7 @@ namespace ErsatzTV.Controllers.Api; [ApiController] public class WatermarkController(IMediator mediator) : ControllerBase { - [HttpGet("/api/watermarks")] + [HttpGet("/api/watermarks", Name = "GetWatermarks")] [Tags("Watermarks")] [EndpointSummary("Get all watermarks")] [EndpointGroupName("general")] -- 2.47.3 From 446f50763a80d622b39e7acaf5dc1c1cf1873a4d Mon Sep 17 00:00:00 2001 From: Timothy Date: Thu, 2 Jul 2026 22:06:24 +0200 Subject: [PATCH 3/3] chore(openapi): regenerate v1.json for picker list endpoints (#105) Adds /api/filler-presets, /api/watermarks, and /api/graphics-elements to the published OpenAPI document via ./scripts/update-openapi.sh. --- ErsatzTV/wwwroot/openapi/v1.json | 186 +++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index e52fa2841..3df42d662 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -1353,6 +1353,86 @@ } } }, + "/api/filler-presets": { + "get": { + "tags": [ + "Filler Presets" + ], + "summary": "Get all filler presets", + "operationId": "GetFillerPresets", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillerPresetResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillerPresetResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FillerPresetResponseModel" + } + } + } + } + } + } + } + }, + "/api/graphics-elements": { + "get": { + "tags": [ + "Graphics Elements" + ], + "summary": "Get all graphics elements", + "operationId": "GetGraphicsElements", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GraphicsElementResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GraphicsElementResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GraphicsElementResponseModel" + } + } + } + } + } + } + } + }, "/api/libraries/{id}/scan": { "post": { "tags": [ @@ -2906,6 +2986,46 @@ } } } + }, + "/api/watermarks": { + "get": { + "tags": [ + "Watermarks" + ], + "summary": "Get all watermarks", + "operationId": "GetWatermarks", + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WatermarkResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WatermarkResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WatermarkResponseModel" + } + } + } + } + } + } + } } }, "components": { @@ -4017,6 +4137,25 @@ ], "type": "string" }, + "FillerPresetResponseModel": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + } + } + }, "FillerPresetViewModel": { "required": [ "id", @@ -4145,6 +4284,25 @@ ], "type": "string" }, + "GraphicsElementResponseModel": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + } + } + }, "GraphicsElementViewModel": { "required": [ "id", @@ -5660,6 +5818,25 @@ "WatermarkLocation": { "type": "integer" }, + "WatermarkResponseModel": { + "required": [ + "id", + "name" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": [ + "null", + "string" + ] + } + } + }, "WatermarkSize": { "type": "integer" }, @@ -5763,6 +5940,12 @@ { "name": "FFmpeg Profiles" }, + { + "name": "Filler Presets" + }, + { + "name": "Graphics Elements" + }, { "name": "Libraries" }, @@ -5786,6 +5969,9 @@ }, { "name": "Version" + }, + { + "name": "Watermarks" } ] } \ No newline at end of file -- 2.47.3