refactor: Change the design pattern for settings access

Simplify the factory pattern so that it creates settings more directly
instead of using a silly setter method.

Also implemented `IntegrationFixture` for easier integration testing.
pull/124/head
Robert Dailey 2 years ago
parent 1f70e7fb61
commit f01edc5700

@ -0,0 +1,37 @@
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
using Autofac;
using CliFx.Infrastructure;
using NUnit.Framework;
using Serilog.Events;
namespace Recyclarr.TestLibrary;
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract class IntegrationFixture
{
private readonly ILifetimeScope _container;
protected IntegrationFixture()
{
var compRoot = new CompositionRoot();
_container = compRoot.Setup(default, new FakeConsole(), LogEventLevel.Debug).Container
.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(Fs).As<IFileSystem>();
});
}
protected MockFileSystem Fs { get; } = new();
protected T Resolve<T>(Action<ContainerBuilder> customRegistrations) where T : notnull
{
var childScope = _container.BeginLifetimeScope(customRegistrations);
return childScope.Resolve<T>();
}
protected T Resolve<T>() where T : notnull
{
return _container.Resolve<T>();
}
}

@ -2,4 +2,8 @@
<ItemGroup>
<ProjectReference Include="..\Recyclarr\Recyclarr.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NUnit" PrivateAssets="All" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" />
</ItemGroup>
</Project>

@ -0,0 +1,33 @@
using System.IO.Abstractions.TestingHelpers;
using FluentAssertions;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using TrashLib.Config.Settings;
using TrashLib.Startup;
namespace Recyclarr.Tests;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ServiceCompatibilityIntegrationTest : IntegrationFixture
{
[Test]
public void Load_data_correctly_when_file_exists()
{
var sut = Resolve<ISettingsProvider>();
var paths = Resolve<IAppPaths>();
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects;
// this test only ensures that the data deserialized is from the actual correct file.
const string yamlData = @"
repository:
clone_url: http://the_url.com
";
Fs.AddFile(paths.SettingsPath.FullName, new MockFileData(yamlData));
var settings = sut.Settings;
settings.Repository.CloneUrl.Should().Be("http://the_url.com");
}
}

@ -65,7 +65,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
public override Task Process(IServiceLocatorProxy container)
{
var log = container.Resolve<ILogger>();
var settingsPersister = container.Resolve<ISettingsPersister>();
var settingsProvider = container.Resolve<ISettingsProvider>();
var repoUpdater = container.Resolve<IRepoUpdater>();
var configFinder = container.Resolve<IConfigurationFinder>();
@ -77,9 +76,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
// Will throw if migration is required, otherwise just a warning is issued.
migration.CheckNeededMigrations();
// Stuff below may use settings.
settingsPersister.Load();
SetupHttp(log, settingsProvider);
repoUpdater.UpdateRepo();

@ -24,7 +24,7 @@ using YamlDotNet.Serialization;
namespace Recyclarr;
internal class CompositionRoot : ICompositionRoot
public class CompositionRoot : ICompositionRoot
{
public IServiceLocatorProxy Setup(string? appDataDir, IConsole console, LogEventLevel logLevel)
{

@ -23,7 +23,7 @@ public sealed class AutoMockDataAttribute : AutoDataAttribute
{
var method = testFixtureClass.GetMethod(methodName,
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
var getContainer = method?.CreateDelegate<Func<IContainer>>();
var getContainer = method?.CreateDelegate<Func<ILifetimeScope>>();
if (getContainer is null)
{
throw new ArgumentException("Unable to find method on test fixture. Method must be non-public to be found.",

@ -6,9 +6,9 @@ namespace TestLibrary.AutoFixture;
public class AutofacSpecimenBuilder : ISpecimenBuilder
{
private readonly IContainer _container;
private readonly ILifetimeScope _container;
public AutofacSpecimenBuilder(IContainer container)
public AutofacSpecimenBuilder(ILifetimeScope container)
{
_container = container;
}

@ -1,13 +1,11 @@
using System.IO.Abstractions.TestingHelpers;
using AutoFixture.NUnit3;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config;
using TrashLib.Config.Settings;
using TrashLib.TestLibrary;
using YamlDotNet.Serialization;
using TrashLib.Startup;
namespace TrashLib.Tests.Config.Settings;
@ -17,45 +15,25 @@ public class SettingsPersisterTest
{
[Test, AutoMockData]
public void Load_should_create_settings_file_if_not_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
SettingsPersister sut)
[Frozen] MockFileSystem fileSystem,
[Frozen] IAppPaths paths,
SettingsProvider sut)
{
sut.Load();
var settings = sut.Settings;
fileSystem.AllFiles.Should().ContainSingle(x => x == paths.SettingsPath.FullName);
fileSystem.AllFiles.Should().ContainSingle(paths.SettingsPath.FullName);
}
[Test, AutoMockData]
public void Load_defaults_when_file_does_not_exist(
[Frozen(Matching.ImplementedInterfaces)] YamlSerializerFactory serializerFactory,
[Frozen(Matching.ImplementedInterfaces)] SettingsProvider settingsProvider,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
SettingsPersister sut)
[Frozen] IAppPaths paths,
SettingsProvider sut)
{
sut.Load();
var expectedSettings = new SettingsValues();
settingsProvider.Settings.Should().BeEquivalentTo(expectedSettings);
}
[Test, AutoMockData]
public void Load_data_correctly_when_file_exists(
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fileSystem,
[Frozen(Matching.ImplementedInterfaces)] TestAppPaths paths,
[Frozen] IDeserializer deserializer,
SettingsPersister sut)
{
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects;
// this test only ensures that the data deserialized is from the actual correct file.
var expectedYamlData = @"
repository:
clone_url: http://the_url.com
";
fileSystem.AddFile(paths.SettingsPath.FullName, new MockFileData(expectedYamlData));
sut.Load();
var settings = sut.Settings;
deserializer.Received().Deserialize<SettingsValues>(expectedYamlData);
settings.Should().BeEquivalentTo(expectedSettings);
}
}

@ -1,19 +0,0 @@
using FluentAssertions;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config.Settings;
namespace TrashLib.Tests.Config.Settings;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class SettingsProviderTest
{
[Test, AutoMockData]
public void Property_returns_same_value_from_set_method(SettingsProvider sut)
{
var settings = new SettingsValues();
sut.UseSettings(settings);
sut.Settings.Should().Be(settings);
}
}

@ -18,6 +18,5 @@ public class ConfigAutofacModule : Module
builder.RegisterType<ConfigurationProvider>().As<IConfigurationProvider>().InstancePerLifetimeScope();
builder.RegisterType<SettingsProvider>().As<ISettingsProvider>().InstancePerLifetimeScope();
builder.RegisterType<YamlSerializerFactory>().As<IYamlSerializerFactory>();
builder.RegisterType<SettingsPersister>().As<ISettingsPersister>();
}
}

@ -1,6 +0,0 @@
namespace TrashLib.Config.Settings;
public interface ISettingsPersister
{
void Load();
}

@ -3,5 +3,4 @@ namespace TrashLib.Config.Settings;
public interface ISettingsProvider
{
SettingsValues Settings { get; }
void UseSettings(SettingsValues settings);
}

@ -1,52 +0,0 @@
using TrashLib.Startup;
namespace TrashLib.Config.Settings;
public class SettingsPersister : ISettingsPersister
{
private readonly IAppPaths _paths;
private readonly ISettingsProvider _settingsProvider;
private readonly IYamlSerializerFactory _serializerFactory;
public SettingsPersister(
IAppPaths paths,
ISettingsProvider settingsProvider,
IYamlSerializerFactory serializerFactory)
{
_paths = paths;
_settingsProvider = settingsProvider;
_serializerFactory = serializerFactory;
}
public void Load()
{
var deserializer = _serializerFactory.CreateDeserializer();
var settings = deserializer.Deserialize<SettingsValues?>(LoadOrCreateSettingsFile()) ?? new SettingsValues();
_settingsProvider.UseSettings(settings);
}
private string LoadOrCreateSettingsFile()
{
if (!_paths.SettingsPath.Exists)
{
CreateDefaultSettingsFile();
}
using var stream = _paths.SettingsPath.OpenText();
return stream.ReadToEnd();
}
private void CreateDefaultSettingsFile()
{
const string fileData =
"# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/settings-schema.json\n" +
"\n" +
"# Edit this file to customize the behavior of Recyclarr beyond its defaults\n" +
"# For the settings file reference guide, visit the link to the wiki below:\n" +
"# https://github.com/recyclarr/recyclarr/wiki/Settings-Reference\n";
_paths.SettingsPath.Directory.Create();
using var stream = _paths.SettingsPath.CreateText();
stream.Write(fileData);
}
}

@ -1,11 +1,43 @@
using TrashLib.Startup;
namespace TrashLib.Config.Settings;
public class SettingsProvider : ISettingsProvider
{
public SettingsValues Settings { get; private set; } = new();
public SettingsValues Settings => _settings.Value;
private readonly IAppPaths _paths;
private readonly Lazy<SettingsValues> _settings;
public SettingsProvider(IAppPaths paths, IYamlSerializerFactory serializerFactory)
{
_paths = paths;
_settings = new Lazy<SettingsValues>(() => LoadOrCreateSettingsFile(serializerFactory));
}
public void UseSettings(SettingsValues settings)
private SettingsValues LoadOrCreateSettingsFile(IYamlSerializerFactory serializerFactory)
{
Settings = settings;
if (!_paths.SettingsPath.Exists)
{
CreateDefaultSettingsFile();
}
using var stream = _paths.SettingsPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<SettingsValues?>(stream.ReadToEnd()) ?? new SettingsValues();
}
private void CreateDefaultSettingsFile()
{
const string fileData =
"# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/settings-schema.json\n" +
"\n" +
"# Edit this file to customize the behavior of Recyclarr beyond its defaults\n" +
"# For the settings file reference guide, visit the link to the wiki below:\n" +
"# https://github.com/recyclarr/recyclarr/wiki/Settings-Reference\n";
_paths.SettingsPath.Directory.Create();
using var stream = _paths.SettingsPath.CreateText();
stream.Write(fileData);
}
}

Loading…
Cancel
Save