File: KeyPerFileTests.cs
Web Access
Project: src\src\Configuration.KeyPerFile\test\Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj (Microsoft.Extensions.Configuration.KeyPerFile.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Xunit;
 
namespace Microsoft.Extensions.Configuration.KeyPerFile.Test;
 
public class KeyPerFileTests
{
    [Fact]
    public void DoesNotThrowWhenOptionalAndNoSecrets()
    {
        new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
    }
 
    [Fact]
    public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
    {
        new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
    }
 
    [Fact]
    public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
    {
        var e = Assert.Throws<ArgumentException>(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
        Assert.Contains("The path must be absolute.", e.Message);
    }
 
    [Fact]
    public void CanLoadMultipleSecrets()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o => o.FileProvider = testFileProvider)
            .Build();
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void CanLoadMultipleSecretsWithDirectory()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"),
            new TestFile("directory"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o => o.FileProvider = testFileProvider)
            .Build();
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void CanLoadNestedKeys()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
            new TestFile("Secret0__Secret1__Key", "SecretValue1"),
            new TestFile("Secret0__Key", "SecretValue0"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o => o.FileProvider = testFileProvider)
            .Build();
 
        Assert.Equal("SecretValue0", config["Secret0:Key"]);
        Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
        Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
    }
 
    [Fact]
    public void LoadWithCustomSectionDelimiter()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Secret0--Secret1--Secret2--Key", "SecretValue2"),
            new TestFile("Secret0--Secret1--Key", "SecretValue1"),
            new TestFile("Secret0--Key", "SecretValue0"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.SectionDelimiter = "--";
            })
            .Build();
 
        Assert.Equal("SecretValue0", config["Secret0:Key"]);
        Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
        Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
    }
 
    [Fact]
    public void CanIgnoreFilesWithDefault()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("ignore.Secret0", "SecretValue0"),
            new TestFile("ignore.Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o => o.FileProvider = testFileProvider)
            .Build();
 
        Assert.Null(config["ignore.Secret0"]);
        Assert.Null(config["ignore.Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void CanTurnOffDefaultIgnorePrefixWithCondition()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("ignore.Secret0", "SecretValue0"),
            new TestFile("ignore.Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.IgnoreCondition = null;
            })
            .Build();
 
        Assert.Equal("SecretValue0", config["ignore.Secret0"]);
        Assert.Equal("SecretValue1", config["ignore.Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void CanIgnoreAllWithCondition()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Secret0", "SecretValue0"),
            new TestFile("Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.IgnoreCondition = s => true;
            })
            .Build();
 
        Assert.Empty(config.AsEnumerable());
    }
 
    [Fact]
    public void CanIgnoreFilesWithCustomIgnore()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("meSecret0", "SecretValue0"),
            new TestFile("meSecret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.IgnorePrefix = "me";
            })
            .Build();
 
        Assert.Null(config["meSecret0"]);
        Assert.Null(config["meSecret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void CanUnIgnoreDefaultFiles()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("ignore.Secret0", "SecretValue0"),
            new TestFile("ignore.Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.IgnorePrefix = null;
            })
            .Build();
 
        Assert.Equal("SecretValue0", config["ignore.Secret0"]);
        Assert.Equal("SecretValue1", config["ignore.Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void BindingDoesNotThrowIfReloadedDuringBinding()
    {
        var testFileProvider = new TestFileProvider(
            new TestFile("Number", "-2"),
            new TestFile("Text", "Foo"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o => o.FileProvider = testFileProvider)
            .Build();
 
        MyOptions options = null;
 
        using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
        {
            void ReloadLoop()
            {
                while (!cts.IsCancellationRequested)
                {
                    config.Reload();
                }
            }
 
            _ = Task.Run(ReloadLoop);
 
            while (!cts.IsCancellationRequested)
            {
                options = config.Get<MyOptions>();
            }
        }
 
        Assert.Equal(-2, options.Number);
        Assert.Equal("Foo", options.Text);
    }
 
    [Fact]
    public void ReloadConfigWhenReloadOnChangeIsTrue()
    {
        var testFileProvider = new TestFileProvider(
           new TestFile("Secret1", "SecretValue1"),
           new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = true;
            }).Build();
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
 
        testFileProvider.ChangeFiles(
            new TestFile("Secret1", "NewSecretValue1"),
            new TestFile("Secret3", "NewSecretValue3"));
 
        Assert.Equal("NewSecretValue1", config["Secret1"]);
        Assert.Null(config["NewSecret2"]);
        Assert.Equal("NewSecretValue3", config["Secret3"]);
    }
 
    [Fact]
    public void SameConfigWhenReloadOnChangeIsFalse()
    {
        var testFileProvider = new TestFileProvider(
           new TestFile("Secret1", "SecretValue1"),
           new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = false;
            }).Build();
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
 
        testFileProvider.ChangeFiles(
            new TestFile("Secret1", "NewSecretValue1"),
            new TestFile("Secret3", "NewSecretValue3"));
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact]
    public void NoFilesReloadWhenAddedFiles()
    {
        var testFileProvider = new TestFileProvider();
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = true;
                o.Optional = true;
            }).Build();
 
        Assert.Empty(config.AsEnumerable());
 
        testFileProvider.ChangeFiles(
            new TestFile("Secret1", "SecretValue1"),
            new TestFile("Secret2", "SecretValue2"));
 
        Assert.Equal("SecretValue1", config["Secret1"]);
        Assert.Equal("SecretValue2", config["Secret2"]);
    }
 
    [Fact(Timeout = 2000)]
    public async Task RaiseChangeEventWhenReloadOnChangeIsTrue()
    {
        var testFileProvider = new TestFileProvider(
           new TestFile("Secret1", "SecretValue1"),
           new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = true;
            }).Build();
 
        var changeToken = config.GetReloadToken();
        var changeTaskCompletion = new TaskCompletionSource<object>();
        changeToken.RegisterChangeCallback(state =>
            ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);
 
        testFileProvider.ChangeFiles(
            new TestFile("Secret1", "NewSecretValue1"),
            new TestFile("Secret3", "NewSecretValue3"));
 
        await changeTaskCompletion.Task;
 
        Assert.Equal("NewSecretValue1", config["Secret1"]);
        Assert.Null(config["NewSecret2"]);
        Assert.Equal("NewSecretValue3", config["Secret3"]);
    }
 
    [Fact(Timeout = 2000)]
    public async Task RaiseChangeEventWhenDirectoryClearsReloadOnChangeIsTrue()
    {
        var testFileProvider = new TestFileProvider(
           new TestFile("Secret1", "SecretValue1"),
           new TestFile("Secret2", "SecretValue2"));
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = true;
            }).Build();
 
        var changeToken = config.GetReloadToken();
        var changeTaskCompletion = new TaskCompletionSource<object>();
        changeToken.RegisterChangeCallback(state =>
            ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);
 
        testFileProvider.ChangeFiles();
 
        await changeTaskCompletion.Task;
 
        Assert.Empty(config.AsEnumerable());
    }
 
    [Fact(Timeout = 2000)]
    public async Task RaiseChangeEventAfterStartingWithEmptyDirectoryReloadOnChangeIsTrue()
    {
        var testFileProvider = new TestFileProvider();
 
        var config = new ConfigurationBuilder()
            .AddKeyPerFile(o =>
            {
                o.FileProvider = testFileProvider;
                o.ReloadOnChange = true;
                o.Optional = true;
            }).Build();
 
        var changeToken = config.GetReloadToken();
        var changeTaskCompletion = new TaskCompletionSource<object>();
        changeToken.RegisterChangeCallback(state =>
            ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);
 
        testFileProvider.ChangeFiles(new TestFile("Secret1", "SecretValue1"));
 
        await changeTaskCompletion.Task;
 
        Assert.Equal("SecretValue1", config["Secret1"]);
    }
 
    [Fact(Timeout = 2000)]
    public async Task RaiseChangeEventAfterProviderSetToNull()
    {
        var testFileProvider = new TestFileProvider(
           new TestFile("Secret1", "SecretValue1"),
           new TestFile("Secret2", "SecretValue2"));
        var configurationSource = new KeyPerFileConfigurationSource
        {
            FileProvider = testFileProvider,
            Optional = true,
        };
        var keyPerFileProvider = new KeyPerFileConfigurationProvider(configurationSource);
        var config = new ConfigurationRoot(new[] { keyPerFileProvider });
 
        var changeToken = config.GetReloadToken();
        var changeTaskCompletion = new TaskCompletionSource<object>();
        changeToken.RegisterChangeCallback(state =>
            ((TaskCompletionSource<object>)state).TrySetResult(null), changeTaskCompletion);
 
        configurationSource.FileProvider = null;
        config.Reload();
 
        await changeTaskCompletion.Task;
 
        Assert.Empty(config.AsEnumerable());
    }
 
    private sealed class MyOptions
    {
        public int Number { get; set; }
        public string Text { get; set; }
    }
}
 
class TestFileProvider : IFileProvider
{
    IDirectoryContents _contents;
    MockChangeToken _changeToken;
 
    public TestFileProvider(params IFileInfo[] files)
    {
        _contents = new TestDirectoryContents(files);
        _changeToken = new MockChangeToken();
    }
 
    public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
 
    public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
 
    public IChangeToken Watch(string filter) => _changeToken;
 
    internal void ChangeFiles(params IFileInfo[] files)
    {
        _contents = new TestDirectoryContents(files);
        _changeToken.RaiseCallback();
    }
}
 
class MockChangeToken : IChangeToken
{
    private Action _callback;
 
    public bool ActiveChangeCallbacks => true;
 
    public bool HasChanged => true;
 
    public IDisposable RegisterChangeCallback(Action<object> callback, object state)
    {
        var disposable = new MockDisposable();
        _callback = () => callback(state);
        return disposable;
    }
 
    internal void RaiseCallback()
    {
        _callback?.Invoke();
    }
}
 
class MockDisposable : IDisposable
{
    public bool Disposed { get; set; }
 
    public void Dispose()
    {
        Disposed = true;
    }
}
 
class TestDirectoryContents : IDirectoryContents
{
    List<IFileInfo> _list;
 
    public TestDirectoryContents(params IFileInfo[] files)
    {
        _list = new List<IFileInfo>(files);
    }
 
    public bool Exists => _list.Any();
 
    public IEnumerator<IFileInfo> GetEnumerator() => _list.GetEnumerator();
 
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
 
//TODO: Probably need a directory and file type.
class TestFile : IFileInfo
{
    private readonly string _name;
    private readonly string _contents;
 
    public bool Exists => true;
 
    public bool IsDirectory
    {
        get;
    }
 
    public DateTimeOffset LastModified => throw new NotImplementedException();
 
    public long Length => throw new NotImplementedException();
 
    public string Name => _name;
 
    public string PhysicalPath => "Root/" + Name;
 
    public TestFile(string name)
    {
        _name = name;
        IsDirectory = true;
    }
 
    public TestFile(string name, string contents)
    {
        _name = name;
        _contents = contents;
    }
 
    public Stream CreateReadStream()
    {
        if (IsDirectory)
        {
            throw new InvalidOperationException("Cannot create stream from directory");
        }
 
        return _contents == null
            ? new MemoryStream()
            : new MemoryStream(Encoding.UTF8.GetBytes(_contents));
    }
}