parent
67b2166d8b
commit
ee377e55fa
@ -0,0 +1,20 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="config create -t" type="DotNetProject" factoryName=".NET Project">
|
||||||
|
<option name="EXE_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0/recyclarr.exe" />
|
||||||
|
<option name="PROGRAM_PARAMETERS" value="config create -t anime-radarr -f" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Recyclarr.Cli/bin/Debug/net7.0" />
|
||||||
|
<option name="PASS_PARENT_ENVS" value="1" />
|
||||||
|
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||||
|
<option name="USE_MONO" value="0" />
|
||||||
|
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||||
|
<option name="PROJECT_PATH" value="$PROJECT_DIR$/Recyclarr.Cli/Recyclarr.Cli.csproj" />
|
||||||
|
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
|
||||||
|
<option name="PROJECT_KIND" value="DotNetCore" />
|
||||||
|
<option name="PROJECT_TFM" value="net7.0" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Build" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
@ -0,0 +1,8 @@
|
|||||||
|
namespace Recyclarr.Cli.Console.Settings;
|
||||||
|
|
||||||
|
public interface ICreateConfigSettings
|
||||||
|
{
|
||||||
|
public string? Path { get; }
|
||||||
|
public IReadOnlyCollection<string> Templates { get; }
|
||||||
|
public bool Force { get; }
|
||||||
|
}
|
@ -1,46 +1,25 @@
|
|||||||
using System.IO.Abstractions;
|
using Recyclarr.Cli.Console.Settings;
|
||||||
using Recyclarr.Common;
|
|
||||||
using Recyclarr.TrashLib.ExceptionTypes;
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
using Recyclarr.TrashLib.Startup;
|
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Processors.Config;
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
public class ConfigCreationProcessor : IConfigCreationProcessor
|
public class ConfigCreationProcessor : IConfigCreationProcessor
|
||||||
{
|
{
|
||||||
private readonly ILogger _log;
|
private readonly IOrderedEnumerable<IConfigCreator> _creators;
|
||||||
private readonly IAppPaths _paths;
|
|
||||||
private readonly IFileSystem _fs;
|
|
||||||
private readonly IResourceDataReader _resources;
|
|
||||||
|
|
||||||
public ConfigCreationProcessor(
|
public ConfigCreationProcessor(IOrderedEnumerable<IConfigCreator> creators)
|
||||||
ILogger log,
|
|
||||||
IAppPaths paths,
|
|
||||||
IFileSystem fs,
|
|
||||||
IResourceDataReader resources)
|
|
||||||
{
|
{
|
||||||
_log = log;
|
_creators = creators;
|
||||||
_paths = paths;
|
|
||||||
_fs = fs;
|
|
||||||
_resources = resources;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Process(string? configFilePath)
|
public async Task Process(ICreateConfigSettings settings)
|
||||||
{
|
{
|
||||||
var configFile = configFilePath is null
|
var creator = _creators.FirstOrDefault(x => x.CanHandle(settings));
|
||||||
? _paths.AppDataDirectory.File("recyclarr.yml")
|
if (creator is null)
|
||||||
: _fs.FileInfo.New(configFilePath);
|
|
||||||
|
|
||||||
if (configFile.Exists)
|
|
||||||
{
|
{
|
||||||
throw new FileExistsException(configFile.FullName);
|
throw new FatalException("Unable to determine which config creation logic to use");
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile.Directory?.Create();
|
await creator.Create(settings);
|
||||||
await using var stream = configFile.CreateText();
|
|
||||||
|
|
||||||
var ymlData = _resources.ReadData("config-template.yml");
|
|
||||||
await stream.WriteAsync(ymlData);
|
|
||||||
|
|
||||||
_log.Information("Created configuration at: {Path}", configFile.FullName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// This was originally intended to be used by `config create`, but YamlDotNet cannot serialize
|
||||||
|
/// comments so this
|
||||||
|
/// class was not used. I kept it around in case I want to revisit later. There might be an
|
||||||
|
/// opportunity to use this
|
||||||
|
/// with the GUI.
|
||||||
|
/// </remarks>
|
||||||
|
public class ConfigManipulator : IConfigManipulator
|
||||||
|
{
|
||||||
|
private readonly IAnsiConsole _console;
|
||||||
|
private readonly ConfigParser _configParser;
|
||||||
|
private readonly ConfigSaver _configSaver;
|
||||||
|
private readonly ConfigValidationExecutor _validator;
|
||||||
|
|
||||||
|
public ConfigManipulator(
|
||||||
|
IAnsiConsole console,
|
||||||
|
ConfigParser configParser,
|
||||||
|
ConfigSaver configSaver,
|
||||||
|
ConfigValidationExecutor validator)
|
||||||
|
{
|
||||||
|
_console = console;
|
||||||
|
_configParser = configParser;
|
||||||
|
_configSaver = configSaver;
|
||||||
|
_validator = validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, TConfig> InvokeCallbackForEach<TConfig>(
|
||||||
|
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback,
|
||||||
|
IReadOnlyDictionary<string, TConfig>? configs)
|
||||||
|
where TConfig : ServiceConfigYaml
|
||||||
|
{
|
||||||
|
var newConfigs = new Dictionary<string, TConfig>();
|
||||||
|
|
||||||
|
if (configs is null)
|
||||||
|
{
|
||||||
|
return newConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (instanceName, config) in configs)
|
||||||
|
{
|
||||||
|
newConfigs[instanceName] = (TConfig) editCallback(instanceName, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadAndSave(
|
||||||
|
IFileInfo source,
|
||||||
|
IFileInfo destinationFile,
|
||||||
|
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback)
|
||||||
|
{
|
||||||
|
// Parse & save the template file to address the following:
|
||||||
|
// - Find & report any syntax errors
|
||||||
|
// - Run validation & report issues
|
||||||
|
// - Consistently reformat the output file (when it is saved again)
|
||||||
|
// - Ignore stuff for diffing purposes, such as comments.
|
||||||
|
var config = _configParser.Load(source);
|
||||||
|
if (config is null)
|
||||||
|
{
|
||||||
|
// Do not log here, since ConfigParser already has substantial logging
|
||||||
|
throw new FileLoadException("Problem while loading config template");
|
||||||
|
}
|
||||||
|
|
||||||
|
config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = InvokeCallbackForEach(editCallback, config.Radarr),
|
||||||
|
Sonarr = InvokeCallbackForEach(editCallback, config.Sonarr)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!_validator.Validate(config))
|
||||||
|
{
|
||||||
|
_console.WriteLine(
|
||||||
|
"The configuration file will still be created, despite the previous validation errors. " +
|
||||||
|
"You must open the file and correct the above issues before running a sync command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_configSaver.Save(config, destinationFile);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Processors.Config;
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
public interface IConfigCreationProcessor
|
public interface IConfigCreationProcessor
|
||||||
{
|
{
|
||||||
Task Process(string? configFilePath);
|
Task Process(ICreateConfigSettings settings);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
|
public interface IConfigCreator
|
||||||
|
{
|
||||||
|
bool CanHandle(ICreateConfigSettings settings);
|
||||||
|
Task Create(ICreateConfigSettings settings);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
|
public interface IConfigManipulator
|
||||||
|
{
|
||||||
|
void LoadAndSave(
|
||||||
|
IFileInfo source,
|
||||||
|
IFileInfo destinationFile,
|
||||||
|
Func<string, ServiceConfigYaml, ServiceConfigYaml> editCallback);
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
using Recyclarr.Common;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Startup;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
|
public class LocalConfigCreator : IConfigCreator
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly IAppPaths _paths;
|
||||||
|
private readonly IFileSystem _fs;
|
||||||
|
private readonly IResourceDataReader _resources;
|
||||||
|
|
||||||
|
public LocalConfigCreator(ILogger log, IAppPaths paths, IFileSystem fs, IResourceDataReader resources)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_paths = paths;
|
||||||
|
_fs = fs;
|
||||||
|
_resources = resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(ICreateConfigSettings settings)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Create(ICreateConfigSettings settings)
|
||||||
|
{
|
||||||
|
var configFile = settings.Path is null
|
||||||
|
? _paths.AppDataDirectory.File("recyclarr.yml")
|
||||||
|
: _fs.FileInfo.New(settings.Path);
|
||||||
|
|
||||||
|
if (configFile.Exists)
|
||||||
|
{
|
||||||
|
throw new FileExistsException(configFile.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile.CreateParentDirectory();
|
||||||
|
await using var stream = configFile.CreateText();
|
||||||
|
|
||||||
|
var ymlData = _resources.ReadData("config-template.yml");
|
||||||
|
await stream.WriteAsync(ymlData);
|
||||||
|
|
||||||
|
_log.Information("Created configuration at: {Path}", configFile.FullName);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Startup;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Processors.Config;
|
||||||
|
|
||||||
|
public class TemplateConfigCreator : IConfigCreator
|
||||||
|
{
|
||||||
|
private readonly ILogger _log;
|
||||||
|
private readonly IConfigTemplateGuideService _templates;
|
||||||
|
|
||||||
|
private readonly IAppPaths _paths;
|
||||||
|
// private readonly IConfigManipulator _configManipulator;
|
||||||
|
// private readonly IAnsiConsole _console;
|
||||||
|
|
||||||
|
public TemplateConfigCreator(
|
||||||
|
ILogger log,
|
||||||
|
IConfigTemplateGuideService templates,
|
||||||
|
IAppPaths paths)
|
||||||
|
{
|
||||||
|
_log = log;
|
||||||
|
_templates = templates;
|
||||||
|
_paths = paths;
|
||||||
|
// _configManipulator = configManipulator;
|
||||||
|
// _console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(ICreateConfigSettings settings)
|
||||||
|
{
|
||||||
|
return settings.Templates.Any();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
|
||||||
|
public Task Create(ICreateConfigSettings settings)
|
||||||
|
{
|
||||||
|
_log.Debug("Creating config from templates: {Templates}", settings.Templates);
|
||||||
|
|
||||||
|
var matchingTemplateData = _templates.LoadTemplateData()
|
||||||
|
.IntersectBy(settings.Templates, path => path.Id, StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
.Select(x => x.TemplateFile);
|
||||||
|
|
||||||
|
foreach (var templateFile in matchingTemplateData)
|
||||||
|
{
|
||||||
|
var destinationFile = _paths.ConfigsDirectory.File(templateFile.Name);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (destinationFile.Exists && !settings.Force)
|
||||||
|
{
|
||||||
|
throw new FileExistsException($"{destinationFile} already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationFile.CreateParentDirectory();
|
||||||
|
templateFile.CopyTo(destinationFile.FullName, true);
|
||||||
|
|
||||||
|
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
|
||||||
|
if (destinationFile.Exists)
|
||||||
|
{
|
||||||
|
_log.Information("Replacing existing file: {Path}", destinationFile);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_log.Information("Created configuration file: {Path}", destinationFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- See comment in ConfigManipulator.cs --
|
||||||
|
// _configManipulator.LoadAndSave(templateFile, destinationFile, (instanceName, config) =>
|
||||||
|
// {
|
||||||
|
// _console.MarkupLineInterpolated($"Enter configuration info for instance [green]{instanceName}[/]:");
|
||||||
|
// var baseUrl = _console.Prompt(new TextPrompt<string>("Base URL:"));
|
||||||
|
// var apiKey = _console.Prompt(new TextPrompt<string>("API Key:"));
|
||||||
|
// return config with
|
||||||
|
// {
|
||||||
|
// BaseUrl = baseUrl,
|
||||||
|
// ApiKey = apiKey
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
catch (FileExistsException e)
|
||||||
|
{
|
||||||
|
_log.Error("Template configuration file could not be saved: {Reason}", e.AttemptedPath);
|
||||||
|
}
|
||||||
|
catch (FileLoadException)
|
||||||
|
{
|
||||||
|
// Do not log here since the origin of this exception is ConfigParser.Load(), which already has
|
||||||
|
// sufficient logging.
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_log.Error(e, "Unable to save configuration template file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Yaml;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
|
||||||
|
public class ConfigSaver
|
||||||
|
{
|
||||||
|
private readonly IYamlSerializerFactory _serializerFactory;
|
||||||
|
|
||||||
|
public ConfigSaver(IYamlSerializerFactory serializerFactory)
|
||||||
|
{
|
||||||
|
_serializerFactory = serializerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(RootConfigYaml config, IFileInfo destinationFile)
|
||||||
|
{
|
||||||
|
var serializer = _serializerFactory.CreateSerializer();
|
||||||
|
|
||||||
|
destinationFile.CreateParentDirectory();
|
||||||
|
using var stream = destinationFile.CreateText();
|
||||||
|
serializer.Serialize(stream, config);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using System.Runtime.Serialization;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class FatalException : Exception
|
||||||
|
{
|
||||||
|
public FatalException(string? message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
protected FatalException(SerializationInfo info, StreamingContext context)
|
||||||
|
: base(info, context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.IO.Abstractions.Extensions;
|
||||||
|
using Recyclarr.Cli.Processors.Config;
|
||||||
|
using Recyclarr.Cli.TestLibrary;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Tests.Processors.Config;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ConfigManipulatorTest : CliIntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Create_file_when_no_file_already_exists()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigManipulator>();
|
||||||
|
var src = Fs.CurrentDirectory().File("template.yml");
|
||||||
|
var dst = Fs.CurrentDirectory().SubDir("one", "two", "three").File("config.yml");
|
||||||
|
|
||||||
|
const string yamlData = @"
|
||||||
|
sonarr:
|
||||||
|
instance1:
|
||||||
|
base_url: http://localhost:80
|
||||||
|
api_key: 123abc
|
||||||
|
";
|
||||||
|
|
||||||
|
Fs.AddFile(src, new MockFileData(yamlData));
|
||||||
|
|
||||||
|
sut.LoadAndSave(src, dst, (_, yaml) => yaml);
|
||||||
|
|
||||||
|
Fs.AllFiles.Should().Contain(dst.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Throw_on_invalid_yaml()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigManipulator>();
|
||||||
|
var src = Fs.CurrentDirectory().File("template.yml");
|
||||||
|
var dst = Fs.CurrentDirectory().File("config.yml");
|
||||||
|
|
||||||
|
const string yamlData = @"
|
||||||
|
sonarr:
|
||||||
|
instance1:
|
||||||
|
invalid: yaml
|
||||||
|
";
|
||||||
|
|
||||||
|
Fs.AddFile(src, new MockFileData(yamlData));
|
||||||
|
|
||||||
|
var act = () => sut.LoadAndSave(src, dst, (_, yaml) => yaml);
|
||||||
|
|
||||||
|
act.Should().Throw<FileLoadException>();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.IO.Abstractions.Extensions;
|
||||||
|
using Recyclarr.Cli.Console.Settings;
|
||||||
|
using Recyclarr.Cli.Processors.Config;
|
||||||
|
using Recyclarr.Cli.TestLibrary;
|
||||||
|
using Recyclarr.TestLibrary.AutoFixture;
|
||||||
|
using Recyclarr.TrashLib.Config;
|
||||||
|
using Recyclarr.TrashLib.Config.Services;
|
||||||
|
using Recyclarr.TrashLib.ExceptionTypes;
|
||||||
|
using Recyclarr.TrashLib.Repo;
|
||||||
|
|
||||||
|
namespace Recyclarr.Cli.Tests.Processors.Config;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class TemplateConfigCreatorTest : CliIntegrationFixture
|
||||||
|
{
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Can_handle_returns_true_with_templates(
|
||||||
|
ICreateConfigSettings settings,
|
||||||
|
TemplateConfigCreator sut)
|
||||||
|
{
|
||||||
|
settings.Templates.Returns(new[] {"template1"});
|
||||||
|
var result = sut.CanHandle(settings);
|
||||||
|
result.Should().Be(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Can_handle_returns_false_with_no_templates(
|
||||||
|
ICreateConfigSettings settings,
|
||||||
|
TemplateConfigCreator sut)
|
||||||
|
{
|
||||||
|
settings.Templates.Returns(Array.Empty<string>());
|
||||||
|
var result = sut.CanHandle(settings);
|
||||||
|
result.Should().Be(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void Throw_when_file_exists_and_not_forced(
|
||||||
|
[Frozen] IConfigTemplateGuideService templates,
|
||||||
|
MockFileSystem fs,
|
||||||
|
ICreateConfigSettings settings,
|
||||||
|
TemplateConfigCreator sut)
|
||||||
|
{
|
||||||
|
templates.LoadTemplateData().Returns(new[]
|
||||||
|
{
|
||||||
|
new TemplatePath
|
||||||
|
{
|
||||||
|
Id = "template1",
|
||||||
|
TemplateFile = fs.CurrentDirectory().File("template-file1.yml"),
|
||||||
|
Service = SupportedServices.Radarr
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.Force.Returns(false);
|
||||||
|
settings.Templates.Returns(new[]
|
||||||
|
{
|
||||||
|
"template1"
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = () => sut.Create(settings);
|
||||||
|
|
||||||
|
act.Should().ThrowAsync<FileExistsException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test, AutoMockData]
|
||||||
|
public void No_throw_when_file_exists_and_forced(
|
||||||
|
[Frozen] IConfigTemplateGuideService templates,
|
||||||
|
MockFileSystem fs,
|
||||||
|
ICreateConfigSettings settings,
|
||||||
|
TemplateConfigCreator sut)
|
||||||
|
{
|
||||||
|
templates.LoadTemplateData().Returns(new[]
|
||||||
|
{
|
||||||
|
new TemplatePath
|
||||||
|
{
|
||||||
|
Id = "template1",
|
||||||
|
TemplateFile = fs.CurrentDirectory().File("template-file1.yml"),
|
||||||
|
Service = SupportedServices.Radarr
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.Force.Returns(true);
|
||||||
|
settings.Templates.Returns(new[]
|
||||||
|
{
|
||||||
|
"template1"
|
||||||
|
});
|
||||||
|
|
||||||
|
var act = () => sut.Create(settings);
|
||||||
|
|
||||||
|
act.Should().NotThrowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Template_id_matching_works()
|
||||||
|
{
|
||||||
|
const string templatesJson = @"
|
||||||
|
{
|
||||||
|
'radarr': [
|
||||||
|
{
|
||||||
|
'template': 'template-file1.yml',
|
||||||
|
'id': 'template1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'sonarr': [
|
||||||
|
{
|
||||||
|
'template': 'template-file2.yml',
|
||||||
|
'id': 'template2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'template': 'template-file3.yml',
|
||||||
|
'id': 'template3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var repo = Resolve<IConfigTemplatesRepo>();
|
||||||
|
Fs.AddFile(repo.Path.File("templates.json"), new MockFileData(templatesJson));
|
||||||
|
Fs.AddEmptyFile(repo.Path.File("template-file1.yml"));
|
||||||
|
Fs.AddEmptyFile(repo.Path.File("template-file2.yml"));
|
||||||
|
// This one shouldn't show up in the result because the user didn't ask for it
|
||||||
|
Fs.AddEmptyFile(repo.Path.File("template-file3.yml"));
|
||||||
|
|
||||||
|
var settings = Substitute.For<ICreateConfigSettings>();
|
||||||
|
settings.Templates.Returns(new[]
|
||||||
|
{
|
||||||
|
"template1",
|
||||||
|
"template2",
|
||||||
|
// This one shouldn't show up in the results because:
|
||||||
|
// User specified it, but no template file exists for it.
|
||||||
|
"template4"
|
||||||
|
});
|
||||||
|
|
||||||
|
var sut = Resolve<TemplateConfigCreator>();
|
||||||
|
sut.Create(settings);
|
||||||
|
|
||||||
|
Fs.AllFiles.Should().Contain(new[]
|
||||||
|
{
|
||||||
|
Paths.ConfigsDirectory.File("template-file1.yml").FullName,
|
||||||
|
Paths.ConfigsDirectory.File("template-file2.yml").FullName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using AutoFixture;
|
||||||
|
|
||||||
|
namespace Recyclarr.TestLibrary.AutoFixture;
|
||||||
|
|
||||||
|
// Based on the answer here: https://stackoverflow.com/a/16735551/157971
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
|
||||||
|
public sealed class CustomizeWithAttribute : CustomizeAttribute
|
||||||
|
{
|
||||||
|
public Type CustomizationType { get; }
|
||||||
|
|
||||||
|
public CustomizeWithAttribute(Type customizationType)
|
||||||
|
{
|
||||||
|
if (customizationType == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(customizationType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!typeof(ICustomization).IsAssignableFrom(customizationType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Type needs to implement ICustomization");
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomizationType = customizationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ICustomization? GetCustomization(ParameterInfo parameter)
|
||||||
|
{
|
||||||
|
return (ICustomization?) Activator.CreateInstance(CustomizationType);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
using Serilog.Sinks.TestCorrelator;
|
||||||
|
|
||||||
|
namespace Recyclarr.TestLibrary;
|
||||||
|
|
||||||
|
public sealed class TestableLogger : ILogger, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Logger _log;
|
||||||
|
private ITestCorrelatorContext _logContext;
|
||||||
|
|
||||||
|
public TestableLogger()
|
||||||
|
{
|
||||||
|
_log = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Is(LogEventLevel.Verbose)
|
||||||
|
.WriteTo.TestCorrelator()
|
||||||
|
.WriteTo.Console()
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
_logContext = TestCorrelator.CreateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
_log.Write(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_logContext.Dispose();
|
||||||
|
_log.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetCapturedLogs()
|
||||||
|
{
|
||||||
|
_logContext.Dispose();
|
||||||
|
_logContext = TestCorrelator.CreateContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetRenderedMessages()
|
||||||
|
{
|
||||||
|
return TestCorrelator.GetLogEventsFromContextGuid(_logContext.Guid)
|
||||||
|
.Select(x => x.RenderMessage());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
using System.IO.Abstractions;
|
||||||
|
using System.IO.Abstractions.Extensions;
|
||||||
|
using Recyclarr.Common.Extensions;
|
||||||
|
using Recyclarr.TrashLib.Config.Parsing;
|
||||||
|
using Recyclarr.TrashLib.TestLibrary;
|
||||||
|
|
||||||
|
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
[Parallelizable(ParallelScope.All)]
|
||||||
|
public class ConfigSaverTest : TrashLibIntegrationFixture
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Replace_file_when_already_exists()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigSaver>();
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"instance1", new RadarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "apikey"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var destFile = Fs.CurrentDirectory().File("config.yml");
|
||||||
|
Fs.AddEmptyFile(destFile);
|
||||||
|
|
||||||
|
sut.Save(config, destFile);
|
||||||
|
|
||||||
|
Fs.GetFile(destFile).TextContents.Should().Contain("apikey");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Create_intermediate_directories()
|
||||||
|
{
|
||||||
|
var sut = Resolve<ConfigSaver>();
|
||||||
|
|
||||||
|
var config = new RootConfigYaml
|
||||||
|
{
|
||||||
|
Radarr = new Dictionary<string, RadarrConfigYaml>
|
||||||
|
{
|
||||||
|
{
|
||||||
|
"instance1", new RadarrConfigYaml
|
||||||
|
{
|
||||||
|
ApiKey = "apikey",
|
||||||
|
BaseUrl = "http://baseurl.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var destFile = Fs.CurrentDirectory().SubDir("one", "two", "three").File("config.yml");
|
||||||
|
|
||||||
|
sut.Save(config, destFile);
|
||||||
|
|
||||||
|
var expectedYaml = @"
|
||||||
|
radarr:
|
||||||
|
instance1:
|
||||||
|
api_key: apikey
|
||||||
|
base_url: http://baseurl.com
|
||||||
|
".TrimStart();
|
||||||
|
|
||||||
|
var expectedFile = Fs.GetFile(destFile);
|
||||||
|
expectedFile.Should().NotBeNull();
|
||||||
|
expectedFile.TextContents.Should().Be(expectedYaml);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue