Activate ChangeToken when Ocelot's configuration changes #1037

* Add configuration change token (#1036)

* Add IOptionsMonitor<IInternalConfiguration>

* Activate change token from *ConfigurationRepository instead of FileAndInternalConfigurationSetter; add acceptance & integration tests

* Update documentation

* Use IWebHostEnvironment as IHostingEnvironment deprecated

Co-authored-by: Chris Swinchatt <chrisswinchatt@gmail.com>
This commit is contained in:
Tom Pallister 2020-02-04 20:50:40 +00:00 committed by GitHub
parent 473d50ff36
commit 86e8d66daf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1546 additions and 1111 deletions

View File

@ -228,3 +228,53 @@ MaxConnectionsPerServer
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
This controls how many connections the internal HttpClient will open. This can be set at ReRoute or global level. This controls how many connections the internal HttpClient will open. This can be set at ReRoute or global level.
React to Configuration Changes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Resolve IOcelotConfigurationChangeTokenSource from the DI container if you wish to react to changes to the Ocelot configuration via the Ocelot.Administration API or ocelot.json being reloaded from the disk. You may either poll the change token's HasChanged property, or register a callback with the RegisterChangeCallback method.
Polling the HasChanged property
-------------------------------
.. code-block:: csharp
public class ConfigurationNotifyingService : BackgroundService
{
private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
private readonly ILogger _logger;
public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger)
{
_tokenSource = tokenSource;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_tokenSource.ChangeToken.HasChanged)
{
_logger.LogInformation("Configuration updated");
}
await Task.Delay(1000, stoppingToken);
}
}
}
Registering a callback
----------------------
.. code-block:: csharp
public class MyDependencyInjectedClass : IDisposable
{
private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
private readonly IDisposable _callbackHolder;
public MyClass(IOcelotConfigurationChangeTokenSource tokenSource)
{
_tokenSource = tokenSource;
_callbackHolder = tokenSource.ChangeToken.RegisterChangeCallback(_ => Console.WriteLine("Configuration changed"), null);
}
public void Dispose()
{
_callbackHolder.Dispose();
}
}

View File

@ -0,0 +1,14 @@
namespace Ocelot.Configuration.ChangeTracking
{
using Microsoft.Extensions.Primitives;
/// <summary>
/// <see cref="IChangeToken" /> source which is activated when Ocelot's configuration is changed.
/// </summary>
public interface IOcelotConfigurationChangeTokenSource
{
IChangeToken ChangeToken { get; }
void Activate();
}
}

View File

@ -0,0 +1,74 @@
namespace Ocelot.Configuration.ChangeTracking
{
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Primitives;
public class OcelotConfigurationChangeToken : IChangeToken
{
public const double PollingIntervalSeconds = 1;
private readonly ICollection<CallbackWrapper> _callbacks = new List<CallbackWrapper>();
private readonly object _lock = new object();
private DateTime? _timeChanged;
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
{
lock (_lock)
{
var wrapper = new CallbackWrapper(callback, state, _callbacks, _lock);
_callbacks.Add(wrapper);
return wrapper;
}
}
public void Activate()
{
lock (_lock)
{
_timeChanged = DateTime.UtcNow;
foreach (var wrapper in _callbacks)
{
wrapper.Invoke();
}
}
}
// Token stays active for PollingIntervalSeconds after a change (could be parameterised) - otherwise HasChanged would be true forever.
// Taking suggestions for better ways to reset HasChanged back to false.
public bool HasChanged => _timeChanged.HasValue && (DateTime.UtcNow - _timeChanged.Value).TotalSeconds < PollingIntervalSeconds;
public bool ActiveChangeCallbacks => true;
private class CallbackWrapper : IDisposable
{
private readonly ICollection<CallbackWrapper> _callbacks;
private readonly object _lock;
public CallbackWrapper(Action<object> callback, object state, ICollection<CallbackWrapper> callbacks, object @lock)
{
_callbacks = callbacks;
_lock = @lock;
Callback = callback;
State = state;
}
public void Invoke()
{
Callback.Invoke(State);
}
public void Dispose()
{
lock (_lock)
{
_callbacks.Remove(this);
}
}
public Action<object> Callback { get; }
public object State { get; }
}
}
}

View File

@ -0,0 +1,16 @@
namespace Ocelot.Configuration.ChangeTracking
{
using Microsoft.Extensions.Primitives;
public class OcelotConfigurationChangeTokenSource : IOcelotConfigurationChangeTokenSource
{
private readonly OcelotConfigurationChangeToken _changeToken = new OcelotConfigurationChangeToken();
public IChangeToken ChangeToken => _changeToken;
public void Activate()
{
_changeToken.Activate();
}
}
}

View File

@ -0,0 +1,30 @@
namespace Ocelot.Configuration.ChangeTracking
{
using System;
using Microsoft.Extensions.Options;
using Ocelot.Configuration.Repository;
public class OcelotConfigurationMonitor : IOptionsMonitor<IInternalConfiguration>
{
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
private readonly IInternalConfigurationRepository _repo;
public OcelotConfigurationMonitor(IInternalConfigurationRepository repo, IOcelotConfigurationChangeTokenSource changeTokenSource)
{
_changeTokenSource = changeTokenSource;
_repo = repo;
}
public IInternalConfiguration Get(string name)
{
return _repo.Get().Data;
}
public IDisposable OnChange(Action<IInternalConfiguration, string> listener)
{
return _changeTokenSource.ChangeToken.RegisterChangeCallback(_ => listener(CurrentValue, ""), null);
}
public IInternalConfiguration CurrentValue => _repo.Get().Data;
}
}

View File

@ -4,18 +4,21 @@ using Ocelot.Configuration.File;
using Ocelot.Responses; using Ocelot.Responses;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ocelot.Configuration.ChangeTracking;
namespace Ocelot.Configuration.Repository namespace Ocelot.Configuration.Repository
{ {
public class DiskFileConfigurationRepository : IFileConfigurationRepository public class DiskFileConfigurationRepository : IFileConfigurationRepository
{ {
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
private readonly string _environmentFilePath; private readonly string _environmentFilePath;
private readonly string _ocelotFilePath; private readonly string _ocelotFilePath;
private static readonly object _lock = new object(); private static readonly object _lock = new object();
private const string ConfigurationFileName = "ocelot"; private const string ConfigurationFileName = "ocelot";
public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment) public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource)
{ {
_changeTokenSource = changeTokenSource;
_environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json"; _environmentFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json";
_ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json"; _ocelotFilePath = $"{AppContext.BaseDirectory}{ConfigurationFileName}.json";
@ -56,6 +59,7 @@ namespace Ocelot.Configuration.Repository
System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration); System.IO.File.WriteAllText(_ocelotFilePath, jsonConfiguration);
} }
_changeTokenSource.Activate();
return Task.FromResult<Response>(new OkResponse()); return Task.FromResult<Response>(new OkResponse());
} }
} }

View File

@ -1,4 +1,5 @@
using Ocelot.Responses; using Ocelot.Configuration.ChangeTracking;
using Ocelot.Responses;
namespace Ocelot.Configuration.Repository namespace Ocelot.Configuration.Repository
{ {
@ -10,6 +11,12 @@ namespace Ocelot.Configuration.Repository
private static readonly object LockObject = new object(); private static readonly object LockObject = new object();
private IInternalConfiguration _internalConfiguration; private IInternalConfiguration _internalConfiguration;
private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource;
public InMemoryInternalConfigurationRepository(IOcelotConfigurationChangeTokenSource changeTokenSource)
{
_changeTokenSource = changeTokenSource;
}
public Response<IInternalConfiguration> Get() public Response<IInternalConfiguration> Get()
{ {
@ -23,6 +30,7 @@ namespace Ocelot.Configuration.Repository
_internalConfiguration = internalConfiguration; _internalConfiguration = internalConfiguration;
} }
_changeTokenSource.Activate();
return new OkResponse(); return new OkResponse();
} }
} }

View File

@ -1,9 +1,12 @@
using Ocelot.Configuration.ChangeTracking;
namespace Ocelot.DependencyInjection namespace Ocelot.DependencyInjection
{ {
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Ocelot.Authorisation; using Ocelot.Authorisation;
using Ocelot.Cache; using Ocelot.Cache;
using Ocelot.Claims; using Ocelot.Claims;
@ -112,6 +115,8 @@ namespace Ocelot.DependencyInjection
Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>(); Services.TryAddSingleton<IDownstreamAddressesCreator, DownstreamAddressesCreator>();
Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>(); Services.TryAddSingleton<IDelegatingHandlerHandlerFactory, DelegatingHandlerHandlerFactory>();
Services.TryAddSingleton<ICacheKeyGenerator, CacheKeyGenerator>(); Services.TryAddSingleton<ICacheKeyGenerator, CacheKeyGenerator>();
Services.TryAddSingleton<IOcelotConfigurationChangeTokenSource, OcelotConfigurationChangeTokenSource>();
Services.TryAddSingleton<IOptionsMonitor<IInternalConfiguration>, OcelotConfigurationMonitor>();
// see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc
// could maybe use a scoped data repository // could maybe use a scoped data repository

View File

@ -1,5 +1,6 @@
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using System; using System;
using Ocelot.Configuration.ChangeTracking;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -54,6 +55,33 @@ namespace Ocelot.AcceptanceTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_trigger_change_token_on_change()
{
this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig))
.And(x => _steps.GivenOcelotIsRunningReloadingConfig(true))
.And(x => _steps.GivenIHaveAChangeToken())
.And(x => _steps.GivenThereIsAConfiguration(_anotherConfig))
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken))
.Then(x => _steps.TheChangeTokenShouldBeActive(true))
.BDDfy();
}
[Fact]
public void should_not_trigger_change_token_with_no_change()
{
this.Given(x => _steps.GivenThereIsAConfiguration(_initialConfig))
.And(x => _steps.GivenOcelotIsRunningReloadingConfig(false))
.And(x => _steps.GivenIHaveAChangeToken())
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken)) // Wait for prior activation to expire.
.And(x => _steps.GivenThereIsAConfiguration(_anotherConfig))
.And(x => _steps.GivenIWait(MillisecondsToWaitForChangeToken))
.Then(x => _steps.TheChangeTokenShouldBeActive(false))
.BDDfy();
}
private const int MillisecondsToWaitForChangeToken = (int) (OcelotConfigurationChangeToken.PollingIntervalSeconds*1000) - 100;
public void Dispose() public void Dispose()
{ {
_steps.Dispose(); _steps.Dispose();

View File

@ -1,4 +1,6 @@
namespace Ocelot.AcceptanceTests using Ocelot.Configuration.ChangeTracking;
namespace Ocelot.AcceptanceTests
{ {
using Caching; using Caching;
using Configuration.Repository; using Configuration.Repository;
@ -54,6 +56,7 @@
private IWebHostBuilder _webHostBuilder; private IWebHostBuilder _webHostBuilder;
private WebHostBuilder _ocelotBuilder; private WebHostBuilder _ocelotBuilder;
private IWebHost _ocelotHost; private IWebHost _ocelotHost;
private IOcelotConfigurationChangeTokenSource _changeToken;
public Steps() public Steps()
{ {
@ -216,6 +219,11 @@
_ocelotClient = _ocelotServer.CreateClient(); _ocelotClient = _ocelotServer.CreateClient();
} }
public void GivenIHaveAChangeToken()
{
_changeToken = _ocelotServer.Host.Services.GetRequiredService<IOcelotConfigurationChangeTokenSource>();
}
/// <summary> /// <summary>
/// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step.
/// </summary> /// </summary>
@ -1123,6 +1131,11 @@
_ocelotClient = _ocelotServer.CreateClient(); _ocelotClient = _ocelotServer.CreateClient();
} }
public void TheChangeTokenShouldBeActive(bool itShouldBeActive)
{
_changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive);
}
public void GivenOcelotIsRunningWithLogger() public void GivenOcelotIsRunningWithLogger()
{ {
_webHostBuilder = new WebHostBuilder(); _webHostBuilder = new WebHostBuilder();

View File

@ -21,6 +21,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using TestStack.BDDfy; using TestStack.BDDfy;
using Ocelot.Configuration.ChangeTracking;
using Xunit; using Xunit;
namespace Ocelot.IntegrationTests namespace Ocelot.IntegrationTests
@ -278,6 +279,51 @@ namespace Ocelot.IntegrationTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_activate_change_token_when_configuration_is_updated()
{
var configuration = new FileConfiguration
{
GlobalConfiguration = new FileGlobalConfiguration(),
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 80,
},
},
DownstreamScheme = "https",
DownstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "get" },
UpstreamPathTemplate = "/",
},
},
};
this.Given(x => GivenThereIsAConfiguration(configuration))
.And(x => GivenOcelotIsRunning())
.And(x => GivenIHaveAnOcelotToken("/administration"))
.And(x => GivenIHaveAddedATokenToMyRequest())
.When(x => WhenIPostOnTheApiGateway("/administration/configuration", configuration))
.Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => TheChangeTokenShouldBeActive())
.And(x => ThenTheResponseShouldBe(configuration))
.When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration"))
.And(x => ThenTheResponseShouldBe(configuration))
.And(_ => ThenTheConfigurationIsSavedCorrectly(configuration))
.BDDfy();
}
private void TheChangeTokenShouldBeActive()
{
_builder.Services.GetRequiredService<IOcelotConfigurationChangeTokenSource>().ChangeToken.HasChanged.ShouldBeTrue();
}
private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected) private void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expected)
{ {
var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json"; var ocelotJsonPath = $"{AppContext.BaseDirectory}ocelot.json";

View File

@ -0,0 +1,35 @@
namespace Ocelot.UnitTests.Configuration.ChangeTracking
{
using Ocelot.Configuration.ChangeTracking;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
public class OcelotConfigurationChangeTokenSourceTests
{
private readonly IOcelotConfigurationChangeTokenSource _source;
public OcelotConfigurationChangeTokenSourceTests()
{
_source = new OcelotConfigurationChangeTokenSource();
}
[Fact]
public void should_activate_change_token()
{
this.Given(_ => GivenIActivateTheChangeTokenSource())
.Then(_ => ThenTheChangeTokenShouldBeActivated())
.BDDfy();
}
private void GivenIActivateTheChangeTokenSource()
{
_source.Activate();
}
private void ThenTheChangeTokenShouldBeActivated()
{
_source.ChangeToken.HasChanged.ShouldBeTrue();
}
}
}

View File

@ -0,0 +1,91 @@
using Xunit;
namespace Ocelot.UnitTests.Configuration.ChangeTracking
{
using System;
using Shouldly;
using Ocelot.Configuration.ChangeTracking;
using TestStack.BDDfy;
public class OcelotConfigurationChangeTokenTests
{
[Fact]
public void should_call_callback_with_state()
{
this.Given(_ => GivenIHaveAChangeToken())
.And(_ => AndIRegisterACallback())
.Then(_ => ThenIShouldGetADisposableWrapper())
.Given(_ => GivenIActivateTheToken())
.Then(_ => ThenTheCallbackShouldBeCalled())
.BDDfy();
}
[Fact]
public void should_not_call_callback_if_it_is_disposed()
{
this.Given(_ => GivenIHaveAChangeToken())
.And(_ => AndIRegisterACallback())
.Then(_ => ThenIShouldGetADisposableWrapper())
.And(_ => GivenIActivateTheToken())
.And(_ => AndIDisposeTheCallbackWrapper())
.And(_ => GivenIActivateTheToken())
.Then(_ => ThenTheCallbackShouldNotBeCalled())
.BDDfy();
}
private OcelotConfigurationChangeToken _changeToken;
private IDisposable _callbackWrapper;
private int _callbackCounter;
private readonly object _callbackInitialState = new object();
private object _callbackState;
private void Callback(object state)
{
_callbackCounter++;
_callbackState = state;
_changeToken.HasChanged.ShouldBeTrue();
}
private void GivenIHaveAChangeToken()
{
_changeToken = new OcelotConfigurationChangeToken();
}
private void AndIRegisterACallback()
{
_callbackWrapper = _changeToken.RegisterChangeCallback(Callback, _callbackInitialState);
}
private void ThenIShouldGetADisposableWrapper()
{
_callbackWrapper.ShouldNotBeNull();
}
private void GivenIActivateTheToken()
{
_callbackCounter = 0;
_callbackState = null;
_changeToken.Activate();
}
private void ThenTheCallbackShouldBeCalled()
{
_callbackCounter.ShouldBe(1);
_callbackState.ShouldNotBeNull();
_callbackState.ShouldBeSameAs(_callbackInitialState);
}
private void ThenTheCallbackShouldNotBeCalled()
{
_callbackCounter.ShouldBe(0);
_callbackState.ShouldBeNull();
}
private void AndIDisposeTheCallbackWrapper()
{
_callbackState = null;
_callbackCounter = 0;
_callbackWrapper.Dispose();
}
}
}

View File

@ -3,6 +3,7 @@ namespace Ocelot.UnitTests.Configuration
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Moq; using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ocelot.Configuration.ChangeTracking;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Repository; using Ocelot.Configuration.Repository;
using Shouldly; using Shouldly;
@ -16,6 +17,7 @@ namespace Ocelot.UnitTests.Configuration
public class DiskFileConfigurationRepositoryTests : IDisposable public class DiskFileConfigurationRepositoryTests : IDisposable
{ {
private readonly Mock<IWebHostEnvironment> _hostingEnvironment; private readonly Mock<IWebHostEnvironment> _hostingEnvironment;
private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;
private IFileConfigurationRepository _repo; private IFileConfigurationRepository _repo;
private string _environmentSpecificPath; private string _environmentSpecificPath;
private string _ocelotJsonPath; private string _ocelotJsonPath;
@ -35,7 +37,9 @@ namespace Ocelot.UnitTests.Configuration
_semaphore.Wait(); _semaphore.Wait();
_hostingEnvironment = new Mock<IWebHostEnvironment>(); _hostingEnvironment = new Mock<IWebHostEnvironment>();
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); _changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);
_changeTokenSource.Setup(m => m.Activate());
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object);
} }
[Fact] [Fact]
@ -70,6 +74,7 @@ namespace Ocelot.UnitTests.Configuration
.When(_ => WhenISetTheConfiguration()) .When(_ => WhenISetTheConfiguration())
.Then(_ => ThenTheConfigurationIsStoredAs(config)) .Then(_ => ThenTheConfigurationIsStoredAs(config))
.And(_ => ThenTheConfigurationJsonIsIndented(config)) .And(_ => ThenTheConfigurationJsonIsIndented(config))
.And(x => AndTheChangeTokenIsActivated())
.BDDfy(); .BDDfy();
} }
@ -117,7 +122,7 @@ namespace Ocelot.UnitTests.Configuration
{ {
_environmentName = null; _environmentName = null;
_hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName);
_repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object);
} }
private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration)
@ -210,6 +215,11 @@ namespace Ocelot.UnitTests.Configuration
} }
} }
private void AndTheChangeTokenIsActivated()
{
_changeTokenSource.Verify(m => m.Activate(), Times.Once);
}
private FileConfiguration FakeFileConfigurationForSet() private FileConfiguration FakeFileConfigurationForSet()
{ {
var reRoutes = new List<FileReRoute> var reRoutes = new List<FileReRoute>
@ -222,11 +232,11 @@ namespace Ocelot.UnitTests.Configuration
{ {
Host = "123.12.12.12", Host = "123.12.12.12",
Port = 80, Port = 80,
} },
}, },
DownstreamScheme = "https", DownstreamScheme = "https",
DownstreamPathTemplate = "/asdfs/test/{test}" DownstreamPathTemplate = "/asdfs/test/{test}",
} },
}; };
var globalConfiguration = new FileGlobalConfiguration var globalConfiguration = new FileGlobalConfiguration

View File

@ -9,6 +9,7 @@ using Ocelot.Errors;
using Ocelot.Responses; using Ocelot.Responses;
using Shouldly; using Shouldly;
using System.Collections.Generic; using System.Collections.Generic;
using Ocelot.Configuration.ChangeTracking;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -104,8 +105,7 @@ namespace Ocelot.UnitTests.Configuration
private void ThenTheConfigurationRepositoryIsCalledCorrectly() private void ThenTheConfigurationRepositoryIsCalledCorrectly()
{ {
_configRepo _configRepo.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once);
.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once);
} }
} }
} }

View File

@ -5,6 +5,8 @@ using Ocelot.Responses;
using Shouldly; using Shouldly;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Moq;
using Ocelot.Configuration.ChangeTracking;
using TestStack.BDDfy; using TestStack.BDDfy;
using Xunit; using Xunit;
@ -16,10 +18,13 @@ namespace Ocelot.UnitTests.Configuration
private IInternalConfiguration _config; private IInternalConfiguration _config;
private Response _result; private Response _result;
private Response<IInternalConfiguration> _getResult; private Response<IInternalConfiguration> _getResult;
private readonly Mock<IOcelotConfigurationChangeTokenSource> _changeTokenSource;
public InMemoryConfigurationRepositoryTests() public InMemoryConfigurationRepositoryTests()
{ {
_repo = new InMemoryInternalConfigurationRepository(); _changeTokenSource = new Mock<IOcelotConfigurationChangeTokenSource>(MockBehavior.Strict);
_changeTokenSource.Setup(m => m.Activate());
_repo = new InMemoryInternalConfigurationRepository(_changeTokenSource.Object);
} }
[Fact] [Fact]
@ -28,6 +33,7 @@ namespace Ocelot.UnitTests.Configuration
this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath"))) this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath")))
.When(x => x.WhenIAddOrReplaceTheConfig()) .When(x => x.WhenIAddOrReplaceTheConfig())
.Then(x => x.ThenNoErrorsAreReturned()) .Then(x => x.ThenNoErrorsAreReturned())
.And(x => AndTheChangeTokenIsActivated())
.BDDfy(); .BDDfy();
} }
@ -71,6 +77,11 @@ namespace Ocelot.UnitTests.Configuration
_result.IsError.ShouldBeFalse(); _result.IsError.ShouldBeFalse();
} }
private void AndTheChangeTokenIsActivated()
{
_changeTokenSource.Verify(m => m.Activate(), Times.Once);
}
private class FakeConfig : IInternalConfiguration private class FakeConfig : IInternalConfiguration
{ {
private readonly string _downstreamTemplatePath; private readonly string _downstreamTemplatePath;