fix: Run validation against Custom Formats in Sonarr

If custom formats are provided, run validation logic against them. This
also was an opportunity to make the ServiceConfiguration validation
logic reusable between Sonarr and Radarr.
pull/139/head
Robert Dailey 2 years ago
parent 1ca192f3e5
commit cda317d3c3

@ -18,6 +18,10 @@ you need to make.
- **BREAKING**: The deprecated feature that still allowed you to keep your `recyclarr.yml` next to
the executable has been removed.
### Fixed
- Sonarr: Run validation on Custom Formats configuration, if specified, to check for errors.
[breaking3]: https://recyclarr.dev/wiki/upgrade-guide/upgrade-guide-v3.0
## [2.6.1] - 2022-10-15

@ -0,0 +1,62 @@
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using TrashLib.Config.Services;
namespace TrashLib.Tests.Config.Services;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ServiceConfigurationTest : IntegrationFixture
{
[Test]
public void Validation_fails_for_all_missing_required_properties()
{
// default construct which should yield default values (invalid) for all required properties
var config = new ServiceConfiguration();
var validator = ServiceLocator.Resolve<IValidator<ServiceConfiguration>>();
var result = validator.Validate(config);
var messages = new ServiceValidationMessages();
var expectedErrorMessageSubstrings = new[]
{
messages.ApiKey,
messages.BaseUrl
};
result.IsValid.Should().BeFalse();
result.Errors.Select(e => e.ErrorMessage)
.Should().BeEquivalentTo(expectedErrorMessageSubstrings);
}
[Test]
public void Fail_when_trash_ids_missing()
{
var config = new ServiceConfiguration
{
BaseUrl = "valid",
ApiKey = "valid",
CustomFormats = new List<CustomFormatConfig>
{
new() // Empty to force validation failure
}
};
var validator = ServiceLocator.Resolve<IValidator<ServiceConfiguration>>();
var result = validator.Validate(config);
var messages = new ServiceValidationMessages();
var expectedErrorMessageSubstrings = new[]
{
messages.CustomFormatTrashIds
};
result.IsValid.Should().BeFalse();
result.Errors.Select(e => e.ErrorMessage)
.Should().BeEquivalentTo(expectedErrorMessageSubstrings);
}
}

@ -1,30 +1,17 @@
using System.Collections.ObjectModel;
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
using TrashLib.Config;
using Recyclarr.TestLibrary;
using TrashLib.Config.Services;
using TrashLib.Services.Radarr;
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Tests.Radarr;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class RadarrConfigurationTest
public class RadarrConfigurationTest : IntegrationFixture
{
private IContainer _container = default!;
[OneTimeSetUp]
public void Setup()
{
var builder = new ContainerBuilder();
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterModule<RadarrAutofacModule>();
_container = builder.Build();
}
[Test]
public void Custom_format_is_valid_with_trash_id()
{
@ -38,7 +25,7 @@ public class RadarrConfigurationTest
}
};
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();
@ -50,7 +37,7 @@ public class RadarrConfigurationTest
{
// default construct which should yield default values (invalid) for all required properties
var config = new RadarrConfiguration();
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
@ -92,7 +79,7 @@ public class RadarrConfigurationTest
}
};
var validator = _container.Resolve<IValidator<RadarrConfiguration>>();
var validator = ServiceLocator.Resolve<IValidator<RadarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();

@ -1,47 +1,34 @@
using Autofac;
using FluentAssertions;
using FluentValidation;
using NUnit.Framework;
using TrashLib.Config;
using TrashLib.Services.Sonarr;
using Recyclarr.TestLibrary;
using TrashLib.Services.Sonarr.Config;
namespace TrashLib.Tests.Sonarr;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class SonarrConfigurationTest
public class SonarrConfigurationTest : IntegrationFixture
{
private IContainer _container = default!;
[OneTimeSetUp]
public void Setup()
{
var builder = new ContainerBuilder();
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterModule<SonarrAutofacModule>();
_container = builder.Build();
}
[Test]
public void Validation_fails_for_all_missing_required_properties()
{
// default construct which should yield default values (invalid) for all required properties
var config = new SonarrConfiguration
{
BaseUrl = "valid",
ApiKey = "valid",
// validation is only applied to actual release profile elements. Not if it's empty.
ReleaseProfiles = new[] {new ReleaseProfileConfig()}
};
var validator = _container.Resolve<IValidator<SonarrConfiguration>>();
var validator = ServiceLocator.Resolve<IValidator<SonarrConfiguration>>();
var result = validator.Validate(config);
var messages = new SonarrValidationMessages();
var expectedErrorMessageSubstrings = new[]
{
messages.ApiKey,
messages.BaseUrl,
messages.ReleaseProfileTrashIds
};
@ -63,7 +50,7 @@ public class SonarrConfigurationTest
}
};
var validator = _container.Resolve<IValidator<SonarrConfiguration>>();
var validator = ServiceLocator.Resolve<IValidator<SonarrConfiguration>>();
var result = validator.Validate(config);
result.IsValid.Should().BeTrue();

@ -1,6 +1,7 @@
using System.Reflection;
using Autofac;
using FluentValidation;
using TrashLib.Config.Services;
using TrashLib.Config.Settings;
using Module = Autofac.Module;
@ -16,5 +17,6 @@ public class ConfigAutofacModule : Module
builder.RegisterType<SettingsProvider>().As<ISettingsProvider>().SingleInstance();
builder.RegisterType<YamlSerializerFactory>().As<IYamlSerializerFactory>();
builder.RegisterType<ServiceValidationMessages>().As<IServiceValidationMessages>();
}
}

@ -0,0 +1,8 @@
namespace TrashLib.Config.Services;
public interface IServiceValidationMessages
{
string BaseUrl { get; }
string ApiKey { get; }
string CustomFormatTrashIds { get; }
}

@ -2,7 +2,7 @@ using JetBrains.Annotations;
namespace TrashLib.Config.Services;
public abstract class ServiceConfiguration : IServiceConfiguration
public class ServiceConfiguration : IServiceConfiguration
{
public string BaseUrl { get; init; } = "";
public string ApiKey { get; init; } = "";

@ -0,0 +1,39 @@
using FluentValidation;
using JetBrains.Annotations;
using TrashLib.Services.Radarr.Config;
namespace TrashLib.Config.Services;
[UsedImplicitly]
internal class ServiceConfigurationValidator : AbstractValidator<ServiceConfiguration>
{
public ServiceConfigurationValidator(
IServiceValidationMessages messages,
IValidator<CustomFormatConfig> customFormatConfigValidator)
{
RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl);
RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey);
RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator);
}
}
[UsedImplicitly]
internal class CustomFormatConfigValidator : AbstractValidator<CustomFormatConfig>
{
public CustomFormatConfigValidator(
IServiceValidationMessages messages,
IValidator<QualityProfileScoreConfig> qualityProfileScoreConfigValidator)
{
RuleFor(x => x.TrashIds).NotEmpty().WithMessage(messages.CustomFormatTrashIds);
RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileScoreConfigValidator);
}
}
[UsedImplicitly]
internal class QualityProfileScoreConfigValidator : AbstractValidator<QualityProfileScoreConfig>
{
public QualityProfileScoreConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName);
}
}

@ -0,0 +1,13 @@
namespace TrashLib.Config.Services;
internal /*abstract*/ class ServiceValidationMessages : IServiceValidationMessages
{
public string BaseUrl =>
"Property 'base_url' is required";
public string ApiKey =>
"Property 'api_key' is required";
public string CustomFormatTrashIds =>
"'custom_formats' elements must contain at least one element under 'trash_ids'";
}

@ -2,9 +2,6 @@ namespace TrashLib.Services.Radarr.Config;
public interface IRadarrValidationMessages
{
string BaseUrl { get; }
string ApiKey { get; }
string CustomFormatTrashIds { get; }
string QualityProfileName { get; }
string QualityDefinitionType { get; }
}

@ -9,35 +9,11 @@ namespace TrashLib.Services.Radarr.Config;
internal class RadarrConfigurationValidator : AbstractValidator<RadarrConfiguration>
{
public RadarrConfigurationValidator(
IRadarrValidationMessages messages,
IValidator<QualityDefinitionConfig> qualityDefinitionConfigValidator,
IValidator<CustomFormatConfig> customFormatConfigValidator)
IValidator<ServiceConfiguration> serviceConfigValidator,
IValidator<QualityDefinitionConfig> qualityDefinitionConfigValidator)
{
RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl);
RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey);
Include(serviceConfigValidator);
RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator);
RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator);
}
}
[UsedImplicitly]
internal class CustomFormatConfigValidator : AbstractValidator<CustomFormatConfig>
{
public CustomFormatConfigValidator(
IRadarrValidationMessages messages,
IValidator<QualityProfileScoreConfig> qualityProfileScoreConfigValidator)
{
RuleFor(x => x.TrashIds).NotEmpty().WithMessage(messages.CustomFormatTrashIds);
RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileScoreConfigValidator);
}
}
[UsedImplicitly]
internal class QualityProfileScoreConfigValidator : AbstractValidator<QualityProfileScoreConfig>
{
public QualityProfileScoreConfigValidator(IRadarrValidationMessages messages)
{
RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName);
}
}

@ -2,15 +2,6 @@ namespace TrashLib.Services.Radarr.Config;
internal class RadarrValidationMessages : IRadarrValidationMessages
{
public string BaseUrl =>
"Property 'base_url' is required";
public string ApiKey =>
"Property 'api_key' is required";
public string CustomFormatTrashIds =>
"'custom_formats' elements must contain at least one element under 'trash_ids'";
public string QualityProfileName =>
"'name' is required for elements under 'quality_profiles'";

@ -2,7 +2,5 @@ namespace TrashLib.Services.Sonarr.Config;
public interface ISonarrValidationMessages
{
string BaseUrl { get; }
string ApiKey { get; }
string ReleaseProfileTrashIds { get; }
}

@ -1,5 +1,6 @@
using FluentValidation;
using JetBrains.Annotations;
using TrashLib.Config.Services;
namespace TrashLib.Services.Sonarr.Config;
@ -8,10 +9,10 @@ internal class SonarrConfigurationValidator : AbstractValidator<SonarrConfigurat
{
public SonarrConfigurationValidator(
ISonarrValidationMessages messages,
IValidator<ServiceConfiguration> serviceConfigValidator,
IValidator<ReleaseProfileConfig> releaseProfileConfigValidator)
{
RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl);
RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey);
Include(serviceConfigValidator);
RuleForEach(x => x.ReleaseProfiles).SetValidator(releaseProfileConfigValidator);
}
}

@ -5,12 +5,6 @@ namespace TrashLib.Services.Sonarr.Config;
[UsedImplicitly]
internal class SonarrValidationMessages : ISonarrValidationMessages
{
public string BaseUrl =>
"Property 'base_url' is required";
public string ApiKey =>
"Property 'api_key' is required";
public string ReleaseProfileTrashIds =>
"'trash_ids' is required for 'release_profiles' elements";
}

Loading…
Cancel
Save