From c44ef1ac7d3a2e027a0a8c685d322fe236c5ea5e Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Thu, 9 Jan 2025 19:22:46 -0800 Subject: [PATCH] Add time based testing to ConfigurationManagerTests through FakeTimeProvider --- build/dependenciesTest.props | 9 ++ .../Configuration/ConfigurationManager.cs | 9 +- .../ConfigurationManagerTests.cs | 96 ++++++++++++++++++- ...Model.Protocols.OpenIdConnect.Tests.csproj | 7 ++ 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/build/dependenciesTest.props b/build/dependenciesTest.props index 65daedcf38..7cc73f438b 100644 --- a/build/dependenciesTest.props +++ b/build/dependenciesTest.props @@ -19,4 +19,13 @@ 3.0.0-pre.49 + + + + 9.0.0 + + + 8.10.0 + + diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index 8e37d60ac2..b1cea9ebdc 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -35,6 +35,8 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private const int ConfigurationRetrieverRunning = 1; private int _configurationRetrieverState = ConfigurationRetrieverIdle; + private readonly TimeProvider _timeProvider = TimeProvider.System; + // If a refresh is requested, then do the refresh as a blocking operation // not on a background thread. bool _refreshRequested; @@ -151,7 +153,7 @@ public async Task GetConfigurationAsync() /// If the time since the last call is less than then is not called and the current Configuration is returned. public virtual async Task GetConfigurationAsync(CancellationToken cancel) { - if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow) + if (_currentConfiguration != null && _syncAfter > _timeProvider.GetUtcNow()) return _currentConfiguration; Exception fetchMetadataFailure = null; @@ -295,7 +297,7 @@ private void UpdateCurrentConfiguration() private void UpdateConfiguration(T configuration) { _currentConfiguration = configuration; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + + _syncAfter = DateTimeUtil.Add(_timeProvider.GetUtcNow().UtcDateTime, AutomaticRefreshInterval + TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); } @@ -319,11 +321,12 @@ public override async Task GetBaseConfigurationAsync(Cancella /// public override void RequestRefresh() { - DateTimeOffset now = DateTimeOffset.UtcNow; + DateTimeOffset now = _timeProvider.GetUtcNow(); if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; _syncAfter = now; + _lastRequestRefresh = now; _refreshRequested = true; } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index f7bba705bc..ebf33d0b68 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs @@ -11,6 +11,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect.Configuration; using Microsoft.IdentityModel.TestUtils; @@ -19,9 +20,6 @@ namespace Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests { - /// - /// - /// public class ConfigurationManagerTests { /// @@ -569,7 +567,7 @@ public static TheoryData("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); @@ -720,6 +718,96 @@ public void TestConfigurationComparer() TestUtilities.AssertFailIfErrors(context); } + [Fact] + public async Task RequestRefresh_RespectsRefreshInterval() + { + // This test checks that the _syncAfter field is set correctly after a refresh. + var context = new CompareContext($"{this}.RequestRefresh_RespectsRefreshInterval"); + + var timeProvider = new FakeTimeProvider(); + + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + // Get the first configuration. + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); + + configManager.RequestRefresh(); + + var configAfterFirstRefresh = await configManager.GetConfigurationAsync(CancellationToken.None); + + // First RequestRefresh triggers a refresh. + if (object.ReferenceEquals(configuration, configAfterFirstRefresh)) + context.Diffs.Add("object.ReferenceEquals(configuration, configAfterFirstRefresh)"); + + configManager.RequestRefresh(); + + var configAfterSecondRefresh = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Second RequestRefresh should not trigger a refresh because the refresh interval has not passed. + if (!object.ReferenceEquals(configAfterFirstRefresh, configAfterSecondRefresh)) + context.Diffs.Add("!object.ReferenceEquals(configAfterFirstRefresh, configAfterSecondRefresh)"); + + // Advance time to trigger a refresh. + timeProvider.Advance(configManager.RefreshInterval); + + configManager.RequestRefresh(); + + var configAfterThirdRefresh = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Third RequestRefresh should trigger a refresh because the refresh interval has passed. + if (object.ReferenceEquals(configAfterSecondRefresh, configAfterThirdRefresh)) + context.Diffs.Add("object.ReferenceEquals(configAfterSecondRefresh, configAfterThirdRefresh)"); + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public async Task GetConfigurationAsync_RespectsRefreshInterval() + { + var context = new CompareContext($"{this}.GetConfigurationAsync_RespectsRefreshInterval"); + + var timeProvider = new FakeTimeProvider(); + + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + TimeSpan advanceInterval = BaseConfigurationManager.DefaultAutomaticRefreshInterval.Add(TimeSpan.FromSeconds(configManager.AutomaticRefreshInterval.TotalSeconds / 20)); + + TestUtilities.SetField(configManager, "_timeProvider", timeProvider); + + // Get the first configuration. + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); + + var configNoAdvanceInTime = await configManager.GetConfigurationAsync(CancellationToken.None); + + // First GetConfigurationAsync should not trigger a refresh because the refresh interval has not passed. + if (!object.ReferenceEquals(configuration, configNoAdvanceInTime)) + context.Diffs.Add("!object.ReferenceEquals(configuration, configNoAdvanceInTime)"); + + // Advance time to trigger a refresh. + timeProvider.Advance(advanceInterval); + + var configAfterTimeIsAdvanced = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Same config, but a task is queued to update the configuration. + if (!object.ReferenceEquals(configNoAdvanceInTime, configAfterTimeIsAdvanced)) + context.Diffs.Add("!object.ReferenceEquals(configuration, configAfterTimeIsAdvanced)"); + + // Need to wait for background task to finish. + Thread.Sleep(250); + + var configAfterBackgroundTask = await configManager.GetConfigurationAsync(CancellationToken.None); + + // Configuration should be updated after the background task finishes. + if (object.ReferenceEquals(configAfterTimeIsAdvanced, configAfterBackgroundTask)) + context.Diffs.Add("object.ReferenceEquals(configuration, configAfterBackgroundTask)"); + + TestUtilities.AssertFailIfErrors(context); + } + [Theory, MemberData(nameof(ValidateOpenIdConnectConfigurationTestCases), DisableDiscoveryEnumeration = true)] public async Task ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTheoryData theoryData) { diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj index 1cbfb9eed3..8d266dc7ce 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj @@ -11,6 +11,12 @@ true + + + true + + PreserveNewest @@ -27,6 +33,7 @@ +