using System.IO.Abstractions; using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using System.Text; using Autofac; using Common; using Common.Extensions; using FluentAssertions; using FluentValidation; using FluentValidation.Results; using NSubstitute; using NUnit.Framework; using Recyclarr.Config; using Recyclarr.TestLibrary; using Serilog.Sinks.TestCorrelator; using TestLibrary; using TestLibrary.AutoFixture; using TrashLib.Config.Secrets; using TrashLib.Services.Sonarr.Config; using TrashLib.TestLibrary; using YamlDotNet.Core; namespace Recyclarr.Tests.Config; [TestFixture] [Parallelizable(ParallelScope.All)] public class ConfigurationLoaderTest : IntegrationFixture { private static TextReader GetResourceData(string file) { var testData = new ResourceDataReader(typeof(ConfigurationLoaderTest), "Data"); return new StringReader(testData.ReadData(file)); } protected override void RegisterExtraTypes(ContainerBuilder builder) { base.RegisterExtraTypes(builder); builder.RegisterMockFor>(); } [Test] public void Load_many_iterations_of_config() { static string MockYaml(params object[] args) { var str = new StringBuilder("sonarr:"); const string templateYaml = "\n - base_url: {0}\n api_key: abc"; str.Append(args.Aggregate("", (current, p) => current + templateYaml.FormatWith(p))); return str.ToString(); } var baseDir = Fs.CurrentDirectory(); var fileData = new (string, string)[] { (baseDir.File("config1.yml").FullName, MockYaml(1, 2)), (baseDir.File("config2.yml").FullName, MockYaml(3)), (baseDir.File("config3.yml").FullName, "bad yaml") }; foreach (var (file, data) in fileData) { Fs.AddFile(file, new MockFileData(data)); } var expected = new List { new() {ApiKey = "abc", BaseUrl = "1"}, new() {ApiKey = "abc", BaseUrl = "2"}, new() {ApiKey = "abc", BaseUrl = "3"} }; var loader = Resolve>(); var actual = loader.LoadMany(fileData.Select(x => x.Item1), "sonarr").ToList(); actual.Should().BeEquivalentTo(expected); } [Test] public void Parse_using_stream() { var configLoader = Resolve>(); var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); configs.Should().BeEquivalentTo(new List { new() { ApiKey = "95283e6b156c42f3af8a9b16173f876b", BaseUrl = "http://localhost:8989", Name = "name", ReleaseProfiles = new List { new() { TrashIds = new[] {"123"}, StrictNegativeScores = true, Tags = new List {"anime"} }, new() { TrashIds = new[] {"456"}, StrictNegativeScores = false, Tags = new List { "tv", "series" } } } } }); } [Test] public void Skip_when_validation_fails() // [Frozen] IValidator validator, // ConfigurationLoader configLoader) { var validator = Resolve>(); var sut = Resolve>(); // force the validator to return a validation error validator.Validate(Arg.Any()).Returns(new ValidationResult { Errors = { new ValidationFailure("PropertyName", "Test Validation Failure"), new ValidationFailure("Another", "This is yet another failure") } }); const string testYml = @" fubar: instance1: api_key: abc "; var result = sut.LoadFromStream(new StringReader(testYml), "fubar"); result.Should().BeEmpty(); } [Test] public void Validation_success_does_not_throw() { var configLoader = Resolve>(); const string testYml = @" fubar: instanceA: api_key: abc "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); act.Should().NotThrow(); } [Test] public void Test_secret_loading() { var configLoader = Resolve>(); const string testYml = @" sonarr: instance1: api_key: !secret api_key base_url: !secret 123GARBAGE_ release_profiles: - trash_ids: - !secret secret_rp "; const string secretsYml = @" api_key: 95283e6b156c42f3af8a9b16173f876b 123GARBAGE_: 'https://radarr:7878' secret_rp: 1234567 "; Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml)); var expected = new List { new() { Name = "instance1", ApiKey = "95283e6b156c42f3af8a9b16173f876b", BaseUrl = "https://radarr:7878", ReleaseProfiles = new List { new() { TrashIds = new[] {"1234567"} } } } }; var parsedSecret = configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); parsedSecret.Should().BeEquivalentTo(expected); } [Test] public void Throw_when_referencing_invalid_secret() { using var logContext = TestCorrelator.CreateContext(); var configLoader = Resolve>(); const string testYml = @" sonarr: instance2: api_key: !secret api_key base_url: fake_url "; const string secretsYml = "no_api_key: 95283e6b156c42f3af8a9b16173f876b"; Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml)); var act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); act.Should().Throw() .WithInnerException() .WithMessage("*api_key is not defined in secrets.yml"); } [Test] public void Throw_when_referencing_secret_without_secrets_file() { var configLoader = Resolve>(); const string testYml = @" sonarr: instance3: api_key: !secret api_key base_url: fake_url "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); act.Should().Throw() .WithInnerException() .WithMessage("*api_key is not defined in secrets.yml"); } [Test] public void Throw_when_secret_value_is_not_scalar() { var configLoader = Resolve>(); const string testYml = @" sonarr: instance4: api_key: !secret { property: value } base_url: fake_url "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); act.Should().Throw().WithMessage("Expected 'Scalar'*"); } [Test] public void Throw_when_expected_value_is_not_scalar() { var configLoader = Resolve>(); const string testYml = @" sonarr: instance5: api_key: fake_key base_url: fake_url release_profiles: !secret bogus_profile "; const string secretsYml = @"bogus_profile: 95283e6b156c42f3af8a9b16173f876b"; Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml)); Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); act.Should().Throw().WithMessage("Exception during deserialization"); } [Test, AutoMockData] public void Yaml_file_with_only_comment_should_be_skipped(ConfigurationLoader sut) { const string testYml = "# YAML with nothing but this comment"; var result = sut.LoadFromStream(new StringReader(testYml), "fubar"); result.Should().BeEmpty(); } }