using System.Reflection; using ErsatzTV.Application.Libraries; using ErsatzTV.Controllers.Api; using ErsatzTV.Core.Api.Libraries; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Tests.Controllers; [TestFixture] public class LibrariesControllerTests { private LibrariesController _controller = null!; private IMediator _mediator = null!; private ITelevisionRepository _televisionRepository = null!; [SetUp] public void SetUp() { _mediator = Substitute.For(); _televisionRepository = Substitute.For(); _controller = new LibrariesController(_televisionRepository, _mediator); } [Test] public void ScanStatus_Should_Expose_Idiomatic_Rest_Route() { MethodInfo action = typeof(LibrariesController).GetMethod(nameof(LibrariesController.GetScanStatus)) ?? throw new AssertionException("Missing action GetScanStatus"); HttpMethodAttribute attribute = action.GetCustomAttributes(inherit: true).Single(); attribute.HttpMethods.ShouldContain("GET"); attribute.Template.ShouldBe("/api/v1/libraries/scan-status"); attribute.Name.ShouldBe("GetLibraryScanStatus"); } [Test] public async Task GetScanStatus_Should_Return_Results_From_Mediator() { var expected = new List { new(1, 42.5m), new(2, 99m) }; _mediator.Send(Arg.Any(), Arg.Any()) .Returns(expected); List result = await _controller.GetScanStatus(CancellationToken.None); result.ShouldBe(expected); } [Test] public async Task ScanShow_Should_Return_NotFoundProblem_When_Show_Not_In_Library() { _televisionRepository.GetShowTitle(3, 999).Returns(Option.None); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(999)); var notFound = result.ShouldBeOfType(); notFound.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status404NotFound); await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); } [Test] public async Task ScanShow_Should_Return_202_And_Queue_Scan_By_Show_Id_When_Show_Belongs_To_Library() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.Queued); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42, DeepScan: true)); result.ShouldBeOfType(); await _mediator.Received(1).Send( Arg.Is(r => r.LibraryId == 3 && r.ShowId == 42 && r.ShowTitle == "The Office" && r.DeepScan), Arg.Any()); } [Test] public async Task ScanShow_Should_Return_409_When_AlreadyScanning() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.AlreadyScanning); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42)); var conflict = result.ShouldBeOfType(); conflict.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status409Conflict); } [Test] public async Task ScanShow_Should_Return_422_When_SyncDisabled() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.SyncDisabled); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42)); var unprocessable = result.ShouldBeOfType(); unprocessable.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status422UnprocessableEntity); } [Test] public async Task ScanShow_Should_Return_422_When_Unsupported() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.Unsupported); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42)); var unprocessable = result.ShouldBeOfType(); unprocessable.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status422UnprocessableEntity); } [Test] public async Task ScanShow_Should_Return_422_When_ScanFailed() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.ScanFailed); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42)); var unprocessable = result.ShouldBeOfType(); unprocessable.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status422UnprocessableEntity); } [Test] public async Task ScanShow_Should_Return_404_When_Handler_Reports_NotFound() { _televisionRepository.GetShowTitle(3, 42).Returns(Option.Some("The Office")); _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueShowScanResult.NotFound); IActionResult result = await _controller.ScanShow(3, new ScanShowRequest(42)); var notFound = result.ShouldBeOfType(); notFound.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status404NotFound); } [Test] public async Task ScanLibrary_Should_Return_202_When_Queued() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueLibraryScanResult.Queued); IActionResult result = await _controller.ScanLibrary(7, cancellationToken: CancellationToken.None); result.ShouldBeOfType(); await _mediator.Received(1).Send( Arg.Is(r => r.LibraryId == 7 && !r.DeepScan), Arg.Any()); } [Test] public async Task ScanLibrary_Should_Pass_DeepScan_When_Deep_Query_True() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueLibraryScanResult.Queued); IActionResult result = await _controller.ScanLibrary(7, deep: true, CancellationToken.None); result.ShouldBeOfType(); await _mediator.Received(1).Send( Arg.Is(r => r.LibraryId == 7 && r.DeepScan), Arg.Any()); } [Test] public async Task ScanLibrary_Should_Return_404_When_NotFound() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueLibraryScanResult.NotFound); IActionResult result = await _controller.ScanLibrary(7, cancellationToken: CancellationToken.None); var notFound = result.ShouldBeOfType(); notFound.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status404NotFound); } [Test] public async Task ScanLibrary_Should_Return_409_When_AlreadyScanning() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueLibraryScanResult.AlreadyScanning); IActionResult result = await _controller.ScanLibrary(7, cancellationToken: CancellationToken.None); var conflict = result.ShouldBeOfType(); conflict.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status409Conflict); } [Test] public async Task ScanLibrary_Should_Return_422_When_SyncDisabled() { _mediator.Send(Arg.Any(), Arg.Any()) .Returns(QueueLibraryScanResult.SyncDisabled); IActionResult result = await _controller.ScanLibrary(7, cancellationToken: CancellationToken.None); var unprocessable = result.ShouldBeOfType(); unprocessable.Value.ShouldBeOfType().Status.ShouldBe(StatusCodes.Status422UnprocessableEntity); } }