- Validation of custom formats in Sonarr v4 is now performedpull/201/head
parent
6997f4ca6a
commit
350fd21358
@ -1,99 +1,39 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.Extensions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using Autofac;
|
||||
using Autofac.Core;
|
||||
using CliFx.Infrastructure;
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.Command;
|
||||
using NUnit.Framework.Internal;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.Repo.VersionControl;
|
||||
using Recyclarr.TrashLib.Startup;
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.Cli.Tests;
|
||||
|
||||
public record ServiceFactoryWrapper(Type Service, Action<ILifetimeScope> Instantiate);
|
||||
|
||||
public static class FactoryForService<TService>
|
||||
{
|
||||
public static ServiceFactoryWrapper WithArgs<TP1>(TP1 arg1 = default!)
|
||||
{
|
||||
return new ServiceFactoryWrapper(typeof(TService),
|
||||
c => c.Resolve<Func<TP1, TService>>().Invoke(arg1));
|
||||
}
|
||||
}
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class CompositionRootTest : IntegrationFixture
|
||||
public class CompositionRootTest
|
||||
{
|
||||
private static readonly List<ServiceFactoryWrapper> FactoryTests = new()
|
||||
{
|
||||
FactoryForService<IGitRepository>.WithArgs("path")
|
||||
};
|
||||
|
||||
[TestCaseSource(typeof(CompositionRootTest), nameof(FactoryTests))]
|
||||
public void Service_requiring_factory_should_be_instantiable(ServiceFactoryWrapper service)
|
||||
{
|
||||
var act = () =>
|
||||
{
|
||||
service.Instantiate(Container);
|
||||
};
|
||||
|
||||
// Do not use `NotThrow()` here because fluent assertions doesn't show the full exception details
|
||||
// See: https://github.com/fluentassertions/fluentassertions/issues/2015
|
||||
act(); //.Should().NotThrow();
|
||||
}
|
||||
|
||||
// Warning CA1812 : CompositionRootTest.ConcreteTypeEnumerator is an internal class that is apparently never
|
||||
// instantiated.
|
||||
[SuppressMessage("Performance", "CA1812",
|
||||
Justification = "Created via reflection by TestCaseSource attribute"
|
||||
)]
|
||||
private sealed class ConcreteTypeEnumerator : IEnumerable
|
||||
[SuppressMessage("Performance", "CA1812", Justification = "Created via reflection by TestCaseSource attribute")]
|
||||
private sealed class ConcreteTypeEnumerator : IntegrationFixture, IEnumerable
|
||||
{
|
||||
private readonly ILifetimeScope _container;
|
||||
|
||||
public ConcreteTypeEnumerator()
|
||||
{
|
||||
_container = CompositionRoot.Setup();
|
||||
}
|
||||
|
||||
public IEnumerator GetEnumerator()
|
||||
{
|
||||
return _container.ComponentRegistry.Registrations
|
||||
return Container.ComponentRegistry.Registrations
|
||||
.SelectMany(x => x.Services)
|
||||
.OfType<TypedService>()
|
||||
.Select(x => x.ServiceType)
|
||||
.Distinct()
|
||||
.Except(FactoryTests.Select(x => x.Service))
|
||||
.Where(x => x.FullName == null || !x.FullName.StartsWith("Autofac."))
|
||||
.Select(x => new TestCaseParameters(new object[] {Container, x}) {TestName = x.FullName})
|
||||
.GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterAdditionalServices(ContainerBuilder builder)
|
||||
{
|
||||
var fs = new MockFileSystem();
|
||||
builder.RegisterInstance(fs).As<IFileSystem>();
|
||||
builder.RegisterInstance(new AppPaths(fs.CurrentDirectory())).As<IAppPaths>();
|
||||
builder.RegisterInstance(Substitute.For<IConsole>());
|
||||
builder.RegisterInstance(Substitute.For<ILogger>());
|
||||
builder.RegisterInstance(Substitute.For<IServiceCommand>());
|
||||
builder.RegisterInstance(Substitute.For<IServiceConfiguration>());
|
||||
}
|
||||
|
||||
[TestCaseSource(typeof(ConcreteTypeEnumerator))]
|
||||
public void Service_should_be_instantiable(Type service)
|
||||
public void Service_should_be_instantiable(ILifetimeScope scope, Type service)
|
||||
{
|
||||
using var container = CompositionRoot.Setup(RegisterAdditionalServices);
|
||||
container.Resolve(service).Should().NotBeNull();
|
||||
scope.Resolve(service).Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,34 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.Config;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.TestLibrary;
|
||||
|
||||
namespace Recyclarr.Cli.Tests.Config;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ConfigValidationExecutorTest : IntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Invalid_returns_false()
|
||||
{
|
||||
var sut = Resolve<ConfigValidationExecutor>();
|
||||
var config = new TestConfig {ApiKey = ""}; // Use bad data
|
||||
|
||||
var result = sut.Validate(config);
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Valid_returns_true()
|
||||
{
|
||||
var sut = Resolve<ConfigValidationExecutor>();
|
||||
var config = new TestConfig {ApiKey = "good", BaseUrl = "good"}; // Use good data
|
||||
|
||||
var result = sut.Validate(config);
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using FluentValidation;
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.Http;
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.Cli.Config;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class ConfigValidationExecutor
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly IValidator<ServiceConfiguration> _validator;
|
||||
|
||||
public ConfigValidationExecutor(
|
||||
ILogger log,
|
||||
IValidator<ServiceConfiguration> validator)
|
||||
{
|
||||
_log = log;
|
||||
_validator = validator;
|
||||
}
|
||||
|
||||
public bool Validate<T>(T config) where T : ServiceConfiguration
|
||||
{
|
||||
var result = _validator.Validate(config);
|
||||
if (result is not {IsValid: false})
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var printableName = config.Name ?? FlurlLogging.SanitizeUrl(config.BaseUrl);
|
||||
_log.Error("Validation failed for instance config {Instance} at line {Line} with {Count} errors",
|
||||
printableName, config.LineNumber, result.Errors.Count);
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
_log.Error("Validation error: {Msg}", error.ErrorMessage);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.TestLibrary;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ServiceConfigurationValidatorTest : IntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Validation_succeeds()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_failure_when_api_key_missing()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "", // Must not be empty
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new[] {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.ApiKey);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_failure_when_base_url_empty()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new[] {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.BaseUrl);
|
||||
}
|
||||
|
||||
public static string FirstCf { get; } = $"{nameof(TestConfig.CustomFormats)}[0].";
|
||||
|
||||
[Test]
|
||||
public void Validation_failure_when_cf_trash_ids_empty()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = Array.Empty<string>(),
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(FirstCf + nameof(CustomFormatConfig.TrashIds));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_failure_when_quality_definition_type_empty()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = ""
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(x => x.QualityDefinition!.Type);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_failure_when_quality_profile_name_empty()
|
||||
{
|
||||
var config = new TestConfig
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Resolve<ServiceConfigurationValidator>();
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldHaveValidationErrorFor(FirstCf +
|
||||
$"{nameof(CustomFormatConfig.QualityProfiles)}[0].{nameof(QualityProfileScoreConfig.Name)}");
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
using Autofac;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.TestLibrary;
|
||||
|
||||
namespace Recyclarr.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 TestConfig();
|
||||
|
||||
var validator = Container.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 TestConfig
|
||||
{
|
||||
BaseUrl = "valid",
|
||||
ApiKey = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new() // Empty to force validation failure
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Container.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
using FluentValidation.TestHelper;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.Services.Sonarr;
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class SonarrConfigurationValidatorTest
|
||||
{
|
||||
[Test]
|
||||
public void Sonarr_v4_succeeds()
|
||||
{
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"valid"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "valid"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = true,
|
||||
SupportsNamedReleaseProfiles = true
|
||||
};
|
||||
var validator = new SonarrConfigurationValidator(capabilities);
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonarr_v3_succeeds()
|
||||
{
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
ApiKey = "valid",
|
||||
BaseUrl = "valid",
|
||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"valid"},
|
||||
Filter = new SonarrProfileFilterConfig {Include = new[] {"valid"}},
|
||||
Tags = new[] {"valid"}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "valid"
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = false,
|
||||
SupportsNamedReleaseProfiles = true
|
||||
};
|
||||
var validator = new SonarrConfigurationValidator(capabilities);
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
result.ShouldNotHaveAnyValidationErrors();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonarr_v4_failures()
|
||||
{
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
ReleaseProfiles = new List<ReleaseProfileConfig> {new()}
|
||||
};
|
||||
|
||||
var capabilities = new SonarrCapabilities {SupportsCustomFormats = true};
|
||||
var validator = new SonarrConfigurationValidator(capabilities);
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
// Release profiles not allowed in v4
|
||||
result.ShouldHaveValidationErrorFor(x => x.ReleaseProfiles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Sonarr_v3_failures()
|
||||
{
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
CustomFormats = new List<CustomFormatConfig> {new()},
|
||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = Array.Empty<string>(),
|
||||
Filter = new SonarrProfileFilterConfig
|
||||
{
|
||||
Include = new[] {"include"},
|
||||
Exclude = new[] {"exclude"}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var capabilities = new SonarrCapabilities
|
||||
{
|
||||
SupportsCustomFormats = false,
|
||||
SupportsNamedReleaseProfiles = false
|
||||
};
|
||||
|
||||
var validator = new SonarrConfigurationValidator(capabilities);
|
||||
var result = validator.TestValidate(config);
|
||||
|
||||
// Custom formats not allowed in v3
|
||||
result.ShouldHaveValidationErrorFor(x => x.CustomFormats);
|
||||
|
||||
// Due to named release profiles not being supported (minimum version requirement not met)
|
||||
result.ShouldHaveValidationErrorFor(x => x);
|
||||
|
||||
var releaseProfiles = $"{nameof(config.ReleaseProfiles)}[0].";
|
||||
|
||||
// Release profile trash IDs cannot be empty
|
||||
result.ShouldHaveValidationErrorFor(releaseProfiles + nameof(ReleaseProfileConfig.TrashIds));
|
||||
|
||||
// Cannot use include + exclude filters together
|
||||
result.ShouldHaveValidationErrorFor(releaseProfiles +
|
||||
$"{nameof(ReleaseProfileConfig.Filter)}.{nameof(SonarrProfileFilterConfig.Include)}");
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Autofac;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.TrashLib.Services.Radarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Radarr;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class RadarrConfigurationTest : IntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Custom_format_is_valid_with_trash_id()
|
||||
{
|
||||
var config = new RadarrConfiguration
|
||||
{
|
||||
ApiKey = "required value",
|
||||
BaseUrl = "required value",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new() {TrashIds = new Collection<string> {"trash_id"}}
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
|
||||
var result = validator.Validate(config);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[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 RadarrConfiguration();
|
||||
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
|
||||
|
||||
var result = validator.Validate(config);
|
||||
|
||||
var expectedErrorMessageSubstrings = new[]
|
||||
{
|
||||
"Property 'base_url' is required",
|
||||
"Property 'api_key' is required",
|
||||
"'custom_formats' elements must contain at least one element under 'trash_ids'",
|
||||
"'name' is required for elements under 'quality_profiles'",
|
||||
"'type' is required for 'quality_definition'"
|
||||
};
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Select(e => e.ErrorMessage).Should()
|
||||
.OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_succeeds_when_no_missing_required_properties()
|
||||
{
|
||||
var config = new RadarrConfiguration
|
||||
{
|
||||
ApiKey = "required value",
|
||||
BaseUrl = "required value",
|
||||
CustomFormats = new List<CustomFormatConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new List<string> {"required value"},
|
||||
QualityProfiles = new List<QualityProfileScoreConfig>
|
||||
{
|
||||
new() {Name = "required value"}
|
||||
}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualityDefinitionConfig
|
||||
{
|
||||
Type = "movie"
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Container.Resolve<IValidator<RadarrConfiguration>>();
|
||||
var result = validator.Validate(config);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using Autofac;
|
||||
using FluentAssertions;
|
||||
using FluentValidation;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Sonarr;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class SonarrConfigurationTest : 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 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 result = validator.Validate(config);
|
||||
|
||||
var messages = new SonarrValidationMessages();
|
||||
var expectedErrorMessageSubstrings = new[]
|
||||
{
|
||||
messages.ReleaseProfileTrashIds
|
||||
};
|
||||
|
||||
result.IsValid.Should().BeFalse();
|
||||
result.Errors.Select(e => e.ErrorMessage)
|
||||
.Should().BeEquivalentTo(expectedErrorMessageSubstrings);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Validation_succeeds_when_no_missing_required_properties()
|
||||
{
|
||||
var config = new SonarrConfiguration
|
||||
{
|
||||
ApiKey = "required value",
|
||||
BaseUrl = "required value",
|
||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||
{
|
||||
new() {TrashIds = new[] {"123"}}
|
||||
}
|
||||
};
|
||||
|
||||
var validator = Container.Resolve<IValidator<SonarrConfiguration>>();
|
||||
var result = validator.Validate(config);
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Errors.Should().BeEmpty();
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
namespace Recyclarr.TrashLib.Config.Services;
|
||||
|
||||
public interface IServiceValidationMessages
|
||||
{
|
||||
string BaseUrl { get; }
|
||||
string ApiKey { get; }
|
||||
string CustomFormatTrashIds { get; }
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
namespace Recyclarr.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'";
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System.Reactive.Linq;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.TrashLib.Services.System;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Common;
|
||||
|
||||
public abstract class ServiceCompatibility<T> where T : class
|
||||
{
|
||||
private readonly IObservable<T> _capabilities;
|
||||
|
||||
public T Capabilities => _capabilities.Wait();
|
||||
|
||||
protected ServiceCompatibility(IServiceInformation compatibility)
|
||||
{
|
||||
_capabilities = compatibility.Version
|
||||
.Select(BuildCapabilitiesObject)
|
||||
.Replay(1)
|
||||
.AutoConnect()
|
||||
.NotNull()
|
||||
.LastAsync();
|
||||
}
|
||||
|
||||
protected abstract T BuildCapabilitiesObject(Version version);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
namespace Recyclarr.TrashLib.Services.Radarr.Config;
|
||||
|
||||
public interface IRadarrValidationMessages
|
||||
{
|
||||
string QualityProfileName { get; }
|
||||
string QualityDefinitionType { get; }
|
||||
}
|
@ -1,27 +1,9 @@
|
||||
using FluentValidation;
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.Common.FluentValidation;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Radarr.Config;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class RadarrConfigurationValidator : AbstractValidator<RadarrConfiguration>
|
||||
{
|
||||
public RadarrConfigurationValidator(
|
||||
IValidator<ServiceConfiguration> serviceConfigValidator,
|
||||
IValidator<QualityDefinitionConfig> qualityDefinitionConfigValidator)
|
||||
{
|
||||
Include(serviceConfigValidator);
|
||||
RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator);
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class QualityDefinitionConfigValidator : AbstractValidator<QualityDefinitionConfig>
|
||||
{
|
||||
public QualityDefinitionConfigValidator(IRadarrValidationMessages messages)
|
||||
{
|
||||
RuleFor(x => x.Type).NotEmpty().WithMessage(messages.QualityDefinitionType);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
namespace Recyclarr.TrashLib.Services.Radarr.Config;
|
||||
|
||||
internal class RadarrValidationMessages : IRadarrValidationMessages
|
||||
{
|
||||
public string QualityProfileName =>
|
||||
"'name' is required for elements under 'quality_profiles'";
|
||||
|
||||
public string QualityDefinitionType =>
|
||||
"'type' is required for 'quality_definition'";
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
public interface ISonarrValidationMessages
|
||||
{
|
||||
string ReleaseProfileTrashIds { get; }
|
||||
}
|
@ -1,27 +1,54 @@
|
||||
using FluentValidation;
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
using Recyclarr.Common.FluentValidation;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class SonarrConfigurationValidator : AbstractValidator<SonarrConfiguration>
|
||||
public class SonarrConfigurationValidator : AbstractValidator<SonarrConfiguration>
|
||||
{
|
||||
public SonarrConfigurationValidator(
|
||||
ISonarrValidationMessages messages,
|
||||
IValidator<ServiceConfiguration> serviceConfigValidator,
|
||||
IValidator<ReleaseProfileConfig> releaseProfileConfigValidator)
|
||||
public SonarrConfigurationValidator(SonarrCapabilities capabilities)
|
||||
{
|
||||
Include(serviceConfigValidator);
|
||||
RuleForEach(x => x.ReleaseProfiles).SetValidator(releaseProfileConfigValidator);
|
||||
RuleForEach(x => x.ReleaseProfiles).SetValidator(new ReleaseProfileConfigValidator());
|
||||
|
||||
// Release profiles may not be used with Sonarr v4
|
||||
RuleFor(x => x)
|
||||
.Must(_ => capabilities.SupportsNamedReleaseProfiles)
|
||||
.WithMessage(
|
||||
$"Your Sonarr version {capabilities.Version} does not meet the minimum " +
|
||||
$"required version of {SonarrCapabilities.MinimumVersion}.");
|
||||
|
||||
// Release profiles may not be used with Sonarr v4
|
||||
RuleFor(x => x.ReleaseProfiles).Empty()
|
||||
.When(_ => capabilities.SupportsCustomFormats)
|
||||
.WithMessage("Release profiles require Sonarr v3. " +
|
||||
"Please use `custom_formats` instead or use the right version of Sonarr.");
|
||||
|
||||
// Custom formats may not be used with Sonarr v3
|
||||
RuleFor(x => x.CustomFormats).Empty()
|
||||
.When(_ => !capabilities.SupportsCustomFormats)
|
||||
.WithMessage("Custom formats require Sonarr v4 or greater. " +
|
||||
"Please use `release_profiles` instead or use the right version of Sonarr.");
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class ReleaseProfileConfigValidator : AbstractValidator<ReleaseProfileConfig>
|
||||
{
|
||||
public ReleaseProfileConfigValidator(ISonarrValidationMessages messages)
|
||||
public ReleaseProfileConfigValidator()
|
||||
{
|
||||
RuleFor(x => x.TrashIds).NotEmpty().WithMessage("'trash_ids' is required for 'release_profiles' elements");
|
||||
RuleFor(x => x.Filter).SetNonNullableValidator(new SonarrProfileFilterConfigValidator());
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class SonarrProfileFilterConfigValidator : AbstractValidator<SonarrProfileFilterConfig>
|
||||
{
|
||||
public SonarrProfileFilterConfigValidator()
|
||||
{
|
||||
RuleFor(x => x.TrashIds).NotEmpty().WithMessage(messages.ReleaseProfileTrashIds);
|
||||
// Include & Exclude may not be used together
|
||||
RuleFor(x => x.Include).Empty().When(x => x.Exclude.Any())
|
||||
.WithMessage("`include` and `exclude` may not be used together.");
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
[UsedImplicitly]
|
||||
internal class SonarrValidationMessages : ISonarrValidationMessages
|
||||
{
|
||||
public string ReleaseProfileTrashIds =>
|
||||
"'trash_ids' is required for 'release_profiles' elements";
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Sonarr;
|
||||
|
||||
public interface ISonarrVersionEnforcement
|
||||
{
|
||||
Task DoVersionEnforcement(SonarrConfiguration config);
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
using System.Reactive.Linq;
|
||||
using Recyclarr.TrashLib.ExceptionTypes;
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.Sonarr;
|
||||
|
||||
public class SonarrVersionEnforcement : ISonarrVersionEnforcement
|
||||
{
|
||||
private readonly ISonarrCompatibility _compatibility;
|
||||
|
||||
public SonarrVersionEnforcement(ISonarrCompatibility compatibility)
|
||||
{
|
||||
_compatibility = compatibility;
|
||||
}
|
||||
|
||||
public async Task DoVersionEnforcement(SonarrConfiguration config)
|
||||
{
|
||||
var capabilities = await _compatibility.Capabilities.LastAsync();
|
||||
if (!capabilities.SupportsNamedReleaseProfiles)
|
||||
{
|
||||
throw new VersionException(
|
||||
$"Your Sonarr version {capabilities.Version} does not meet the minimum " +
|
||||
$"required version of {_compatibility.MinimumVersion} to use this program");
|
||||
}
|
||||
|
||||
switch (capabilities.SupportsCustomFormats)
|
||||
{
|
||||
case true when config.ReleaseProfiles.Any():
|
||||
throw new VersionException(
|
||||
"Sonarr v4 does not support Release Profiles. Please use Custom Formats instead.");
|
||||
|
||||
case false when config.CustomFormats.Any():
|
||||
throw new VersionException(
|
||||
"Sonarr v3 does not support Custom Formats. Please use Release Profiles instead.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.TrashLib.Services.System;
|
||||
|
||||
public interface IServiceInformation
|
||||
{
|
||||
IObservable<Version> Version { get; }
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using Recyclarr.TrashLib.Services.System.Dto;
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.System;
|
||||
|
||||
public abstract class ServiceCompatibility<T> where T : new()
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
protected ServiceCompatibility(ISystemApiService api, ILogger log)
|
||||
{
|
||||
_log = log;
|
||||
Capabilities = Observable.FromAsync(async () => await api.GetStatus(), NewThreadScheduler.Default)
|
||||
.Timeout(TimeSpan.FromSeconds(15))
|
||||
.Do(LogServiceInfo)
|
||||
.Select(x => new Version(x.Version))
|
||||
.Select(BuildCapabilitiesObject)
|
||||
.Replay(1)
|
||||
.AutoConnect();
|
||||
}
|
||||
|
||||
public IObservable<T> Capabilities { get; }
|
||||
|
||||
private void LogServiceInfo(SystemStatus status)
|
||||
{
|
||||
_log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
|
||||
}
|
||||
|
||||
protected abstract T BuildCapabilitiesObject(Version version);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Linq;
|
||||
using Flurl.Http;
|
||||
using Recyclarr.TrashLib.Http;
|
||||
using Recyclarr.TrashLib.Services.System.Dto;
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.TrashLib.Services.System;
|
||||
|
||||
public class ServiceInformation : IServiceInformation
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
public ServiceInformation(ISystemApiService api, ILogger log)
|
||||
{
|
||||
_log = log;
|
||||
Version = Observable.FromAsync(async () => await api.GetStatus(), ThreadPoolScheduler.Instance)
|
||||
.Timeout(TimeSpan.FromSeconds(15))
|
||||
.Do(LogServiceInfo)
|
||||
.Select(x => new Version(x.Version))
|
||||
.Catch((Exception ex) =>
|
||||
{
|
||||
log.Error("Exception trying to obtain service version: {Message}", ex switch
|
||||
{
|
||||
FlurlHttpException flex => flex.SanitizedExceptionMessage(),
|
||||
_ => ex.Message
|
||||
});
|
||||
|
||||
return Observable.Return(new Version());
|
||||
})
|
||||
.Replay(1)
|
||||
.AutoConnect();
|
||||
}
|
||||
|
||||
public IObservable<Version> Version { get; }
|
||||
|
||||
private void LogServiceInfo(SystemStatus status)
|
||||
{
|
||||
_log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
|
||||
}
|
||||
}
|
Loading…
Reference in new issue