parent
6e397aefcf
commit
aa523a0e14
@ -0,0 +1,171 @@
|
||||
using FluentAssertions;
|
||||
using NSubstitute;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.TrashLib.Config;
|
||||
using Recyclarr.TrashLib.Config.Parsing;
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ConfigurationLoaderEnvVarTest : IntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Test_successful_environment_variable_loading()
|
||||
{
|
||||
var env = Resolve<IEnvironment>();
|
||||
env.GetEnvironmentVariable("SONARR_API_KEY").Returns("the_api_key");
|
||||
env.GetEnvironmentVariable("SONARR_URL").Returns("the_url");
|
||||
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = @"
|
||||
sonarr:
|
||||
instance:
|
||||
api_key: !env_var SONARR_API_KEY
|
||||
base_url: !env_var SONARR_URL http://sonarr:1233
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "the_url",
|
||||
ApiKey = "the_api_key"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Use_default_value_if_env_var_not_defined()
|
||||
{
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = @"
|
||||
sonarr:
|
||||
instance:
|
||||
base_url: !env_var SONARR_URL http://sonarr:1233
|
||||
api_key: value
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "http://sonarr:1233"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Default_value_with_spaces_is_allowed()
|
||||
{
|
||||
var env = Resolve<IEnvironment>();
|
||||
env.GetEnvironmentVariable("SONARR_URL").Returns("");
|
||||
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = @"
|
||||
sonarr:
|
||||
instance:
|
||||
base_url: !env_var SONARR_URL some value
|
||||
api_key: value
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "some value"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Quotation_characters_are_stripped_from_default_value()
|
||||
{
|
||||
var env = Resolve<IEnvironment>();
|
||||
env.GetEnvironmentVariable("SONARR_URL").Returns("");
|
||||
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = @"
|
||||
sonarr:
|
||||
instance:
|
||||
base_url: !env_var SONARR_URL ""the url""
|
||||
api_key: !env_var SONARR_API 'the key'
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "the url",
|
||||
ApiKey = "the key"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Multiple_spaces_between_default_and_env_var_work()
|
||||
{
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = @"
|
||||
sonarr:
|
||||
instance:
|
||||
base_url: !env_var SONARR_URL some value
|
||||
api_key: value
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "some value"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Tab_characters_are_stripped()
|
||||
{
|
||||
var sut = Resolve<ConfigurationLoader>();
|
||||
|
||||
const string testYml = $@"
|
||||
sonarr:
|
||||
instance:
|
||||
base_url: !env_var SONARR_URL {"\t"}some value
|
||||
api_key: value
|
||||
";
|
||||
|
||||
var configCollection = sut.LoadFromStream(new StringReader(testYml));
|
||||
|
||||
var config = configCollection.Get<SonarrConfiguration>(SupportedServices.Sonarr);
|
||||
config.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
BaseUrl = "some value"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using Recyclarr.Cli.TestLibrary;
|
||||
using Recyclarr.TrashLib.Config;
|
||||
using Recyclarr.TrashLib.Config.Parsing;
|
||||
using Recyclarr.TrashLib.Config.Secrets;
|
||||
using Recyclarr.TrashLib.Services.Sonarr.Config;
|
||||
using Serilog.Sinks.TestCorrelator;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace Recyclarr.TrashLib.Tests.Config.Parsing;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class ConfigurationLoaderSecretsTest : IntegrationFixture
|
||||
{
|
||||
[Test]
|
||||
public void Test_secret_loading()
|
||||
{
|
||||
var configLoader = Resolve<ConfigurationLoader>();
|
||||
|
||||
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<SonarrConfiguration>
|
||||
{
|
||||
new()
|
||||
{
|
||||
InstanceName = "instance1",
|
||||
ApiKey = "95283e6b156c42f3af8a9b16173f876b",
|
||||
BaseUrl = "https://radarr:7878",
|
||||
ReleaseProfiles = new List<ReleaseProfileConfig>
|
||||
{
|
||||
new()
|
||||
{
|
||||
TrashIds = new[] {"1234567"}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var parsedSecret = configLoader.LoadFromStream(new StringReader(testYml), "sonarr");
|
||||
parsedSecret.Get<SonarrConfiguration>(SupportedServices.Sonarr)
|
||||
.Should().BeEquivalentTo(expected, o => o.Excluding(x => x.LineNumber));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_referencing_invalid_secret()
|
||||
{
|
||||
using var logContext = TestCorrelator.CreateContext();
|
||||
var configLoader = Resolve<ConfigurationLoader>();
|
||||
|
||||
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<YamlException>()
|
||||
.WithInnerException<SecretNotFoundException>()
|
||||
.WithMessage("*api_key is not defined in secrets.yml");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_referencing_secret_without_secrets_file()
|
||||
{
|
||||
var configLoader = Resolve<ConfigurationLoader>();
|
||||
|
||||
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<YamlException>()
|
||||
.WithInnerException<SecretNotFoundException>()
|
||||
.WithMessage("*api_key is not defined in secrets.yml");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_secret_value_is_not_scalar()
|
||||
{
|
||||
var configLoader = Resolve<ConfigurationLoader>();
|
||||
|
||||
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<YamlException>().WithMessage("Expected 'Scalar'*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Throw_when_expected_value_is_not_scalar()
|
||||
{
|
||||
var configLoader = Resolve<ConfigurationLoader>();
|
||||
|
||||
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<YamlException>().WithMessage("Exception during deserialization");
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
namespace Recyclarr.TrashLib.Config.EnvironmentVariables;
|
||||
|
||||
public class EnvironmentVariableNotDefinedException : Exception
|
||||
{
|
||||
public int Line { get; }
|
||||
public string EnvironmentVariableName { get; }
|
||||
|
||||
public EnvironmentVariableNotDefinedException(int line, string environmentVariableName)
|
||||
: base(
|
||||
$"Line {line} refers to undefined environment variable {environmentVariableName} and no default is specified.")
|
||||
{
|
||||
Line = line;
|
||||
EnvironmentVariableName = environmentVariableName;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using Recyclarr.Common;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.EnvironmentVariables;
|
||||
|
||||
public record EnvironmentVariableTag;
|
||||
|
||||
public class EnvironmentVariablesDeserializer : INodeDeserializer
|
||||
{
|
||||
private readonly IEnvironment _environment;
|
||||
|
||||
public EnvironmentVariablesDeserializer(IEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer,
|
||||
out object? value)
|
||||
{
|
||||
// Only process items flagged as Environment Variables
|
||||
if (expectedType != typeof(EnvironmentVariableTag))
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var scalar = reader.Consume<Scalar>();
|
||||
var split = scalar.Value.Trim().Split(' ', 2);
|
||||
var envVarValue = _environment.GetEnvironmentVariable(split[0]);
|
||||
if (string.IsNullOrWhiteSpace(envVarValue))
|
||||
{
|
||||
// Trim whitespace + quotation characters
|
||||
envVarValue = split.ElementAtOrDefault(1)?.Trim().Trim('\'', '"');
|
||||
}
|
||||
|
||||
value = envVarValue ?? throw new EnvironmentVariableNotDefinedException(scalar.Start.Line, scalar.Value);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using JetBrains.Annotations;
|
||||
using Recyclarr.Common;
|
||||
using Recyclarr.TrashLib.Config.Yaml;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Recyclarr.TrashLib.Config.EnvironmentVariables;
|
||||
|
||||
[UsedImplicitly]
|
||||
public class EnvironmentVariablesYamlBehavior : IYamlBehavior
|
||||
{
|
||||
private readonly IEnvironment _environment;
|
||||
|
||||
public EnvironmentVariablesYamlBehavior(IEnvironment environment)
|
||||
{
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
public void Setup(DeserializerBuilder builder)
|
||||
{
|
||||
builder
|
||||
.WithNodeDeserializer(new EnvironmentVariablesDeserializer(_environment))
|
||||
.WithTagMapping("!env_var", typeof(EnvironmentVariableTag));
|
||||
}
|
||||
}
|
Loading…
Reference in new issue