diff --git a/src/Recyclarr.TestLibrary/IntegrationFixture.cs b/src/Recyclarr.TestLibrary/IntegrationFixture.cs index f10769c8..62e3acf8 100644 --- a/src/Recyclarr.TestLibrary/IntegrationFixture.cs +++ b/src/Recyclarr.TestLibrary/IntegrationFixture.cs @@ -37,12 +37,18 @@ public abstract class IntegrationFixture : IDisposable RegisterMockFor(builder); RegisterMockFor(builder); + RegisterExtraTypes(builder); + builder.RegisterSource(); }); SetupMetadataJson(); } + protected virtual void RegisterExtraTypes(ContainerBuilder builder) + { + } + private static ILogger CreateLogger() { return new LoggerConfiguration() diff --git a/src/Recyclarr.Tests/Command/ServiceCommandTest.cs b/src/Recyclarr.Tests/Command/ServiceCommandTest.cs deleted file mode 100644 index 46036b41..00000000 --- a/src/Recyclarr.Tests/Command/ServiceCommandTest.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System.IO.Abstractions; -using System.IO.Abstractions.Extensions; -using System.IO.Abstractions.TestingHelpers; -using AutoFixture.NUnit3; -using CliFx.Attributes; -using CliFx.Exceptions; -using FluentAssertions; -using JetBrains.Annotations; -using NUnit.Framework; -using Recyclarr.Command; -using Recyclarr.Config; -using Recyclarr.TestLibrary; -using TestLibrary.AutoFixture; -using TrashLib; -using TrashLib.Config.Services; -using TrashLib.Startup; - -namespace Recyclarr.Tests.Command; - -[UsedImplicitly] -public class TestConfiguration : ServiceConfiguration -{ - public string ServiceName { get; set; } -} - -[Command] -[UsedImplicitly] -public class TestServiceCommand : ServiceCommand -{ - private readonly ConfigurationLoader _loader; - public override string Name => nameof(TestServiceCommand); - - public IEnumerable LoadedConfigs { get; private set; } = Array.Empty(); - - public TestServiceCommand(ConfigurationLoader loader) - { - _loader = loader; - } - - public async Task Process(IServiceLocatorProxy container, string[] configSections) - { - await base.Process(container); - - LoadedConfigs = configSections.SelectMany(x => - { - return _loader.LoadMany(Config, x).Select(y => - { - y.ServiceName = x; - return y; - }); - }); - } -} - -[TestFixture] -[Parallelizable(ParallelScope.All)] -public class ServiceCommandTest : IntegrationFixture -{ - private static string[] GetYamlPaths(IAppPaths paths) - { - return new[] - { - paths.ConfigPath.FullName, - paths.ConfigsDirectory.File("b.yml").FullName, - paths.ConfigsDirectory.File("c.yml").FullName - }; - } - - [Test, AutoMockData] - public async Task Use_configs_dir_and_file_if_no_cli_argument( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - var yamlPaths = GetYamlPaths(paths); - - foreach (var path in yamlPaths) - { - fs.AddFile(path, new MockFileData("")); - } - - await sut.Process(container); - - sut.Config.Should().BeEquivalentTo(yamlPaths); - } - - [Test, AutoMockData] - public async Task Use_paths_from_cli_instead_of_configs_dir_and_file_if_argument_specified( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - var yamlPaths = GetYamlPaths(paths); - - foreach (var path in yamlPaths) - { - fs.AddFile(path, new MockFileData("")); - } - - var manualConfig = fs.CurrentDirectory().File("manual-config.yml"); - fs.AddFile(manualConfig.FullName, new MockFileData("")); - sut.Config = new[] {manualConfig.FullName}; - - await sut.Process(container); - - sut.Config.Should().BeEquivalentTo(manualConfig.FullName); - } - - [Test, AutoMockData] - public async Task Non_existent_files_are_skipped( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - var yamlPaths = GetYamlPaths(paths); - - fs.AddFile(yamlPaths[0], new MockFileData("")); - fs.AddFile(yamlPaths[1], new MockFileData("")); - - sut.Config = yamlPaths.Take(2).ToList(); - - await sut.Process(container); - - sut.Config.Should().BeEquivalentTo(yamlPaths.Take(2)); - } - - [Test, AutoMockData] - public async Task No_recyclarr_yml_when_not_exists( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - var testFile = paths.ConfigsDirectory.File("test.yml").FullName; - fs.AddFile(testFile, new MockFileData("")); - - await sut.Process(container); - - sut.Config.Should().BeEquivalentTo(testFile); - } - - [Test, AutoMockData] - public async Task Only_add_recyclarr_yml_when_exists( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - fs.AddFile(paths.ConfigPath.FullName, new MockFileData("")); - - await sut.Process(container); - - sut.Config.Should().BeEquivalentTo(paths.ConfigPath.FullName); - } - - [Test, AutoMockData] - public async Task Throw_when_no_configs_found( - [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - IServiceLocatorProxy container, - TestServiceCommand sut) - { - var act = () => sut.Process(container); - - await act.Should().ThrowAsync(); - } - - private static string MakeYamlData(string service, string url) - { - return $@" -{service}: - - base_url: {url} - api_key: abc123 -"; - } - - [Test] - public async Task Correct_yaml_loaded_multi_config( - // [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, - // [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, - // IServiceLocatorProxy container, - // TestServiceCommand sut - ) - { - var sut = Resolve(); - var paths = Resolve(); - - var yamlPaths = GetYamlPaths(paths); - var data = new[] - { - MakeYamlData("sonarr", "a"), - MakeYamlData("radarr", "b") + MakeYamlData("sonarr", "c"), - MakeYamlData("radarr", "d") - }; - - foreach (var (path, yaml) in yamlPaths.Zip(data)) - { - Fs.AddFile(path, new MockFileData(yaml)); - } - - await sut.Process(ServiceLocator, new[] {"sonarr", "radarr"}); - - sut.LoadedConfigs.Should().BeEquivalentTo(new[] - { - new {ServiceName = "sonarr", BaseUrl = "a"}, - new {ServiceName = "radarr", BaseUrl = "b"}, - new {ServiceName = "sonarr", BaseUrl = "c"}, - new {ServiceName = "radarr", BaseUrl = "d"} - }); - } -} diff --git a/src/Recyclarr.Tests/Config/ConfigurationFinderTest.cs b/src/Recyclarr.Tests/Config/ConfigurationFinderTest.cs new file mode 100644 index 00000000..6c52002f --- /dev/null +++ b/src/Recyclarr.Tests/Config/ConfigurationFinderTest.cs @@ -0,0 +1,139 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.Extensions; +using System.IO.Abstractions.TestingHelpers; +using AutoFixture.NUnit3; +using CliFx.Exceptions; +using FluentAssertions; +using NUnit.Framework; +using Recyclarr.Config; +using TestLibrary.AutoFixture; +using TrashLib; +using TrashLib.Startup; + +namespace Recyclarr.Tests.Config; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ConfigurationFinderTest +{ + private static string[] GetYamlPaths(IAppPaths paths) + { + return new[] + { + paths.ConfigPath.FullName, + paths.ConfigsDirectory.File("b.yml").FullName, + paths.ConfigsDirectory.File("c.yml").FullName + }; + } + + [Test, AutoMockData] + public void Use_default_configs_if_explicit_list_null( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var yamlPaths = GetYamlPaths(paths); + + foreach (var path in yamlPaths) + { + fs.AddFile(path, new MockFileData("")); + } + + var result = sut.GetConfigFiles(null); + + result.Should().BeEquivalentTo(yamlPaths); + } + + [Test, AutoMockData] + public void Use_default_configs_if_explicit_list_empty( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var yamlPaths = GetYamlPaths(paths); + + foreach (var path in yamlPaths) + { + fs.AddFile(path, new MockFileData("")); + } + + var result = sut.GetConfigFiles(new List()); + + result.Should().BeEquivalentTo(yamlPaths); + } + + [Test, AutoMockData] + public void Use_explicit_paths_instead_of_default( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var yamlPaths = GetYamlPaths(paths); + + foreach (var path in yamlPaths) + { + fs.AddFile(path, new MockFileData("")); + } + + var manualConfig = fs.CurrentDirectory().File("manual-config.yml"); + fs.AddFile(manualConfig.FullName, new MockFileData("")); + + var result = sut.GetConfigFiles(new[] {manualConfig.FullName}); + + result.Should().BeEquivalentTo(manualConfig.FullName); + } + + [Test, AutoMockData] + public void Non_existent_files_are_skipped( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var yamlPaths = GetYamlPaths(paths); + + fs.AddFile(yamlPaths[0], new MockFileData("")); + fs.AddFile(yamlPaths[1], new MockFileData("")); + + var result = sut.GetConfigFiles(yamlPaths); + + result.Should().BeEquivalentTo(yamlPaths.Take(2)); + } + + [Test, AutoMockData] + public void No_recyclarr_yml_when_not_exists( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var testFile = paths.ConfigsDirectory.File("test.yml").FullName; + fs.AddFile(testFile, new MockFileData("")); + + var result = sut.GetConfigFiles(Array.Empty()); + + result.Should().BeEquivalentTo(testFile); + } + + [Test, AutoMockData] + public void Only_add_recyclarr_yml_when_exists( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + fs.AddFile(paths.ConfigPath.FullName, new MockFileData("")); + + var result = sut.GetConfigFiles(Array.Empty()); + + result.Should().BeEquivalentTo(paths.ConfigPath.FullName); + } + + [Test, AutoMockData] + public void Throw_when_no_configs_found( + [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, + [Frozen(Matching.ImplementedInterfaces)] AppPaths paths, + ConfigurationFinder sut) + { + var act = () => sut.GetConfigFiles(Array.Empty()); + + act.Should().Throw(); + } +} diff --git a/src/Recyclarr.Tests/Config/Services/ConfigurationLoaderTest.cs b/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs similarity index 99% rename from src/Recyclarr.Tests/Config/Services/ConfigurationLoaderTest.cs rename to src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs index 1df88b32..431056ca 100644 --- a/src/Recyclarr.Tests/Config/Services/ConfigurationLoaderTest.cs +++ b/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs @@ -18,7 +18,7 @@ using TestLibrary.AutoFixture; using TrashLib.Config.Services; using TrashLib.Services.Sonarr.Config; -namespace Recyclarr.Tests.Config.Services; +namespace Recyclarr.Tests.Config; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/src/Recyclarr.Tests/Config/Services/Data/Load_UsingStream_CorrectParsing.yml b/src/Recyclarr.Tests/Config/Data/Load_UsingStream_CorrectParsing.yml similarity index 100% rename from src/Recyclarr.Tests/Config/Services/Data/Load_UsingStream_CorrectParsing.yml rename to src/Recyclarr.Tests/Config/Data/Load_UsingStream_CorrectParsing.yml diff --git a/src/Recyclarr/Command/RadarrCommand.cs b/src/Recyclarr/Command/RadarrCommand.cs index 1cfc7129..65f1bbf3 100644 --- a/src/Recyclarr/Command/RadarrCommand.cs +++ b/src/Recyclarr/Command/RadarrCommand.cs @@ -34,7 +34,6 @@ internal class RadarrCommand : ServiceCommand var lister = container.Resolve(); var log = container.Resolve(); - var configLoader = container.Resolve>(); var guideService = container.Resolve(); if (ListCustomFormats) @@ -49,7 +48,9 @@ internal class RadarrCommand : ServiceCommand return; } - foreach (var config in configLoader.LoadMany(Config, "radarr")) + var configFinder = container.Resolve(); + var configLoader = container.Resolve>(); + foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "radarr")) { await using var scope = container.BeginLifetimeScope(builder => { diff --git a/src/Recyclarr/Command/ServiceCommand.cs b/src/Recyclarr/Command/ServiceCommand.cs index 10ae134f..0b3b9871 100644 --- a/src/Recyclarr/Command/ServiceCommand.cs +++ b/src/Recyclarr/Command/ServiceCommand.cs @@ -1,4 +1,3 @@ -using System.IO.Abstractions; using System.Text; using Autofac; using CliFx.Attributes; @@ -14,7 +13,6 @@ using Serilog; using TrashLib.Config.Settings; using TrashLib.Extensions; using TrashLib.Repo; -using TrashLib.Startup; using YamlDotNet.Core; namespace Recyclarr.Command; @@ -30,7 +28,7 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand "One or more YAML config files to use. All configs will be used and settings are additive. " + "If not specified, the script will look for `recyclarr.yml` in the same directory as the executable.")] // ReSharper disable once MemberCanBeProtected.Global - public ICollection Config { get; [UsedImplicitly] set; } = new List(); + public IReadOnlyCollection Config { get; [UsedImplicitly] set; } = new List(); [CommandOption("app-data", Description = "Explicitly specify the location of the recyclarr application data directory. " + @@ -70,29 +68,11 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand } } - private static ICollection FindAllConfigFiles(IAppPaths paths) - { - var configs = new List(); - - if (paths.ConfigsDirectory.Exists) - { - configs.AddRange(paths.ConfigsDirectory.EnumerateFiles("*.yml").Select(x => x.FullName)); - } - - if (paths.ConfigPath.Exists) - { - configs.Add(paths.ConfigPath.FullName); - } - - return configs; - } - public override Task Process(ILifetimeScope container) { var log = container.Resolve(); var settingsProvider = container.Resolve(); var repoUpdater = container.Resolve(); - var paths = container.Resolve(); var migration = container.Resolve(); // Will throw if migration is required, otherwise just a warning is issued. @@ -101,29 +81,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand SetupHttp(log, settingsProvider); repoUpdater.UpdateRepo(); - if (!Config.Any()) - { - Config = FindAllConfigFiles(paths); - } - else - { - var fs = container.Resolve(); - var split = Config.ToLookup(x => fs.File.Exists(x)); - foreach (var nonExistentConfig in split[false]) - { - log.Warning("Configuration file does not exist {File}", nonExistentConfig); - } - - Config = split[true].ToList(); - } - - if (Config.Count == 0) - { - throw new CommandException("No configuration YAML files found"); - } - - log.Debug("Using config files: {ConfigFiles}", Config); - return Task.CompletedTask; } diff --git a/src/Recyclarr/Command/SonarrCommand.cs b/src/Recyclarr/Command/SonarrCommand.cs index 32a81da7..bbab7324 100644 --- a/src/Recyclarr/Command/SonarrCommand.cs +++ b/src/Recyclarr/Command/SonarrCommand.cs @@ -47,7 +47,6 @@ public class SonarrCommand : ServiceCommand await base.Process(container); var lister = container.Resolve(); - var configLoader = container.Resolve>(); var log = container.Resolve(); var guideService = container.Resolve(); @@ -84,7 +83,9 @@ public class SonarrCommand : ServiceCommand return; } - foreach (var config in configLoader.LoadMany(Config, "sonarr")) + var configFinder = container.Resolve(); + var configLoader = container.Resolve>(); + foreach (var config in configLoader.LoadMany(configFinder.GetConfigFiles(Config), "sonarr")) { await using var scope = container.BeginLifetimeScope(builder => { diff --git a/src/Recyclarr/CompositionRoot.cs b/src/Recyclarr/CompositionRoot.cs index 6def0834..6a9d3329 100644 --- a/src/Recyclarr/CompositionRoot.cs +++ b/src/Recyclarr/CompositionRoot.cs @@ -81,6 +81,7 @@ public static class CompositionRoot builder.RegisterModule(); builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterGeneric(typeof(ConfigurationLoader<>)) .WithProperty(new AutowiringParameter()) diff --git a/src/Recyclarr/Config/ConfigurationFinder.cs b/src/Recyclarr/Config/ConfigurationFinder.cs new file mode 100644 index 00000000..7c92f290 --- /dev/null +++ b/src/Recyclarr/Config/ConfigurationFinder.cs @@ -0,0 +1,63 @@ +using System.IO.Abstractions; +using CliFx.Exceptions; +using Serilog; +using TrashLib.Startup; + +namespace Recyclarr.Config; + +public class ConfigurationFinder : IConfigurationFinder +{ + private readonly IAppPaths _paths; + private readonly IFileSystem _fs; + private readonly ILogger _log; + + public ConfigurationFinder(IAppPaths paths, IFileSystem fs, ILogger log) + { + _paths = paths; + _fs = fs; + _log = log; + } + + private IReadOnlyCollection FindDefaultConfigFiles() + { + var configs = new List(); + + if (_paths.ConfigsDirectory.Exists) + { + configs.AddRange(_paths.ConfigsDirectory.EnumerateFiles("*.yml").Select(x => x.FullName)); + } + + if (_paths.ConfigPath.Exists) + { + configs.Add(_paths.ConfigPath.FullName); + } + + return configs; + } + + public IReadOnlyCollection GetConfigFiles(IReadOnlyCollection? configs) + { + if (configs is null || !configs.Any()) + { + configs = FindDefaultConfigFiles(); + } + else + { + var split = configs.ToLookup(x => _fs.File.Exists(x)); + foreach (var nonExistentConfig in split[false]) + { + _log.Warning("Configuration file does not exist {File}", nonExistentConfig); + } + + configs = split[true].ToList(); + } + + if (configs.Count == 0) + { + throw new CommandException("No configuration YAML files found"); + } + + _log.Debug("Using config files: {ConfigFiles}", configs); + return configs; + } +} diff --git a/src/Recyclarr/Config/IConfigurationFinder.cs b/src/Recyclarr/Config/IConfigurationFinder.cs new file mode 100644 index 00000000..20abeb55 --- /dev/null +++ b/src/Recyclarr/Config/IConfigurationFinder.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.Config; + +public interface IConfigurationFinder +{ + IReadOnlyCollection GetConfigFiles(IReadOnlyCollection? configs); +} diff --git a/src/TestLibrary/AutoFixture/InlineAutoMockDataAttribute.cs b/src/TestLibrary/AutoFixture/InlineAutoMockDataAttribute.cs index e7b40f45..8c3df61c 100644 --- a/src/TestLibrary/AutoFixture/InlineAutoMockDataAttribute.cs +++ b/src/TestLibrary/AutoFixture/InlineAutoMockDataAttribute.cs @@ -7,7 +7,7 @@ public sealed class InlineAutoMockDataAttribute : InlineAutoDataAttribute { [SuppressMessage("Design", "CA1019", MessageId = "Define accessors for attribute arguments", Justification = "The parameter is forwarded to the base class and not used directly")] - public InlineAutoMockDataAttribute(params object[] parameters) + public InlineAutoMockDataAttribute(params object?[] parameters) : base(NSubstituteFixture.Create, parameters) { }