refactor: validation logic for service configuration

recyclarr
Robert Dailey 3 years ago
parent 4d99530e48
commit 08541961bd

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Text;
using Common;
using FluentAssertions;
using JetBrains.Annotations;
using NSubstitute;
using NUnit.Framework;
using TestLibrary;
@ -20,54 +22,51 @@ namespace Trash.Tests.Config
[Parallelizable(ParallelScope.All)]
public class ConfigurationLoaderTest
{
private TextReader GetResourceData(string file)
private static TextReader GetResourceData(string file)
{
var testData = new ResourceDataReader(typeof(ConfigurationLoaderTest), "Data");
return new StringReader(testData.ReadData(file));
}
[Test]
public void Load_UsingStream_CorrectParsing()
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class TestConfigValidFalse : IServiceConfiguration
{
var configLoader = new ConfigurationLoader<SonarrConfiguration>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
public const string Msg = "validate_false";
public string BaseUrl { get; init; } = "";
public string ApiKey { get; init; } = "";
var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr");
public string BuildUrl()
{
throw new NotImplementedException();
}
configs.Should()
.BeEquivalentTo(new List<SonarrConfiguration>
{
new()
{
ApiKey = "95283e6b156c42f3af8a9b16173f876b",
BaseUrl = "http://localhost:8989",
ReleaseProfiles = new List<ReleaseProfileConfig>
{
new()
{
Type = ReleaseProfileType.Anime,
StrictNegativeScores = true,
Tags = new List<string> {"anime"}
},
new()
{
Type = ReleaseProfileType.Series,
StrictNegativeScores = false,
Tags = new List<string>
{
"tv",
"series"
}
}
}
}
});
public bool IsValid(out string msg)
{
msg = Msg;
return false;
}
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class TestConfigValidTrue : IServiceConfiguration
{
public string BaseUrl { get; init; } = "";
public string ApiKey { get; init; } = "";
public string BuildUrl()
{
throw new NotImplementedException();
}
public bool IsValid(out string msg)
{
msg = "";
return true;
}
}
[Test]
public void LoadMany_CorrectNumberOfIterations()
public void Load_many_iterations_of_config()
{
static StreamReader MockYaml(params object[] args)
{
@ -107,7 +106,81 @@ namespace Trash.Tests.Config
var actual = loader.LoadMany(fakeFiles, "sonarr").ToList();
actual.Should().BeEquivalentTo(expected);
actualActiveConfigs.Should().BeEquivalentTo(expected);
actualActiveConfigs.Should().BeEquivalentTo(expected, op => op.WithoutStrictOrdering());
}
[Test]
public void Parse_using_stream()
{
var configLoader = new ConfigurationLoader<SonarrConfiguration>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr");
configs.Should()
.BeEquivalentTo(new List<SonarrConfiguration>
{
new()
{
ApiKey = "95283e6b156c42f3af8a9b16173f876b",
BaseUrl = "http://localhost:8989",
ReleaseProfiles = new List<ReleaseProfileConfig>
{
new()
{
Type = ReleaseProfileType.Anime,
StrictNegativeScores = true,
Tags = new List<string> {"anime"}
},
new()
{
Type = ReleaseProfileType.Series,
StrictNegativeScores = false,
Tags = new List<string>
{
"tv",
"series"
}
}
}
}
});
}
[Test]
public void Validation_failure_throws()
{
var configLoader = new ConfigurationLoader<TestConfigValidFalse>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
var testYml = @"
fubar:
- api_key: abc
";
Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar");
act.Should().Throw<ConfigurationException>()
.WithMessage($"*{TestConfigValidFalse.Msg}");
}
[Test]
public void Validation_success_does_not_throw()
{
var configLoader = new ConfigurationLoader<TestConfigValidTrue>(
Substitute.For<IConfigurationProvider>(),
Substitute.For<IFileSystem>(),
new DefaultObjectFactory());
var testYml = @"
fubar:
- api_key: abc
";
Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar");
act.Should().NotThrow();
}
}
}

@ -25,6 +25,11 @@ namespace Trash.Tests.Config
{
throw new NotImplementedException();
}
public override bool IsValid(out string msg)
{
throw new NotImplementedException();
}
}
[Test]

@ -4,7 +4,9 @@ namespace Trash.Config
{
public class ConfigurationException : Exception
{
public ConfigurationException(string propertyName, Type deserializableType)
public ConfigurationException(string propertyName, Type deserializableType, string msg)
: base($"An exception occurred while deserializing type '{deserializableType}' " +
$"for YML property '{propertyName}': {msg}")
{
PropertyName = propertyName;
DeserializableType = deserializableType;

@ -11,7 +11,7 @@ using YamlDotNet.Serialization.NamingConventions;
namespace Trash.Config
{
public class ConfigurationLoader<T> : IConfigurationLoader<T>
where T : ServiceConfiguration
where T : IServiceConfiguration
{
private readonly IConfigurationProvider _configProvider;
private readonly IDeserializer _deserializer;
@ -43,23 +43,35 @@ namespace Trash.Config
parser.Consume<DocumentStart>();
parser.Consume<MappingStart>();
var configs = new List<T>();
var validConfigs = new List<T>();
while (parser.TryConsume<Scalar>(out var key))
{
if (key.Value == configSection)
{
configs = _deserializer.Deserialize<List<T>>(parser);
var configs = (List<T>?) _deserializer.Deserialize<List<T>>(parser);
if (configs != null)
{
foreach (var config in configs)
{
if (!config.IsValid(out var msg))
{
throw new ConfigurationException(configSection, typeof(T), msg);
}
validConfigs.Add(config);
}
}
}
parser.SkipThisAndNestedEvents();
}
if (configs.Count == 0)
if (validConfigs.Count == 0)
{
throw new ConfigurationException(configSection, typeof(T));
throw new ConfigurationException(configSection, typeof(T), "There are no configured instances defined");
}
return configs;
return validConfigs;
}
public IEnumerable<T> LoadMany(IEnumerable<string> configFiles, string configSection)

@ -4,7 +4,7 @@ using System.IO;
namespace Trash.Config
{
public interface IConfigurationLoader<out T>
where T : ServiceConfiguration
where T : IServiceConfiguration
{
IEnumerable<T> Load(string propertyName, string configSection);
IEnumerable<T> LoadFromStream(TextReader stream, string configSection);

@ -5,5 +5,6 @@ namespace Trash.Config
string BaseUrl { get; init; }
string ApiKey { get; init; }
string BuildUrl();
bool IsValid(out string msg);
}
}

@ -11,5 +11,6 @@ namespace Trash.Config
public string ApiKey { get; init; } = "";
public abstract string BuildUrl();
public abstract bool IsValid(out string msg);
}
}

@ -21,6 +21,12 @@ namespace Trash.Radarr
.AppendPathSegment("api/v3")
.SetQueryParams(new {apikey = ApiKey});
}
public override bool IsValid(out string msg)
{
msg = "";
return true;
}
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]

@ -20,6 +20,12 @@ namespace Trash.Sonarr
.AppendPathSegment("api/v3")
.SetQueryParams(new {apikey = ApiKey});
}
public override bool IsValid(out string msg)
{
msg = "";
return true;
}
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]

Loading…
Cancel
Save