Skip to content

Commit

Permalink
Add time based testing to ConfigurationManagerTests
Browse files Browse the repository at this point in the history
through FakeTimeProvider
  • Loading branch information
Keegan Caruso committed Jan 10, 2025
1 parent 931a3bf commit c44ef1a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 7 deletions.
9 changes: 9 additions & 0 deletions build/dependenciesTest.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,13 @@
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net9.0'))">
<XunitRunnerVisualStudioVersion>3.0.0-pre.49</XunitRunnerVisualStudioVersion>
</PropertyGroup>

<!-- MicrosoftExtensionsTimeProviderTesting 8.x has a 6.0.0 target, 9.x does not. -->
<PropertyGroup Condition="'$(TargetFramework)' != 'net6.0'">
<MicrosoftExtensionsTimeProviderTestingVersion>9.0.0</MicrosoftExtensionsTimeProviderTestingVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net6.0'">
<MicrosoftExtensionsTimeProviderTestingVersion>8.10.0</MicrosoftExtensionsTimeProviderTestingVersion>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class ConfigurationManager<T> : 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;
Expand Down Expand Up @@ -151,7 +153,7 @@ public async Task<T> GetConfigurationAsync()
/// <remarks>If the time since the last call is less than <see cref="BaseConfigurationManager.AutomaticRefreshInterval"/> then <see cref="IConfigurationRetriever{T}.GetConfigurationAsync"/> is not called and the current Configuration is returned.</remarks>
public virtual async Task<T> GetConfigurationAsync(CancellationToken cancel)
{
if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow)
if (_currentConfiguration != null && _syncAfter > _timeProvider.GetUtcNow())
return _currentConfiguration;

Exception fetchMetadataFailure = null;
Expand Down Expand Up @@ -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)));
}

Expand All @@ -319,11 +321,12 @@ public override async Task<BaseConfiguration> GetBaseConfigurationAsync(Cancella
/// </summary>
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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,9 +20,6 @@

namespace Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests
{
/// <summary>
///
/// </summary>
public class ConfigurationManagerTests
{
/// <summary>
Expand Down Expand Up @@ -569,7 +567,7 @@ public static TheoryData<ConfigurationManagerTheoryData<OpenIdConnectConfigurati
public async Task CheckSyncAfterAndRefreshRequested()
{
// This test checks that the _syncAfter field is set correctly after a refresh.
var context = new CompareContext($"{this}.CheckSyncAfter");
var context = new CompareContext($"{this}.CheckSyncAfterAndRefreshRequested");

var docRetriever = new FileDocumentRetriever();
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever);
Expand Down Expand Up @@ -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<OpenIdConnectConfiguration>("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<OpenIdConnectConfiguration>("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<OpenIdConnectConfiguration> theoryData)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
<DelaySign>true</DelaySign>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'net472'">
<!-- This warning comes from Microsoft.Extensions.TimeProvider.Testing
see here for details: https://github.com/dotnet/extensions/issues/5162#issuecomment-2316266601-->
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>

<ItemGroup>
<None Update="JsonWebKeySet.json;JsonWebKeySetBadBase64Data.json;JsonWebKeySetBadX509Data.json;JsonWebKeySetEnd2End.json;JsonWebKeySetSingleX509Data.json;OpenIdConnectMetadata.json;OpenIdConnectMetadata2.json;JsonWebKeySetUnrecognizedKty.json;JsonWebKeySetBadRsaDataMissingComponent.json;OpenIdConnectMetadataBadBase64Data.json;OpenIdConnectMetadataBadX509Data.json;OpenIdConnectMetadataEnd2End.json;OpenIdConnectMetadataJsonWebKeySetBadUri.json;PingLabsJWKS.json;PingLabs-openid-configuration.json;;JsonWebKeySetEnd2EndEC.json;OpenIdConnectMetadataEnd2EndEC.json;OpenIdConnectMetadataUnrecognizedKty.json;OpenIdConnectMetadataBadRsaDataMissingComponent.json;OpenIdConnectMetadataEnd2EndAcrValuesLast.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand All @@ -27,6 +33,7 @@
<ItemGroup>
<PackageReference Include="System.Text.RegularExpressions" Version="$(SystemTextRegularExpressions)"/>
<PackageReference Include="System.Net.Http" Version="$(SystemNetHttp)"/>
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="$(MicrosoftExtensionsTimeProviderTestingVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit c44ef1a

Please sign in to comment.