diff --git a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs index 17a7b95f..493dab0b 100644 --- a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs +++ b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs @@ -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( - Substitute.For(), - Substitute.For(), - 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 - { - new() - { - ApiKey = "95283e6b156c42f3af8a9b16173f876b", - BaseUrl = "http://localhost:8989", - ReleaseProfiles = new List - { - new() - { - Type = ReleaseProfileType.Anime, - StrictNegativeScores = true, - Tags = new List {"anime"} - }, - new() - { - Type = ReleaseProfileType.Series, - StrictNegativeScores = false, - Tags = new List - { - "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( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory()); + + var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); + + configs.Should() + .BeEquivalentTo(new List + { + new() + { + ApiKey = "95283e6b156c42f3af8a9b16173f876b", + BaseUrl = "http://localhost:8989", + ReleaseProfiles = new List + { + new() + { + Type = ReleaseProfileType.Anime, + StrictNegativeScores = true, + Tags = new List {"anime"} + }, + new() + { + Type = ReleaseProfileType.Series, + StrictNegativeScores = false, + Tags = new List + { + "tv", + "series" + } + } + } + } + }); + } + + [Test] + public void Validation_failure_throws() + { + var configLoader = new ConfigurationLoader( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory()); + + var testYml = @" +fubar: +- api_key: abc +"; + Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); + + act.Should().Throw() + .WithMessage($"*{TestConfigValidFalse.Msg}"); + } + + [Test] + public void Validation_success_does_not_throw() + { + var configLoader = new ConfigurationLoader( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory()); + + var testYml = @" +fubar: +- api_key: abc +"; + Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); + act.Should().NotThrow(); } } } diff --git a/src/Trash.Tests/Config/ServiceConfigurationTest.cs b/src/Trash.Tests/Config/ServiceConfigurationTest.cs index bc2955c9..f8a3e804 100644 --- a/src/Trash.Tests/Config/ServiceConfigurationTest.cs +++ b/src/Trash.Tests/Config/ServiceConfigurationTest.cs @@ -25,6 +25,11 @@ namespace Trash.Tests.Config { throw new NotImplementedException(); } + + public override bool IsValid(out string msg) + { + throw new NotImplementedException(); + } } [Test] diff --git a/src/Trash/Config/ConfigurationException.cs b/src/Trash/Config/ConfigurationException.cs index f0300548..542e6041 100644 --- a/src/Trash/Config/ConfigurationException.cs +++ b/src/Trash/Config/ConfigurationException.cs @@ -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; diff --git a/src/Trash/Config/ConfigurationLoader.cs b/src/Trash/Config/ConfigurationLoader.cs index 2685abea..de08e353 100644 --- a/src/Trash/Config/ConfigurationLoader.cs +++ b/src/Trash/Config/ConfigurationLoader.cs @@ -11,7 +11,7 @@ using YamlDotNet.Serialization.NamingConventions; namespace Trash.Config { public class ConfigurationLoader : IConfigurationLoader - where T : ServiceConfiguration + where T : IServiceConfiguration { private readonly IConfigurationProvider _configProvider; private readonly IDeserializer _deserializer; @@ -43,23 +43,35 @@ namespace Trash.Config parser.Consume(); parser.Consume(); - var configs = new List(); + var validConfigs = new List(); while (parser.TryConsume(out var key)) { if (key.Value == configSection) { - configs = _deserializer.Deserialize>(parser); + var configs = (List?) _deserializer.Deserialize>(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 LoadMany(IEnumerable configFiles, string configSection) diff --git a/src/Trash/Config/IConfigurationLoader.cs b/src/Trash/Config/IConfigurationLoader.cs index 8326e359..8a361f39 100644 --- a/src/Trash/Config/IConfigurationLoader.cs +++ b/src/Trash/Config/IConfigurationLoader.cs @@ -4,7 +4,7 @@ using System.IO; namespace Trash.Config { public interface IConfigurationLoader - where T : ServiceConfiguration + where T : IServiceConfiguration { IEnumerable Load(string propertyName, string configSection); IEnumerable LoadFromStream(TextReader stream, string configSection); diff --git a/src/Trash/Config/IServiceConfiguration.cs b/src/Trash/Config/IServiceConfiguration.cs index 2df53058..567c5a19 100644 --- a/src/Trash/Config/IServiceConfiguration.cs +++ b/src/Trash/Config/IServiceConfiguration.cs @@ -5,5 +5,6 @@ namespace Trash.Config string BaseUrl { get; init; } string ApiKey { get; init; } string BuildUrl(); + bool IsValid(out string msg); } } diff --git a/src/Trash/Config/ServiceConfiguration.cs b/src/Trash/Config/ServiceConfiguration.cs index cce47bf1..8f931cb7 100644 --- a/src/Trash/Config/ServiceConfiguration.cs +++ b/src/Trash/Config/ServiceConfiguration.cs @@ -11,5 +11,6 @@ namespace Trash.Config public string ApiKey { get; init; } = ""; public abstract string BuildUrl(); + public abstract bool IsValid(out string msg); } } diff --git a/src/Trash/Radarr/RadarrConfiguration.cs b/src/Trash/Radarr/RadarrConfiguration.cs index 001d8d3f..7b12f302 100644 --- a/src/Trash/Radarr/RadarrConfiguration.cs +++ b/src/Trash/Radarr/RadarrConfiguration.cs @@ -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)] diff --git a/src/Trash/Sonarr/SonarrConfiguration.cs b/src/Trash/Sonarr/SonarrConfiguration.cs index f8d0ee09..3277e824 100644 --- a/src/Trash/Sonarr/SonarrConfiguration.cs +++ b/src/Trash/Sonarr/SonarrConfiguration.cs @@ -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)]