@@ -26,7 +26,7 @@ public class ConfigurationManagerTests
2626 {
2727 /// <summary>
2828 /// This test reaches out the the internet to fetch the OpenIdConnectConfiguration from the specified metadata address.
29- /// There is no validaiton of the configuration. The validation is done in the OpenIdConnectConfigurationSerializationTests.Deserialize
29+ /// There is no validation of the configuration. The validation is done in the OpenIdConnectConfigurationSerializationTests.Deserialize
3030 /// against values obtained 2/2/2024
3131 /// </summary>
3232 /// <param name="theoryData"></param>
@@ -209,6 +209,94 @@ public async Task FetchMetadataFailureTest()
209209 TestUtilities . AssertFailIfErrors ( context ) ;
210210 }
211211
212+ [ Fact ]
213+ public async Task VerifyInterlockGuardForRequestRefresh ( )
214+ {
215+ ManualResetEvent waitEvent = new ManualResetEvent ( false ) ;
216+ ManualResetEvent signalEvent = new ManualResetEvent ( false ) ;
217+ InMemoryDocumentRetriever inMemoryDocumentRetriever = InMemoryDocumentRetrieverWithEvents ( waitEvent , signalEvent ) ;
218+
219+ var configurationManager = new ConfigurationManager < OpenIdConnectConfiguration > (
220+ "AADCommonV1Json" ,
221+ new OpenIdConnectConfigurationRetriever ( ) ,
222+ inMemoryDocumentRetriever ) ;
223+
224+ // populate the configurationManager with AADCommonV1Config
225+ TestUtilities . SetField ( configurationManager , "_currentConfiguration" , OpenIdConfigData . AADCommonV1Config ) ;
226+
227+ // InMemoryDocumentRetrieverWithEvents will block until waitEvent.Set() is called.
228+ // The first RequestRefresh will not have finished before the next RequestRefresh() is called.
229+ // The guard '_lastRequestRefresh' will not block as we set it to DateTimeOffset.MinValue.
230+ // Interlocked guard will block.
231+ // Configuration should be AADCommonV1Config
232+ signalEvent . Reset ( ) ;
233+ configurationManager . RequestRefresh ( ) ;
234+
235+ // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress
236+ // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished.
237+ signalEvent . WaitOne ( ) ;
238+
239+ // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event.
240+ configurationManager . MetadataAddress = "AADCommonV2Json" ;
241+ TestUtilities . SetField ( configurationManager , "_lastRequestRefresh" , DateTimeOffset . MinValue ) ;
242+ configurationManager . RequestRefresh ( ) ;
243+
244+ // Set the event to release the lock and let the previous retriever finish.
245+ waitEvent . Set ( ) ;
246+
247+ // Configuration should be AADCommonV1Config
248+ var configuration = await configurationManager . GetConfigurationAsync ( ) ;
249+ Assert . True ( configuration . Issuer . Equals ( OpenIdConfigData . AADCommonV1Config . Issuer ) ,
250+ $ "configuration.Issuer from configurationManager was not as expected," +
251+ $ "configuration.Issuer: '{ configuration . Issuer } ' != expected '{ OpenIdConfigData . AADCommonV1Config . Issuer } '.") ;
252+ }
253+
254+ [ Fact ]
255+ public async Task VerifyInterlockGuardForGetConfigurationAsync ( )
256+ {
257+ ManualResetEvent waitEvent = new ManualResetEvent ( false ) ;
258+ ManualResetEvent signalEvent = new ManualResetEvent ( false ) ;
259+
260+ InMemoryDocumentRetriever inMemoryDocumentRetriever = InMemoryDocumentRetrieverWithEvents ( waitEvent , signalEvent ) ;
261+ waitEvent . Set ( ) ;
262+
263+ var configurationManager = new ConfigurationManager < OpenIdConnectConfiguration > (
264+ "AADCommonV1Json" ,
265+ new OpenIdConnectConfigurationRetriever ( ) ,
266+ inMemoryDocumentRetriever ) ;
267+
268+ OpenIdConnectConfiguration configuration = await configurationManager . GetConfigurationAsync ( ) ;
269+
270+ // InMemoryDocumentRetrieverWithEvents will block until waitEvent.Set() is called.
271+ // The GetConfigurationAsync to update config will not have finished before the next GetConfigurationAsync() is called.
272+ // The guard '_syncAfter' will not block as we set it to DateTimeOffset.MinValue.
273+ // Interlocked guard should block.
274+ // Configuration should be AADCommonV1Config
275+
276+ waitEvent . Reset ( ) ;
277+ signalEvent . Reset ( ) ;
278+
279+ TestUtilities . SetField ( configurationManager , "_syncAfter" , DateTimeOffset . MinValue ) ;
280+ await configurationManager . GetConfigurationAsync ( CancellationToken . None ) ;
281+
282+ // InMemoryDocumentRetrieverWithEvents will signal when it is OK to change the MetadataAddress
283+ // otherwise, it may be the case that the MetadataAddress is changed before the previous Task has finished.
284+ signalEvent . WaitOne ( ) ;
285+
286+ // AADCommonV1Json would have been passed to the the previous retriever, which is blocked on an event.
287+ configurationManager . MetadataAddress = "AADCommonV2Json" ;
288+ await configurationManager . GetConfigurationAsync ( CancellationToken . None ) ;
289+
290+ // Set the event to release the lock and let the previous retriever finish.
291+ waitEvent . Set ( ) ;
292+
293+ // Configuration should be AADCommonV1Config
294+ configuration = await configurationManager . GetConfigurationAsync ( ) ;
295+ Assert . True ( configuration . Issuer . Equals ( OpenIdConfigData . AADCommonV1Config . Issuer ) ,
296+ $ "configuration.Issuer from configurationManager was not as expected," +
297+ $ " configuration.Issuer: '{ configuration . Issuer } ' != expected: '{ OpenIdConfigData . AADCommonV1Config . Issuer } '.") ;
298+ }
299+
212300 [ Fact ]
213301 public async Task BootstrapRefreshIntervalTest ( )
214302 {
@@ -814,6 +902,20 @@ public static TheoryData<ConfigurationManagerTheoryData<OpenIdConnectConfigurati
814902 { "https://login.microsoftonline.com/common/discovery/v2.0/keys" , OpenIdConfigData . AADCommonV2JwksString }
815903 } ) ;
816904
905+ private static InMemoryDocumentRetriever InMemoryDocumentRetrieverWithEvents ( ManualResetEvent waitEvent , ManualResetEvent signalEvent )
906+ {
907+ return new InMemoryDocumentRetriever (
908+ new Dictionary < string , string >
909+ {
910+ { "AADCommonV1Json" , OpenIdConfigData . AADCommonV1Json } ,
911+ { "https://login.microsoftonline.com/common/discovery/keys" , OpenIdConfigData . AADCommonV1JwksString } ,
912+ { "AADCommonV2Json" , OpenIdConfigData . AADCommonV2Json } ,
913+ { "https://login.microsoftonline.com/common/discovery/v2.0/keys" , OpenIdConfigData . AADCommonV2JwksString }
914+ } ,
915+ waitEvent ,
916+ signalEvent ) ;
917+ }
918+
817919 public class ConfigurationManagerTheoryData < T > : TheoryDataBase where T : class
818920 {
819921 public ConfigurationManager < T > ConfigurationManager { get ; set ; }
0 commit comments