diff --git a/.gitignore b/.gitignore index 4094c46a6..d17209556 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ src/.idea/ # API doc generation .config/ + +# Ignore Jetbrains IntelliJ Workspace Directories +.idea/ diff --git a/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs new file mode 100644 index 000000000..a331de82a --- /dev/null +++ b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs @@ -0,0 +1,198 @@ +using FluentValidation.TestHelper; +using NUnit.Framework; +using NzbDrone.Core.Qualities; +using Sonarr.Api.V3.Qualities; + +namespace NzbDrone.Api.Test.v3.Qualities; + +[Parallelizable(ParallelScope.All)] +public class QualityDefinitionResourceValidatorTests +{ + private readonly QualityDefinitionResourceValidator _validator = new (); + + [Test] + public void Validate_fails_when_min_size_is_below_min_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = QualityDefinitionLimits.Min - 1, + PreferredSize = null, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_fails_when_min_size_is_above_preferred_size_and_below_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = 10, + PreferredSize = 5, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("LessThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_min_size_is_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = QualityDefinitionLimits.Min, + PreferredSize = null, + MaxSize = null + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_max_size_is_below_preferred_size_and_above_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = 10, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("LessThanOrEqualTo"); + } + + [Test] + public void Validate_fails_when_max_size_exceeds_max_limit() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = null, + MaxSize = QualityDefinitionLimits.Max + 1 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("LessThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_max_size_is_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = null, + PreferredSize = null, + MaxSize = QualityDefinitionLimits.Max + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_preferred_size_is_below_min_size_and_above_max_size() + { + var resource = new QualityDefinitionResource + { + MinSize = 10, + PreferredSize = 7, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_preferred_size_is_null_and_other_sizes_are_valid() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = null, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_passes_when_preferred_size_equals_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = 5, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } + + [Test] + public void Validate_fails_when_all_sizes_are_provided_and_invalid() + { + var resource = new QualityDefinitionResource + { + MinSize = 15, + PreferredSize = 10, + MaxSize = 5 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldHaveValidationErrorFor(r => r.MinSize) + .WithErrorCode("LessThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.MaxSize) + .WithErrorCode("GreaterThanOrEqualTo"); + + result.ShouldHaveValidationErrorFor(r => r.PreferredSize) + .WithErrorCode("GreaterThanOrEqualTo"); + } + + [Test] + public void Validate_passes_when_preferred_size_is_valid_within_limits() + { + var resource = new QualityDefinitionResource + { + MinSize = 5, + PreferredSize = 7, + MaxSize = 10 + }; + + var result = _validator.TestValidate(resource); + + result.ShouldNotHaveAnyValidationErrors(); + } +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs new file mode 100644 index 000000000..418dbc837 --- /dev/null +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs @@ -0,0 +1,7 @@ +namespace NzbDrone.Core.Qualities; + +public static class QualityDefinitionLimits +{ + public const int Min = 0; + public const int Max = 1000; +} diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs index 5910ee896..e0e26d7cb 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionController.cs @@ -12,14 +12,21 @@ using Sonarr.Http.REST.Attributes; namespace Sonarr.Api.V3.Qualities { [V3ApiController] - public class QualityDefinitionController : RestControllerWithSignalR, IHandle + public class QualityDefinitionController : + RestControllerWithSignalR, + IHandle { private readonly IQualityDefinitionService _qualityDefinitionService; - public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster) + public QualityDefinitionController( + IQualityDefinitionService qualityDefinitionService, + IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { _qualityDefinitionService = qualityDefinitionService; + + SharedValidator.RuleFor(c => c) + .SetValidator(new QualityDefinitionResourceValidator()); } [RestPutById] @@ -45,9 +52,7 @@ namespace Sonarr.Api.V3.Qualities public object UpdateMany([FromBody] List resource) { // Read from request - var qualityDefinitions = resource - .ToModel() - .ToList(); + var qualityDefinitions = resource.ToModel().ToList(); _qualityDefinitionService.UpdateMany(qualityDefinitions); @@ -55,6 +60,14 @@ namespace Sonarr.Api.V3.Qualities .ToResource()); } + [HttpGet("limits")] + public ActionResult GetLimits() + { + return Ok(new QualityDefinitionLimitsResource( + QualityDefinitionLimits.Min, + QualityDefinitionLimits.Max)); + } + [NonAction] public void Handle(CommandExecutedEvent message) { diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs new file mode 100644 index 000000000..1ccacbf28 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs @@ -0,0 +1,6 @@ +namespace Sonarr.Api.V3.Qualities; + +// SA1313 still applies to records until https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3181 is available in a release build. +#pragma warning disable SA1313 +public record QualityDefinitionLimitsResource(int Min, int Max); +#pragma warning restore SA1313 diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs new file mode 100644 index 000000000..dfe5b82b5 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; +using NzbDrone.Core.Qualities; + +namespace Sonarr.Api.V3.Qualities; + +public class QualityDefinitionResourceValidator : AbstractValidator +{ + public QualityDefinitionResourceValidator() + { + RuleFor(c => c.MinSize) + .GreaterThanOrEqualTo(QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.MinSize is not null); + + RuleFor(c => c.PreferredSize) + .GreaterThanOrEqualTo(c => c.MinSize ?? QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(c => c.MaxSize ?? QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.PreferredSize is not null); + + RuleFor(c => c.MaxSize) + .GreaterThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Min) + .WithErrorCode("GreaterThanOrEqualTo") + .LessThanOrEqualTo(QualityDefinitionLimits.Max) + .WithErrorCode("LessThanOrEqualTo") + .When(c => c.MaxSize is not null); + } +} diff --git a/src/Sonarr.Http/REST/RestController.cs b/src/Sonarr.Http/REST/RestController.cs index 7632d8b7f..e08790a45 100644 --- a/src/Sonarr.Http/REST/RestController.cs +++ b/src/Sonarr.Http/REST/RestController.cs @@ -70,11 +70,15 @@ namespace Sonarr.Http.REST var skipValidate = skipAttribute?.Skip ?? false; var skipShared = skipAttribute?.SkipShared ?? false; - if (Request.Method == "POST" || Request.Method == "PUT") + if (Request.Method is "POST" or "PUT") { - var resourceArgs = context.ActionArguments.Values.Where(x => x.GetType() == typeof(TResource)) - .Select(x => x as TResource) - .ToList(); + var resourceArgs = context.ActionArguments.Values + .SelectMany(x => x switch + { + TResource single => new[] { single }, + IEnumerable multiple => multiple, + _ => Enumerable.Empty() + }); foreach (var resource in resourceArgs) {