File: Routing\RoutingStrategyTest.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Http.Resilience.Tests\Microsoft.Extensions.Http.Resilience.Tests.csproj (Microsoft.Extensions.Http.Resilience.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.Generic;
using System.Linq;
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http.Resilience.Internal;
using Microsoft.Extensions.Http.Resilience.Routing.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
 
namespace Microsoft.Extensions.Http.Resilience.Test.Routing;
 
public abstract class RoutingStrategyTest
{
    public const string RoutingName = "dummy-routing";
 
    protected RoutingStrategyTest()
    {
        Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection());
        Builder.Services.TryAddSingleton(Randomizer.Object);
    }
 
    public IRoutingStrategyBuilder Builder { get; set; }
 
    internal Mock<Randomizer> Randomizer { get; } = new Mock<Randomizer>(MockBehavior.Strict);
 
    public virtual bool CompareOrder => true;
 
    [Fact]
    public void CreateStrategy_EnsurePooled()
    {
        SetupRandomizer(60d);
        SetupRandomizer(60);
        Configure(Builder);
 
        var factory = CreateRoutingFactory();
        var strategies = new HashSet<RequestRoutingStrategy>();
 
        for (int i = 0; i < 10; i++)
        {
            using var strategy = factory();
            strategies.Add(strategy);
        }
 
        // assert that some strategies were pooled
        Assert.True(strategies.Count < 5);
    }
 
    [Fact]
    public virtual void MinRoutes_Ok()
    {
        SetupRandomizer(0);
        SetupRandomizer(0d);
 
        var routes = ConfigureMinRoutes(Builder).ToArray();
 
        var urls = CollectUrls(CreateStrategy()).ToArray();
        Assert.Equal(routes.Length, urls.Length);
        if (CompareOrder)
        {
            urls.Should().Equal(routes);
        }
        else
        {
            urls.Should().BeEquivalentTo(routes);
        }
    }
 
    [Fact]
    public void InvalidRoutes_ValidationException()
    {
        foreach (var action in ConfigureInvalidRoutes())
        {
            Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection());
            Builder.Services.TryAddSingleton(Randomizer.Object);
            action(Builder);
 
            Assert.Throws<OptionsValidationException>(() => CreateStrategy());
        }
    }
 
    [Fact]
    public void TryGetNextRoute_NotInitialized_Throws()
    {
        Assert.Throws<InvalidOperationException>(() => CreateEmptyStrategy().TryGetNextRoute(out _));
    }
 
    [Fact]
    public void TryGetNextRoute_AfterReset_Throws()
    {
        SetupRandomizer(0);
 
        Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection());
        Builder.Services.TryAddSingleton(Randomizer.Object);
        Configure(Builder);
 
        var strategy = CreateStrategy();
 
        _ = ((IResettable)strategy).TryReset();
 
        Assert.Throws<InvalidOperationException>(() => strategy.TryGetNextRoute(out _));
    }
 
    protected void ReloadHelper(
        Action<IRoutingStrategyBuilder, IConfiguration> configure,
        Dictionary<string, string?> config1,
        Dictionary<string, string?> config2,
        string[] urls1,
        string[] urls2)
    {
        var provider = new ReloadableConfiguration();
        provider.Reload(config1);
 
        var builder = new ConfigurationBuilder();
        builder.Add(provider);
        configure(Builder, builder.Build());
 
        CollectUrls(CreateStrategy()).Should().Equal(urls1);
 
        // empty data -> failure
        Assert.Throws<AggregateException>(() => provider.Reload([]));
 
        provider.Reload(config2);
        CollectUrls(CreateStrategy()).Should().Equal(urls2);
    }
 
    internal void StrategyResultHelper(params string[] expectedUrls)
    {
        Builder = new RoutingStrategyBuilder(RoutingName, new ServiceCollection());
        Builder.Services.TryAddSingleton(Randomizer.Object);
        Configure(Builder);
 
        var factory = CreateRoutingFactory();
 
        CollectUrls(factory()).Should().Equal(expectedUrls);
 
        // intentionally, we check that the output on subsequent calls is the same
        CollectUrls(factory()).Should().Equal(expectedUrls);
    }
 
    internal RequestRoutingStrategy CreateStrategy(string? name = null) => CreateRoutingFactory(name)();
 
    internal Func<RequestRoutingStrategy> CreateRoutingFactory(string? name = null)
    {
        return Builder.Services
            .BuildServiceProvider()
            .GetRequiredService<IOptionsMonitor<RequestRoutingOptions>>()
            .Get(name ?? Builder.Name).RoutingStrategyProvider!;
    }
 
    private static IEnumerable<string> CollectUrls(RequestRoutingStrategy strategy)
    {
        while (strategy.TryGetNextRoute(out var route))
        {
            yield return route.ToString();
        }
    }
 
    protected static IConfigurationSection GetSection(IDictionary<string, string> values)
    {
        return new ConfigurationBuilder().AddInMemoryCollection(values.Select(pair => new KeyValuePair<string, string?>("section:" + pair.Key, pair.Value))).Build().GetSection("section");
    }
 
    protected abstract void Configure(IRoutingStrategyBuilder routingBuilder);
 
    protected abstract IEnumerable<string> ConfigureMinRoutes(IRoutingStrategyBuilder routingBuilder);
 
    protected abstract IEnumerable<Action<IRoutingStrategyBuilder>> ConfigureInvalidRoutes();
 
    internal abstract RequestRoutingStrategy CreateEmptyStrategy();
 
    protected void SetupRandomizer(double result) => Randomizer.Setup(r => r.NextDouble(It.IsAny<double>())).Returns(result);
 
    protected void SetupRandomizer(int result) => Randomizer.Setup(r => r.NextInt(It.IsAny<int>())).Returns(result);
 
    private class ReloadableConfiguration : ConfigurationProvider, IConfigurationSource
    {
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return this;
        }
 
        public void Reload(Dictionary<string, string?> data)
        {
            Data = new Dictionary<string, string?>(data, StringComparer.OrdinalIgnoreCase);
            OnReload();
        }
    }
}