parent
175aa6733b
commit
5bb2bfa8a0
@ -0,0 +1,45 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.TrashLib.Startup;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class ConfigIncludeProcessor : IIncludeProcessor
|
||||
{
|
||||
private readonly IFileSystem _fs;
|
||||
private readonly IAppPaths _paths;
|
||||
|
||||
public ConfigIncludeProcessor(IFileSystem fs, IAppPaths paths)
|
||||
{
|
||||
_fs = fs;
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
public bool CanProcess(IYamlInclude includeDirective)
|
||||
{
|
||||
return includeDirective is ConfigYamlInclude;
|
||||
}
|
||||
|
||||
public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType)
|
||||
{
|
||||
var include = (ConfigYamlInclude) includeDirective;
|
||||
|
||||
if (include.Config is null)
|
||||
{
|
||||
throw new YamlIncludeException("`config` property is required.");
|
||||
}
|
||||
|
||||
var rooted = _fs.Path.IsPathRooted(include.Config);
|
||||
|
||||
var configFile = rooted
|
||||
? _fs.FileInfo.New(include.Config)
|
||||
: _paths.ConfigsDirectory.File(include.Config);
|
||||
|
||||
if (!configFile.Exists)
|
||||
{
|
||||
var pathType = rooted ? "Absolute" : "Relative";
|
||||
throw new YamlIncludeException($"{pathType} include path does not exist: {configFile.FullName}");
|
||||
}
|
||||
|
||||
return configFile;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public interface IIncludeProcessor
|
||||
{
|
||||
bool CanProcess(IYamlInclude includeDirective);
|
||||
IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public interface IYamlIncludeResolver
|
||||
{
|
||||
IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.TrashLib.Config.Yaml;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class PolymorphicIncludeYamlBehavior : IYamlBehavior
|
||||
{
|
||||
public void Setup(DeserializerBuilder builder)
|
||||
{
|
||||
builder.WithTypeDiscriminatingNodeDeserializer(o => o
|
||||
.AddUniqueKeyTypeDiscriminator<IYamlInclude>(new Dictionary<string, Type>
|
||||
{
|
||||
[nameof(ConfigYamlInclude.Config).ToSnakeCase()] = typeof(ConfigYamlInclude),
|
||||
[nameof(TemplateYamlInclude.Template).ToSnakeCase()] = typeof(TemplateYamlInclude)
|
||||
}));
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class RadarrConfigMerger : ServiceConfigMerger<RadarrConfigYaml>
|
||||
{
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[SuppressMessage("ReSharper", "WithExpressionModifiesAllMembers")]
|
||||
public abstract class ServiceConfigMerger<T> where T : ServiceConfigYaml
|
||||
{
|
||||
protected static TVal? Combine<TVal>(TVal? a, TVal? b, Func<TVal, TVal, TVal?> combine)
|
||||
{
|
||||
if (b is null)
|
||||
{
|
||||
return a;
|
||||
}
|
||||
|
||||
if (a is null)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
return combine(a, b);
|
||||
}
|
||||
|
||||
public virtual T Merge(T a, T b)
|
||||
{
|
||||
return a with
|
||||
{
|
||||
CustomFormats = Combine(a.CustomFormats, b.CustomFormats, (x, y) => x.Concat(y).ToList()),
|
||||
QualityProfiles = MergeQualityProfiles(a.QualityProfiles, b.QualityProfiles),
|
||||
QualityDefinition = Combine(a.QualityDefinition, b.QualityDefinition,
|
||||
(x, y) => x with
|
||||
{
|
||||
Type = y.Type ?? x.Type,
|
||||
PreferredRatio = y.PreferredRatio ?? x.PreferredRatio
|
||||
}),
|
||||
|
||||
DeleteOldCustomFormats =
|
||||
b.DeleteOldCustomFormats ?? a.DeleteOldCustomFormats,
|
||||
|
||||
ReplaceExistingCustomFormats =
|
||||
b.ReplaceExistingCustomFormats ?? a.ReplaceExistingCustomFormats
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<QualityProfileConfigYaml>? MergeQualityProfiles(
|
||||
IReadOnlyCollection<QualityProfileConfigYaml>? a,
|
||||
IReadOnlyCollection<QualityProfileConfigYaml>? b)
|
||||
{
|
||||
return Combine(a, b, (x1, y1) =>
|
||||
{
|
||||
return x1
|
||||
.FullOuterJoin(y1, JoinType.Hash,
|
||||
x => x.Name,
|
||||
x => x.Name,
|
||||
l => l,
|
||||
r => r,
|
||||
MergeQualityProfile,
|
||||
StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToList();
|
||||
});
|
||||
}
|
||||
|
||||
private static QualityProfileConfigYaml MergeQualityProfile(QualityProfileConfigYaml a, QualityProfileConfigYaml b)
|
||||
{
|
||||
return a with
|
||||
{
|
||||
Upgrade = Combine(a.Upgrade, b.Upgrade, (x1, y1) => x1 with
|
||||
{
|
||||
Allowed = y1.Allowed ?? x1.Allowed,
|
||||
UntilQuality = y1.UntilQuality ?? x1.UntilQuality,
|
||||
UntilScore = y1.UntilScore ?? x1.UntilScore
|
||||
}),
|
||||
MinFormatScore = b.MinFormatScore ?? a.MinFormatScore,
|
||||
QualitySort = b.QualitySort ?? a.QualitySort,
|
||||
ScoreSet = b.ScoreSet ?? a.ScoreSet,
|
||||
ResetUnmatchedScores = Combine(a.ResetUnmatchedScores, b.ResetUnmatchedScores, (x1, y1) => x1 with
|
||||
{
|
||||
Enabled = y1.Enabled ?? x1.Enabled,
|
||||
Except = Combine(x1.Except, y1.Except, (x2, y2) => Combine(x2, y2, (x3, y3) => x3
|
||||
.Concat(y3)
|
||||
.Distinct(StringComparer.InvariantCultureIgnoreCase)
|
||||
.ToList()))
|
||||
}),
|
||||
Qualities = Combine(a.Qualities, b.Qualities, (x1, y1) => x1.Concat(y1).ToList())
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class SonarrConfigMerger : ServiceConfigMerger<SonarrConfigYaml>
|
||||
{
|
||||
public override SonarrConfigYaml Merge(SonarrConfigYaml a, SonarrConfigYaml b)
|
||||
{
|
||||
return base.Merge(a, b) with
|
||||
{
|
||||
ReleaseProfiles = Combine(a.ReleaseProfiles, b.ReleaseProfiles,
|
||||
(x, y) => x.Concat(y).ToList())
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class TemplateIncludeProcessor : IIncludeProcessor
|
||||
{
|
||||
private readonly IConfigTemplateGuideService _templates;
|
||||
|
||||
public TemplateIncludeProcessor(IConfigTemplateGuideService templates)
|
||||
{
|
||||
_templates = templates;
|
||||
}
|
||||
|
||||
public bool CanProcess(IYamlInclude includeDirective)
|
||||
{
|
||||
return includeDirective is TemplateYamlInclude;
|
||||
}
|
||||
|
||||
public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType)
|
||||
{
|
||||
var include = (TemplateYamlInclude) includeDirective;
|
||||
|
||||
if (include.Template is null)
|
||||
{
|
||||
throw new YamlIncludeException("`template` property is required.");
|
||||
}
|
||||
|
||||
var includePath = _templates.GetIncludeData()
|
||||
.Where(x => x.Service == serviceType)
|
||||
.FirstOrDefault(x => x.Id.EqualsIgnoreCase(include.Template));
|
||||
|
||||
if (includePath is null)
|
||||
{
|
||||
throw new YamlIncludeException(
|
||||
$"For service type '{serviceType}', unable to find config template with name '{include.Template}'");
|
||||
}
|
||||
|
||||
return includePath.TemplateFile;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[SuppressMessage("Design", "CA1040:Avoid empty interfaces", Justification =
|
||||
"Used for type-discriminating node deserializer")]
|
||||
public interface IYamlInclude
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record ConfigYamlInclude : IYamlInclude
|
||||
{
|
||||
public string? Config { get; init; }
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public record TemplateYamlInclude : IYamlInclude
|
||||
{
|
||||
public string? Template { get; init; }
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class YamlIncludeException : Exception
|
||||
{
|
||||
public YamlIncludeException(string? message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
public class YamlIncludeResolver : IYamlIncludeResolver
|
||||
{
|
||||
private readonly IReadOnlyCollection<IIncludeProcessor> _includeProcessors;
|
||||
|
||||
public YamlIncludeResolver(IReadOnlyCollection<IIncludeProcessor> includeProcessors)
|
||||
{
|
||||
_includeProcessors = includeProcessors;
|
||||
}
|
||||
|
||||
public IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType)
|
||||
{
|
||||
var processor = _includeProcessors.FirstOrDefault(x => x.CanProcess(includeType));
|
||||
if (processor is null)
|
||||
{
|
||||
throw new YamlIncludeException("Include type is not supported");
|
||||
}
|
||||
|
||||
var yamlFile = processor.GetPathToConfig(includeType, serviceType);
|
||||
if (!yamlFile.Exists)
|
||||
{
|
||||
throw new YamlIncludeException($"Included YAML file does not exist: {yamlFile.FullName}");
|
||||
}
|
||||
|
||||
return yamlFile;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
using Serilog.Context;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.Parsing.PostProcessing;
|
||||
|
||||
public class IncludePostProcessor : IConfigPostProcessor
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
private readonly ConfigParser _parser;
|
||||
private readonly ConfigValidationExecutor _validator;
|
||||
private readonly IYamlIncludeResolver _includeResolver;
|
||||
|
||||
public IncludePostProcessor(
|
||||
ILogger log,
|
||||
ConfigParser parser,
|
||||
ConfigValidationExecutor validator,
|
||||
IYamlIncludeResolver includeResolver)
|
||||
{
|
||||
_log = log;
|
||||
_parser = parser;
|
||||
_validator = validator;
|
||||
_includeResolver = includeResolver;
|
||||
}
|
||||
|
||||
public RootConfigYaml Process(RootConfigYaml config)
|
||||
{
|
||||
return new RootConfigYaml
|
||||
{
|
||||
Radarr = ProcessIncludes(config.Radarr, new RadarrConfigMerger(), SupportedServices.Radarr),
|
||||
Sonarr = ProcessIncludes(config.Sonarr, new SonarrConfigMerger(), SupportedServices.Sonarr)
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, T>? ProcessIncludes<T>(
|
||||
IReadOnlyDictionary<string, T>? configs,
|
||||
ServiceConfigMerger<T> merger,
|
||||
SupportedServices serviceType)
|
||||
where T : ServiceConfigYaml, new()
|
||||
{
|
||||
if (configs is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var mergedConfigs = new Dictionary<string, T>();
|
||||
|
||||
foreach (var (key, config) in configs)
|
||||
{
|
||||
if (config.Include is null)
|
||||
{
|
||||
mergedConfigs.Add(key, config);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Combine all includes together first
|
||||
var aggregateInclude = config.Include
|
||||
.Select(x => LoadYamlInclude<T>(x, serviceType))
|
||||
.Aggregate(new T(), merger.Merge);
|
||||
|
||||
// Merge the config into the aggregated includes so that root config values overwrite included values.
|
||||
mergedConfigs.Add(key, merger.Merge(aggregateInclude, config) with
|
||||
{
|
||||
// No reason to keep these around anymore now that they have been merged
|
||||
Include = null
|
||||
});
|
||||
}
|
||||
|
||||
return mergedConfigs;
|
||||
}
|
||||
|
||||
private T LoadYamlInclude<T>(IYamlInclude includeType, SupportedServices serviceType)
|
||||
where T : ServiceConfigYaml
|
||||
{
|
||||
var yamlFile = _includeResolver.GetIncludePath(includeType, serviceType);
|
||||
using var logScope = LogContext.PushProperty(LogProperty.Scope, $"Include {yamlFile.Name}");
|
||||
|
||||
var configToMerge = _parser.Load<T>(yamlFile);
|
||||
if (configToMerge is null)
|
||||
{
|
||||
throw new YamlIncludeException($"Failed to parse include file: {yamlFile.FullName}");
|
||||
}
|
||||
|
||||
if (!_validator.Validate(configToMerge))
|
||||
{
|
||||
throw new YamlIncludeException($"Validation of included YAML failed: {yamlFile.FullName}");
|
||||
}
|
||||
|
||||
if (configToMerge.BaseUrl is not null)
|
||||
{
|
||||
_log.Warning("`base_url` is not allowed in YAML includes");
|
||||
}
|
||||
|
||||
if (configToMerge.ApiKey is not null)
|
||||
{
|
||||
_log.Warning("`api_key` is not allowed in YAML includes");
|
||||
}
|
||||
|
||||
if (configToMerge.Include is not null)
|
||||
{
|
||||
_log.Warning("Nested `include` directives are not supported");
|
||||
}
|
||||
|
||||
return configToMerge;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace Recyclarr.TrashLib.Config.Parsing;
|
||||
|
||||
public static class YamlValidatorRuleSets
|
||||
{
|
||||
public const string RootConfig = "RootConfig";
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Recyclarr.TrashLib.ExceptionTypes;
|
||||
|
||||
public class PostProcessingException : Exception
|
||||
{
|
||||
public PostProcessingException(string? message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
using Recyclarr.TrashLib.Startup;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ConfigIncludeProcessorTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Can_process_expected_type(
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
var result = sut.CanProcess(new ConfigYamlInclude());
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_null_include_path(
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
var includeDirective = new ConfigYamlInclude
|
||||
{
|
||||
Config = null
|
||||
};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>();
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Get_relative_config_include_path(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IAppPaths paths,
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
fs.AddEmptyFile(paths.ConfigsDirectory.File("foo/bar/config.yml"));
|
||||
|
||||
var includeDirective = new ConfigYamlInclude
|
||||
{
|
||||
Config = "foo/bar/config.yml"
|
||||
};
|
||||
|
||||
var path = sut.GetPathToConfig(includeDirective, default);
|
||||
|
||||
path.FullName.Should().Be(paths.ConfigsDirectory.File("foo/bar/config.yml").FullName);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Get_absolute_config_include_path(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
var absolutePath = fs.CurrentDirectory().File("foo/bar/config.yml");
|
||||
fs.AddEmptyFile(absolutePath);
|
||||
|
||||
var includeDirective = new ConfigYamlInclude
|
||||
{
|
||||
Config = absolutePath.FullName
|
||||
};
|
||||
|
||||
var path = sut.GetPathToConfig(includeDirective, default);
|
||||
|
||||
path.FullName.Should().Be(absolutePath.FullName);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_relative_config_include_path_does_not_exist(
|
||||
// Freeze the mock FS even though we don't use it so that the "Exists" check works right.
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
var includeDirective = new ConfigYamlInclude
|
||||
{
|
||||
Config = "foo/bar/config.yml"
|
||||
};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("Relative*not exist*");
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_absolute_config_include_path_does_not_exist(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
ConfigIncludeProcessor sut)
|
||||
{
|
||||
var absolutePath = fs.CurrentDirectory().File("foo/bar/config.yml");
|
||||
|
||||
var includeDirective = new ConfigYamlInclude
|
||||
{
|
||||
Config = absolutePath.FullName
|
||||
};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, default);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("Absolute*not exist*");
|
||||
}
|
||||
}
|
@ -0,0 +1,525 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentAssertions.Execution;
|
||||
using Recyclarr.TrashLib.Config.Parsing;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ServiceConfigMergerTest
|
||||
{
|
||||
[Test]
|
||||
public void Merge_api_key_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_api_key_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_api_key_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "a"
|
||||
};
|
||||
|
||||
// API Key should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ApiKey = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
// BaseUrl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_base_url_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "a"
|
||||
};
|
||||
|
||||
// Baseurl should not be merged!
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
BaseUrl = "b"
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_definition_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type1",
|
||||
PreferredRatio = 0.5m
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "type2",
|
||||
PreferredRatio = 1.0m
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_custom_formats_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1", "id2"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "c", Score = 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id3", "id4"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "d", Score = 200}
|
||||
}
|
||||
},
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id5", "id6"},
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityScoreConfigYaml {Name = "e", Score = 300}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
CustomFormats = leftConfig.CustomFormats.Concat(rightConfig.CustomFormats).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
//------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_profiles_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_quality_profiles_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Merge_quality_profiles_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set1",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality1",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Except = new[] {"except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
UntilQuality = "quality2"
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
using var scope = new AssertionScope().UsingLineBreaks;
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
QualityProfiles = new[]
|
||||
{
|
||||
new QualityProfileConfigYaml
|
||||
{
|
||||
Name = "e",
|
||||
QualitySort = QualitySortAlgorithm.Top,
|
||||
MinFormatScore = 100,
|
||||
ScoreSet = "set2",
|
||||
ResetUnmatchedScores = new ResetUnmatchedScoresConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Except = new[] {"except1", "except2", "except3"}
|
||||
},
|
||||
Upgrade = new QualityProfileFormatUpgradeYaml
|
||||
{
|
||||
Allowed = true,
|
||||
UntilQuality = "quality2",
|
||||
UntilScore = 200
|
||||
},
|
||||
Qualities = new[]
|
||||
{
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality1",
|
||||
Qualities = new[] {"quality"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = false,
|
||||
Name = "quality2",
|
||||
Qualities = new[] {"quality3"}
|
||||
},
|
||||
new QualityProfileQualityConfigYaml
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "quality4",
|
||||
Qualities = new[] {"quality5", "quality6"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentAssertions.Execution;
|
||||
using Recyclarr.TrashLib.Config.Parsing;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class SonarrConfigMergerTest
|
||||
{
|
||||
[Test]
|
||||
public void Merge_release_profiles_from_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude"},
|
||||
Include = new[] {"include"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml();
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(leftConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_release_profiles_from_non_empty_right_to_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml();
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude"},
|
||||
Include = new[] {"include"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
result.Should().BeEquivalentTo(rightConfig);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
|
||||
public void Merge_release_profiles_from_non_empty_right_to_non_empty_left()
|
||||
{
|
||||
var leftConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id1"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude1"},
|
||||
Include = new[] {"include1"}
|
||||
},
|
||||
Tags = new[] {"tag1", "tag2"},
|
||||
StrictNegativeScores = true
|
||||
},
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id2", "id3"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude2"},
|
||||
Include = new[] {"include2"}
|
||||
},
|
||||
Tags = new[] {"tag3"},
|
||||
StrictNegativeScores = true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var rightConfig = new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = new[]
|
||||
{
|
||||
new ReleaseProfileConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"id4"},
|
||||
Filter = new ReleaseProfileFilterConfigYaml
|
||||
{
|
||||
Exclude = new[] {"exclude3"},
|
||||
Include = new[] {"include3"}
|
||||
},
|
||||
Tags = new[] {"tag4", "tag5"},
|
||||
StrictNegativeScores = false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var sut = new SonarrConfigMerger();
|
||||
|
||||
var result = sut.Merge(leftConfig, rightConfig);
|
||||
|
||||
using var scope = new AssertionScope().UsingLineBreaks;
|
||||
|
||||
result.Should().BeEquivalentTo(new SonarrConfigYaml
|
||||
{
|
||||
ReleaseProfiles = leftConfig.ReleaseProfiles.Concat(rightConfig.ReleaseProfiles).ToList()
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.TrashLib.Config;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
using Recyclarr.TrashLib.Config.Services;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class TemplateIncludeProcessorTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void Can_process_expected_type(
|
||||
TemplateIncludeProcessor sut)
|
||||
{
|
||||
var result = sut.CanProcess(new TemplateYamlInclude());
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Obtain_path_from_template(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IConfigTemplateGuideService templates,
|
||||
TemplateIncludeProcessor sut)
|
||||
{
|
||||
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||
templates.GetIncludeData().Returns(new[]
|
||||
{
|
||||
new TemplatePath
|
||||
{
|
||||
Id = "my-template",
|
||||
Service = SupportedServices.Radarr,
|
||||
TemplateFile = templatePath
|
||||
}
|
||||
});
|
||||
|
||||
var includeDirective = new TemplateYamlInclude {Template = "my-template"};
|
||||
|
||||
var path = sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||
|
||||
path.FullName.Should().Be(templatePath.FullName);
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_template_is_null(
|
||||
TemplateIncludeProcessor sut)
|
||||
{
|
||||
var includeDirective = new TemplateYamlInclude {Template = null};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*template*is required*");
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_service_types_are_mixed(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IConfigTemplateGuideService templates,
|
||||
TemplateIncludeProcessor sut)
|
||||
{
|
||||
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||
templates.GetIncludeData().Returns(new[]
|
||||
{
|
||||
new TemplatePath
|
||||
{
|
||||
Id = "my-template",
|
||||
Service = SupportedServices.Radarr,
|
||||
TemplateFile = templatePath
|
||||
}
|
||||
});
|
||||
|
||||
var includeDirective = new TemplateYamlInclude {Template = "my-template"};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Sonarr);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*unable to find*");
|
||||
}
|
||||
|
||||
[Test, AutoMockData]
|
||||
public void Throw_when_no_template_found(
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
[Frozen] IConfigTemplateGuideService templates,
|
||||
TemplateIncludeProcessor sut)
|
||||
{
|
||||
var templatePath = fs.CurrentDirectory().File("some/path/template.yml");
|
||||
templates.GetIncludeData().Returns(new[]
|
||||
{
|
||||
new TemplatePath
|
||||
{
|
||||
Id = "my-template",
|
||||
Service = SupportedServices.Radarr,
|
||||
TemplateFile = templatePath
|
||||
}
|
||||
});
|
||||
|
||||
var includeDirective = new TemplateYamlInclude {Template = "template-does-not-exist"};
|
||||
|
||||
var act = () => sut.GetPathToConfig(includeDirective, SupportedServices.Radarr);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*unable to find*");
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.TrashLib.Config;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class YamlIncludeResolverTest
|
||||
{
|
||||
[Test]
|
||||
public void Find_and_return_processor()
|
||||
{
|
||||
var processors = new[]
|
||||
{
|
||||
Substitute.For<IIncludeProcessor>(),
|
||||
Substitute.For<IIncludeProcessor>()
|
||||
};
|
||||
|
||||
processors[1].CanProcess(default!).ReturnsForAnyArgs(true);
|
||||
processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ =>
|
||||
{
|
||||
var fileInfo = Substitute.For<IFileInfo>();
|
||||
fileInfo.Exists.Returns(true);
|
||||
fileInfo.FullName.Returns("the_path");
|
||||
return fileInfo;
|
||||
});
|
||||
|
||||
var sut = new YamlIncludeResolver(processors);
|
||||
var result = sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||
|
||||
result.FullName.Should().Be("the_path");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_no_matching_processor()
|
||||
{
|
||||
var processors = new[]
|
||||
{
|
||||
Substitute.For<IIncludeProcessor>(),
|
||||
Substitute.For<IIncludeProcessor>()
|
||||
};
|
||||
|
||||
var sut = new YamlIncludeResolver(processors);
|
||||
var act = () => sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*type is not supported*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_path_does_not_exist()
|
||||
{
|
||||
var processors = new[]
|
||||
{
|
||||
Substitute.For<IIncludeProcessor>(),
|
||||
Substitute.For<IIncludeProcessor>()
|
||||
};
|
||||
|
||||
processors[1].CanProcess(default!).ReturnsForAnyArgs(true);
|
||||
processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ =>
|
||||
{
|
||||
var fileInfo = Substitute.For<IFileInfo>();
|
||||
fileInfo.Exists.Returns(false);
|
||||
return fileInfo;
|
||||
});
|
||||
|
||||
var sut = new YamlIncludeResolver(processors);
|
||||
var act = () => sut.GetIncludePath(Substitute.For<IYamlInclude>(), SupportedServices.Radarr);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*does not exist*");
|
||||
}
|
||||
}
|
@ -0,0 +1,176 @@
|
||||
using System.IO.Abstractions;
|
||||
using Recyclarr.TrashLib.Config.Parsing;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing;
|
||||
using Recyclarr.TrashLib.Config.Parsing.PostProcessing.ConfigMerging;
|
||||
using Recyclarr.TrashLib.TestLibrary;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing.PostProcessing;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class IncludePostProcessorIntegrationTest : TrashLibIntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void No_change_when_no_includes()
|
||||
{
|
||||
var sut = Resolve<IncludePostProcessor>();
|
||||
|
||||
var config = new RootConfigYaml
|
||||
{
|
||||
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||
{
|
||||
["service1"] = new()
|
||||
{
|
||||
ApiKey = "asdf",
|
||||
BaseUrl = "fdsa"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Process(config);
|
||||
|
||||
result.Should().BeEquivalentTo(config);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_unable_to_parse()
|
||||
{
|
||||
var sut = Resolve<IncludePostProcessor>();
|
||||
|
||||
var configPath = Fs.CurrentDirectory().File("my-include.yml");
|
||||
|
||||
Fs.AddFile(configPath, new MockFileData(
|
||||
"""
|
||||
asdf: invalid
|
||||
"""));
|
||||
|
||||
var config = new RootConfigYaml
|
||||
{
|
||||
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||
{
|
||||
["service1"] = new()
|
||||
{
|
||||
Include = new[]
|
||||
{
|
||||
new ConfigYamlInclude {Config = configPath.FullName}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => sut.Process(config);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*parse include file*my-include.yml*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_unable_to_validate()
|
||||
{
|
||||
var sut = Resolve<IncludePostProcessor>();
|
||||
|
||||
var configPath = Fs.CurrentDirectory().File("my-include.yml");
|
||||
|
||||
Fs.AddFile(configPath, new MockFileData(
|
||||
"""
|
||||
custom_formats:
|
||||
"""));
|
||||
|
||||
var config = new RootConfigYaml
|
||||
{
|
||||
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||
{
|
||||
["service1"] = new()
|
||||
{
|
||||
Include = new[]
|
||||
{
|
||||
new ConfigYamlInclude {Config = configPath.FullName}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var act = () => sut.Process(config);
|
||||
|
||||
act.Should().Throw<YamlIncludeException>().WithMessage("*Validation*failed*my-include.yml*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Merge_works()
|
||||
{
|
||||
var sut = Resolve<IncludePostProcessor>();
|
||||
|
||||
var configPath1 = Fs.CurrentDirectory().File("my-include1.yml");
|
||||
Fs.AddFile(configPath1, new MockFileData(
|
||||
"""
|
||||
custom_formats:
|
||||
- trash_ids:
|
||||
- 496f355514737f7d83bf7aa4d24f8169
|
||||
|
||||
quality_definition:
|
||||
type: anime
|
||||
preferred_ratio: 0.75
|
||||
|
||||
delete_old_custom_formats: false
|
||||
"""));
|
||||
|
||||
var configPath2 = Fs.CurrentDirectory().File("sub_dir/my-include2.yml");
|
||||
Fs.AddFile(configPath2, new MockFileData(
|
||||
"""
|
||||
custom_formats:
|
||||
- trash_ids:
|
||||
- 240770601cc226190c367ef59aba7463
|
||||
|
||||
delete_old_custom_formats: true
|
||||
"""));
|
||||
|
||||
var config = new RootConfigYaml
|
||||
{
|
||||
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||
{
|
||||
["service1"] = new()
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml
|
||||
{
|
||||
TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}
|
||||
}
|
||||
},
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "series"
|
||||
},
|
||||
Include = new[]
|
||||
{
|
||||
new ConfigYamlInclude {Config = configPath1.FullName},
|
||||
new ConfigYamlInclude {Config = configPath2.FullName}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var result = sut.Process(config);
|
||||
|
||||
result.Should().BeEquivalentTo(new RootConfigYaml
|
||||
{
|
||||
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||
{
|
||||
["service1"] = new()
|
||||
{
|
||||
CustomFormats = new[]
|
||||
{
|
||||
new CustomFormatConfigYaml {TrashIds = new[] {"496f355514737f7d83bf7aa4d24f8169"}},
|
||||
new CustomFormatConfigYaml {TrashIds = new[] {"2f22d89048b01681dde8afe203bf2e95"}},
|
||||
new CustomFormatConfigYaml {TrashIds = new[] {"240770601cc226190c367ef59aba7463"}}
|
||||
},
|
||||
QualityDefinition = new QualitySizeConfigYaml
|
||||
{
|
||||
Type = "series",
|
||||
PreferredRatio = 0.75m
|
||||
},
|
||||
DeleteOldCustomFormats = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in new issue