From 12e89cc989aa63c39c5a6f424ef7bfc0a1ce4beb Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 23 Apr 2025 17:17:39 -0400 Subject: [PATCH 01/68] Retry policies are now pre request instead of per HttpManager. --- .../Http/HttpManager.cs | 53 +++++++-- .../Http/HttpManagerFactory.cs | 19 +--- .../Http/IHttpManager.cs | 2 + .../Http/LinearRetryPolicy.cs | 7 +- .../AbstractManagedIdentity.cs | 6 +- .../ImdsManagedIdentitySource.cs | 3 + .../ManagedIdentity/ManagedIdentityRequest.cs | 19 +++- .../Core/Mocks/MockHttpManager.cs | 2 + .../Infrastructure/MsiProxyHttpManager.cs | 1 + .../Helpers/ParallelRequestMockHandler.cs | 1 + .../ManagedIdentityTests.cs | 105 ++++++++++++++++++ 11 files changed, 185 insertions(+), 33 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 6f0add34fb..4f228d8445 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -26,22 +26,41 @@ namespace Microsoft.Identity.Client.Http /// internal class HttpManager : IHttpManager { + // referenced in unit tests, cannot be private + public const int DEFAULT_ESTS_MAX_RETRIES = 1; + // this will be overridden in the unit tests so that they run faster + public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; + protected readonly IMsalHttpClientFactory _httpClientFactory; - private readonly IRetryPolicy _retryPolicy; + private readonly bool _isManagedIdentity; + private readonly bool _withRetry; public long LastRequestDurationInMs { get; private set; } /// - /// A new instance of the HTTP manager with a retry *condition*. The retry policy hardcodes: - /// - the number of retries (1) - /// - the delay between retries (1 second) + /// Initializes a new instance of the class. /// + /// + /// An instance of used to create and manage instances. + /// This factory ensures proper reuse of to avoid socket exhaustion. + /// + /// + /// A boolean flag indicating whether the HTTP manager is being used in a managed identity context. + /// + /// + /// A boolean flag indicating whether the HTTP manager should enable retry logic for transient failures. + /// + /// + /// Thrown when is null. + /// public HttpManager( IMsalHttpClientFactory httpClientFactory, - IRetryPolicy retryPolicy) + bool isManagedIdentity, + bool withRetry) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - _retryPolicy = retryPolicy; + _isManagedIdentity = isManagedIdentity; + _withRetry = withRetry; } public async Task SendRequestAsync( @@ -54,8 +73,20 @@ public async Task SendRequestAsync( X509Certificate2 bindingCertificate, Func validateServerCert, CancellationToken cancellationToken, + IRetryPolicy retryPolicy = null, int retryCount = 0) { + // Use the default STS retry policy if the request is not for managed identity + // and a non-default STS retry policy is not provided. + // Skip this if statement the dev indicated that they do not want retry logic. + if (!_isManagedIdentity && retryPolicy == null && _withRetry) + { + retryPolicy = new LinearRetryPolicy( + DEFAULT_ESTS_RETRY_DELAY_MS, + DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + } + Exception timeoutException = null; HttpResponse response = null; @@ -102,9 +133,9 @@ public async Task SendRequestAsync( timeoutException = exception; } - while (_retryPolicy.pauseForRetry(response, timeoutException, retryCount)) + while (_withRetry && retryPolicy.pauseForRetry(response, timeoutException, retryCount)) { - logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {_retryPolicy.DelayInMilliseconds}ms."); + logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms."); return await SendRequestAsync( endpoint, headers, @@ -113,8 +144,10 @@ public async Task SendRequestAsync( logger, doNotThrow, bindingCertificate, - validateServerCert, cancellationToken: cancellationToken, - retryCount: retryCount) // Pass the updated retry count + validateServerCert, + cancellationToken, + retryPolicy, + retryCount) // Pass the updated retry count .ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index eb6ffa1c49..3d09a3103c 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.Identity.Client.Http { /// @@ -16,17 +10,10 @@ internal sealed class HttpManagerFactory { public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, - bool withRetry, - bool isManagedIdentity) + bool isManagedIdentity = false, + bool withRetry = true) { - if (!withRetry) - { - return new HttpManager(httpClientFactory, new NoRetryPolicy()); - } - - return isManagedIdentity ? - new HttpManager(httpClientFactory, new LinearRetryPolicy(1000, 3, HttpRetryConditions.ManagedIdentity)) : - new HttpManager(httpClientFactory, new LinearRetryPolicy(1000, 1, HttpRetryConditions.Sts)); + return new HttpManager(httpClientFactory, isManagedIdentity, withRetry); } } } diff --git a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs index 5a5372a3a8..c3ecb906a8 100644 --- a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs @@ -28,6 +28,7 @@ internal interface IHttpManager /// Certificate used for MTLS authentication. /// Callback to validate the server cert for service fabric managed identity flow. /// + /// Retry policy to be used for the request. /// Number of retries to be attempted in case of retriable status codes. /// Task SendRequestAsync( @@ -40,6 +41,7 @@ Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCertificate, CancellationToken cancellationToken, + IRetryPolicy retryPolicy = null, int retryCount = 0); } } diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs index 1c9a277a55..46293356dc 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs @@ -9,7 +9,9 @@ namespace Microsoft.Identity.Client.Http { internal class LinearRetryPolicy : IRetryPolicy { - + // referenced in unit tests, cannot be private + public static int numRetries { get; private set; } = 0; + private int _maxRetries; private readonly Func _retryCondition; public int DelayInMilliseconds { private set; get; } @@ -23,6 +25,9 @@ public LinearRetryPolicy(int delayMilliseconds, int maxRetries, Func AuthenticateAsync( doNotThrow: true, mtlsCertificate: null, validateServerCertificate: ValidateServerCertificate, - cancellationToken: cancellationToken).ConfigureAwait(false); + cancellationToken: cancellationToken, + retryPolicy: request.RetryPolicy).ConfigureAwait(false); } else { @@ -80,7 +81,8 @@ public virtual async Task AuthenticateAsync( doNotThrow: true, mtlsCertificate: null, validateServerCertificate: ValidateServerCertificate, - cancellationToken: cancellationToken) + cancellationToken: cancellationToken, + retryPolicy: request.RetryPolicy) .ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 40b1d4ca89..c7204ba6d5 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,6 +82,9 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } + // uncomment in follow-up IMDS retry policy PR + // request.RetryPolicy = new ImdsRetryPolicy(); + return request; } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 6eb5a5bba0..5091d5499c 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -3,16 +3,19 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.ManagedIdentity { internal class ManagedIdentityRequest { + // referenced in unit tests, cannot be private + public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + // this will be overridden in the unit tests so that they run faster + public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; + private readonly Uri _baseEndpoint; public HttpMethod Method { get; } @@ -23,13 +26,21 @@ internal class ManagedIdentityRequest public IDictionary QueryParameters { get; } - public ManagedIdentityRequest(HttpMethod method, Uri endpoint) + public IRetryPolicy RetryPolicy { get; set; } + + public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retryPolicy = null) { Method = method; _baseEndpoint = endpoint; Headers = new Dictionary(); BodyParameters = new Dictionary(); QueryParameters = new Dictionary(); + + IRetryPolicy defaultRetryPolicy = new LinearRetryPolicy( + DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS, + DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, + HttpRetryConditions.ManagedIdentity); + RetryPolicy = retryPolicy ?? defaultRetryPolicy; } public Uri ComputeUri() diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index ce177d958a..126bb6385b 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -116,6 +116,7 @@ public Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, + IRetryPolicy retryPolicy = null, int retryCount = 0) { return _httpManager.SendRequestAsync( @@ -127,6 +128,7 @@ public Task SendRequestAsync( doNotThrow, mtlsCertificate, validateServerCert, cancellationToken, + retryPolicy, retryCount); } } diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs index 81c491358e..88ce8269c9 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs @@ -50,6 +50,7 @@ public async Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, + IRetryPolicy retryPolicy = null, int retryCount = 0) { //Get token for the MSIHelperService diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs index 813433657a..4a0f5d0f44 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs @@ -40,6 +40,7 @@ public async Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, + IRetryPolicy retryPolicy = null, int retryCount = 0) { Interlocked.Increment(ref _requestCount); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 7a7c89929f..f25ee658ac 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; @@ -1301,5 +1302,109 @@ public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() Assert.AreEqual(SystemAssignedClientId, systemAssignedTokens[0].ClientId, "System-assigned ClientId mismatch in cache."); } } + + [DataTestMethod] + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + // (numRetries would be x2 if retry policy was NOT per request) + Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + // (numRetries would be x3 if retry policy was NOT per request) + Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } } } From 132ec53ae1a6f27124bf9091737baccd0d4df1b8 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 24 Apr 2025 17:01:56 -0400 Subject: [PATCH 02/68] Improved unit test --- .../Http/HttpManager.cs | 2 +- .../Http/IRetryPolicy.cs | 2 +- .../Http/LinearRetryPolicy.cs | 2 +- .../Http/NoRetryPolicy.cs | 2 +- .../ManagedIdentityTests.cs | 127 +++++------------- 5 files changed, 41 insertions(+), 94 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 4f228d8445..2a8a3ae7ec 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -133,7 +133,7 @@ public async Task SendRequestAsync( timeoutException = exception; } - while (_withRetry && retryPolicy.pauseForRetry(response, timeoutException, retryCount)) + while (_withRetry && retryPolicy.PauseForRetry(response, timeoutException, retryCount)) { logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms."); return await SendRequestAsync( diff --git a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs index 33650f0d0a..db3b466759 100644 --- a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs @@ -12,6 +12,6 @@ namespace Microsoft.Identity.Client.Http internal interface IRetryPolicy { int DelayInMilliseconds { get; } - bool pauseForRetry(HttpResponse response, Exception exception, int retryCount); + bool PauseForRetry(HttpResponse response, Exception exception, int retryCount); } } diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs index 46293356dc..a2a8f53799 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs @@ -23,7 +23,7 @@ public LinearRetryPolicy(int delayMilliseconds, int maxRetries, Func throw new NotImplementedException(); } - public bool pauseForRetry(HttpResponse response, Exception exception, int retryCount) + public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount) { return false; } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index f25ee658ac..d79753492f 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -19,6 +19,7 @@ using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenTelemetry.Resources; +using OpenTelemetry.Trace; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -651,77 +652,13 @@ await mi.AcquireTokenForManagedIdentity(Resource) } } - [DataTestMethod] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.RequestTimeout)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.InternalServerError)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.ServiceUnavailable)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.GatewayTimeout)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.NotFound)] - [DataRow(ManagedIdentitySource.Imds, ImdsEndpoint, HttpStatusCode.NotFound)] - [DataRow(ManagedIdentitySource.AzureArc, AzureArcEndpoint, HttpStatusCode.NotFound)] - [DataRow(ManagedIdentitySource.CloudShell, CloudShellEndpoint, HttpStatusCode.NotFound)] - [DataRow(ManagedIdentitySource.ServiceFabric, ServiceFabricEndpoint, HttpStatusCode.NotFound)] - [DataRow(ManagedIdentitySource.MachineLearning, MachineLearningEndpoint, HttpStatusCode.NotFound)] - public async Task ManagedIdentityTestRetryAsync(ManagedIdentitySource managedIdentitySource, string endpoint, HttpStatusCode statusCode) - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) - { - SetEnvironmentVariables(managedIdentitySource, endpoint); - - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disabling shared cache options to avoid cross test pollution. - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - httpManager.AddManagedIdentityMockHandler( - endpoint, - Resource, - "", - managedIdentitySource, - statusCode: statusCode); - - httpManager.AddManagedIdentityMockHandler( - endpoint, - Resource, - "", - managedIdentitySource, - statusCode: statusCode); - - httpManager.AddManagedIdentityMockHandler( - endpoint, - Resource, - "", - managedIdentitySource, - statusCode: statusCode); - - httpManager.AddManagedIdentityMockHandler( - endpoint, - Resource, - "", - managedIdentitySource, - statusCode: statusCode); - - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => - await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); - - Assert.IsNotNull(ex); - Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); - Assert.AreEqual(managedIdentitySource.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); - Assert.IsTrue(ex.IsRetryable); - } - } - [TestMethod] public async Task SystemAssignedManagedIdentityApiIdTestAsync() { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) @@ -1304,17 +1241,27 @@ public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() } [DataTestMethod] - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.NotFound)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.RequestTimeout)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, 429)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.InternalServerError)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.ServiceUnavailable)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.GatewayTimeout)] + [DataRow(ManagedIdentitySource.AzureArc, AzureArcEndpoint, HttpStatusCode.GatewayTimeout)] + [DataRow(ManagedIdentitySource.CloudShell, CloudShellEndpoint, HttpStatusCode.GatewayTimeout)] + [DataRow(ManagedIdentitySource.Imds, ImdsEndpoint, HttpStatusCode.GatewayTimeout)] + [DataRow(ManagedIdentitySource.MachineLearning, MachineLearningEndpoint, HttpStatusCode.GatewayTimeout)] + [DataRow(ManagedIdentitySource.ServiceFabric, ServiceFabricEndpoint, HttpStatusCode.GatewayTimeout)] + public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint, + HttpStatusCode statusCode) { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + SetEnvironmentVariables(managedIdentitySource, endpoint); + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -1323,22 +1270,22 @@ public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync var mi = miBuilder.Build(); - // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) + // Simulate permanent errors (to trigger the maximum number of retries) + const int NUM_ERRORS = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_ERRORS; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, - ManagedIdentityTests.Resource, + Resource, "", managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + statusCode: statusCode); } MsalServiceException msalException = null; try { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) @@ -1347,15 +1294,15 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that the first request was made and retried 3 times - Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + // 4 total: request + 3 retries + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); - for (int i = 0; i < NUM_500; i++) + for (int i = 0; i < NUM_ERRORS; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, - ManagedIdentityTests.Resource, + Resource, "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError); @@ -1364,7 +1311,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) msalException = null; try { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) @@ -1373,16 +1320,16 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that the second request was made and retried 3 times + // 4 total: request + 3 retries // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); - for (int i = 0; i < NUM_500; i++) + for (int i = 0; i < NUM_ERRORS; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, - ManagedIdentityTests.Resource, + Resource, "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError); @@ -1391,7 +1338,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) msalException = null; try { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false); } catch (Exception ex) @@ -1400,9 +1347,9 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that the third request was made and retried 3 times + // 4 total: request + 3 retries // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } From fc7eafa4bc40bc7f7ae82af67eefc1b1c7c2aa82 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 25 Apr 2025 15:32:16 -0400 Subject: [PATCH 03/68] Addressed some GitHub feedback --- .../AppConfig/ApplicationConfiguration.cs | 1 + .../Microsoft.Identity.Client/Http/HttpManager.cs | 12 ++++++------ .../Http/HttpManagerFactory.cs | 4 ++-- .../Internal/ServiceBundle.cs | 2 +- .../Core/Mocks/MockHttpManager.cs | 10 +++++----- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index d3ed7f9948..0ca48cb7e5 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -117,6 +117,7 @@ public string ClientVersion public bool RetryOnServerErrors { get; set; } = true; public ManagedIdentityId ManagedIdentityId { get; internal set; } + public bool DisableInternalRetries { get; internal set; } public bool IsManagedIdentity { get; } public bool IsConfidentialClient { get; } diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 2a8a3ae7ec..fcf56faa22 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -33,7 +33,7 @@ internal class HttpManager : IHttpManager protected readonly IMsalHttpClientFactory _httpClientFactory; private readonly bool _isManagedIdentity; - private readonly bool _withRetry; + private readonly bool _disableInternalRetries; public long LastRequestDurationInMs { get; private set; } /// @@ -46,7 +46,7 @@ internal class HttpManager : IHttpManager /// /// A boolean flag indicating whether the HTTP manager is being used in a managed identity context. /// - /// + /// /// A boolean flag indicating whether the HTTP manager should enable retry logic for transient failures. /// /// @@ -55,12 +55,12 @@ internal class HttpManager : IHttpManager public HttpManager( IMsalHttpClientFactory httpClientFactory, bool isManagedIdentity, - bool withRetry) + bool disableInternalRetries) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); _isManagedIdentity = isManagedIdentity; - _withRetry = withRetry; + _disableInternalRetries = disableInternalRetries; } public async Task SendRequestAsync( @@ -79,7 +79,7 @@ public async Task SendRequestAsync( // Use the default STS retry policy if the request is not for managed identity // and a non-default STS retry policy is not provided. // Skip this if statement the dev indicated that they do not want retry logic. - if (!_isManagedIdentity && retryPolicy == null && _withRetry) + if (!_isManagedIdentity && retryPolicy == null && !_disableInternalRetries) { retryPolicy = new LinearRetryPolicy( DEFAULT_ESTS_RETRY_DELAY_MS, @@ -133,7 +133,7 @@ public async Task SendRequestAsync( timeoutException = exception; } - while (_withRetry && retryPolicy.PauseForRetry(response, timeoutException, retryCount)) + while (!_disableInternalRetries && retryPolicy.PauseForRetry(response, timeoutException, retryCount)) { logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms."); return await SendRequestAsync( diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 3d09a3103c..6f8a821610 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -11,9 +11,9 @@ internal sealed class HttpManagerFactory public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, bool isManagedIdentity = false, - bool withRetry = true) + bool disableInternalRetries = false) { - return new HttpManager(httpClientFactory, isManagedIdentity, withRetry); + return new HttpManager(httpClientFactory, isManagedIdentity, disableInternalRetries); } } } diff --git a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs index f2c89be17c..098327299d 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs @@ -32,7 +32,7 @@ internal ServiceBundle( HttpManager = config.HttpManager ?? HttpManagerFactory.GetHttpManager(config.HttpClientFactory ?? PlatformProxy.CreateDefaultHttpClientFactory(), - config.RetryOnServerErrors, config.IsManagedIdentity); + config.IsManagedIdentity, config.DisableInternalRetries); HttpTelemetryManager = new HttpTelemetryManager(); diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 126bb6385b..46b45d1107 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -38,7 +38,7 @@ public MockHttpManager(string testName = null, } public MockHttpManager( - bool retry, + bool disableInternalRetries, string testName = null, bool isManagedIdentity = false, Func messageHandlerFunc = null, @@ -47,12 +47,12 @@ public MockHttpManager( _httpManager = invokeNonMtlsHttpManagerFactory ? HttpManagerFactory.GetHttpManager( new MockNonMtlsHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName), - retry, - isManagedIdentity) + isManagedIdentity, + disableInternalRetries) : HttpManagerFactory.GetHttpManager( new MockHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName), - retry, - isManagedIdentity); + isManagedIdentity, + disableInternalRetries); _testName = testName; } From dc831fef2e1537f793be1885e17e6b8be4b115df Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 29 Apr 2025 16:29:47 -0400 Subject: [PATCH 04/68] Implemented GitHub feedback --- .../Http/HttpManager.cs | 24 +- .../Http/HttpManagerFactory.cs | 5 +- .../Http/IHttpManager.cs | 2 +- .../Http/LinearRetryPolicy.cs | 3 + .../Instance/Region/RegionManager.cs | 16 +- .../Validation/AdfsAuthorityValidator.cs | 13 +- .../Internal/ServiceBundle.cs | 5 +- .../AzureArcManagedIdentitySource.cs | 8 +- .../OAuth2/OAuth2Client.cs | 12 +- .../WsTrust/WsTrustWebRequestManager.cs | 64 ++-- .../Core/Mocks/MockHttpManager.cs | 8 +- .../Infrastructure/MsiProxyHttpManager.cs | 2 +- .../CoreTests/HttpTests/HttpManagerTests.cs | 300 ++++++++++-------- .../Helpers/ParallelRequestMockHandler.cs | 2 +- .../ManagedIdentityTests/AppServiceTests.cs | 2 +- .../ManagedIdentityTests/AzureArcTests.cs | 8 +- .../ManagedIdentityTests/CloudShellTests.cs | 4 +- .../ManagedIdentityTests/ImdsTests.cs | 2 +- .../MachineLearningTests.cs | 2 +- .../ManagedIdentityTests.cs | 50 ++- .../ServiceFabricTests.cs | 6 +- .../PublicApiTests/RetryPolicyTests.cs | 2 +- .../OTelInstrumentationTests.cs | 2 +- 23 files changed, 295 insertions(+), 247 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index fcf56faa22..4e54e426f5 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -26,13 +26,7 @@ namespace Microsoft.Identity.Client.Http /// internal class HttpManager : IHttpManager { - // referenced in unit tests, cannot be private - public const int DEFAULT_ESTS_MAX_RETRIES = 1; - // this will be overridden in the unit tests so that they run faster - public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; - protected readonly IMsalHttpClientFactory _httpClientFactory; - private readonly bool _isManagedIdentity; private readonly bool _disableInternalRetries; public long LastRequestDurationInMs { get; private set; } @@ -43,9 +37,6 @@ internal class HttpManager : IHttpManager /// An instance of used to create and manage instances. /// This factory ensures proper reuse of to avoid socket exhaustion. /// - /// - /// A boolean flag indicating whether the HTTP manager is being used in a managed identity context. - /// /// /// A boolean flag indicating whether the HTTP manager should enable retry logic for transient failures. /// @@ -54,12 +45,10 @@ internal class HttpManager : IHttpManager /// public HttpManager( IMsalHttpClientFactory httpClientFactory, - bool isManagedIdentity, bool disableInternalRetries) { _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - _isManagedIdentity = isManagedIdentity; _disableInternalRetries = disableInternalRetries; } @@ -73,20 +62,9 @@ public async Task SendRequestAsync( X509Certificate2 bindingCertificate, Func validateServerCert, CancellationToken cancellationToken, - IRetryPolicy retryPolicy = null, + IRetryPolicy retryPolicy, int retryCount = 0) { - // Use the default STS retry policy if the request is not for managed identity - // and a non-default STS retry policy is not provided. - // Skip this if statement the dev indicated that they do not want retry logic. - if (!_isManagedIdentity && retryPolicy == null && !_disableInternalRetries) - { - retryPolicy = new LinearRetryPolicy( - DEFAULT_ESTS_RETRY_DELAY_MS, - DEFAULT_ESTS_MAX_RETRIES, - HttpRetryConditions.Sts); - } - Exception timeoutException = null; HttpResponse response = null; diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 6f8a821610..120735fbeb 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -4,16 +4,15 @@ namespace Microsoft.Identity.Client.Http { /// - /// Factory to return the instance of HttpManager based on retry configuration and type of MSAL application. + /// Factory to return the instance of HttpManager based on type of MSAL application. /// internal sealed class HttpManagerFactory { public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, - bool isManagedIdentity = false, bool disableInternalRetries = false) { - return new HttpManager(httpClientFactory, isManagedIdentity, disableInternalRetries); + return new HttpManager(httpClientFactory, disableInternalRetries); } } } diff --git a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs index c3ecb906a8..87cd955a2a 100644 --- a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs @@ -41,7 +41,7 @@ Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCertificate, CancellationToken cancellationToken, - IRetryPolicy retryPolicy = null, + IRetryPolicy retryPolicy, int retryCount = 0); } } diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs index a2a8f53799..f75f9ce617 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs @@ -11,6 +11,9 @@ internal class LinearRetryPolicy : IRetryPolicy { // referenced in unit tests, cannot be private public static int numRetries { get; private set; } = 0; + public const int DEFAULT_ESTS_MAX_RETRIES = 1; + // this will be overridden in the unit tests so that they run faster + public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; private int _maxRetries; private readonly Func _retryCondition; diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index b6dfc429d8..f74708dc49 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -46,6 +46,11 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static bool s_failedAutoDiscovery = false; private static string s_regionDiscoveryDetails; + private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + public RegionManager( IHttpManager httpManager, int imdsCallTimeout = 2000, @@ -206,7 +211,9 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation logger: logger, doNotThrow: false, mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken)) + validateServerCertificate: null, + cancellationToken: GetCancellationToken(requestCancellationToken), + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); // A bad request occurs when the version in the IMDS call is no longer supported. @@ -222,7 +229,9 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation logger: logger, doNotThrow: false, mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken)) + validateServerCertificate: null, + cancellationToken: GetCancellationToken(requestCancellationToken), + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); // Call again with updated version } @@ -324,7 +333,8 @@ private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dict doNotThrow: false, mtlsCertificate: null, validateServerCertificate: null, - cancellationToken: GetCancellationToken(userCancellationToken)) + cancellationToken: GetCancellationToken(userCancellationToken), + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); // When IMDS endpoint is called without the api version query param, bad request response comes back with latest version. diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index f32f73d2a5..3462c75b90 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -6,6 +6,7 @@ using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; @@ -28,7 +29,12 @@ public async Task ValidateAuthorityAsync( var resource = $"https://{authorityInfo.Host}"; string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); - Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( + LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + + Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( new Uri(webFingerUrl), null, body: null, @@ -36,7 +42,10 @@ public async Task ValidateAuthorityAsync( logger: _requestContext.Logger, doNotThrow: false, mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: _requestContext.UserCancellationToken) + validateServerCertificate: null, + cancellationToken: _requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy + ) .ConfigureAwait(false); if (httpResponse.StatusCode != HttpStatusCode.OK) diff --git a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs index 098327299d..4078092116 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ServiceBundle.cs @@ -31,8 +31,9 @@ internal ServiceBundle( PlatformProxy = config.PlatformProxy ?? PlatformProxyFactory.CreatePlatformProxy(ApplicationLogger); HttpManager = config.HttpManager ?? - HttpManagerFactory.GetHttpManager(config.HttpClientFactory ?? PlatformProxy.CreateDefaultHttpClientFactory(), - config.IsManagedIdentity, config.DisableInternalRetries); + HttpManagerFactory.GetHttpManager( + config.HttpClientFactory ?? PlatformProxy.CreateDefaultHttpClientFactory(), + config.DisableInternalRetries); HttpTelemetryManager = new HttpTelemetryManager(); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index b2235401ba..9f4338024a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -123,6 +123,11 @@ protected override async Task HandleResponseAsync( _requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request."); request.Headers.Add("Authorization", authHeaderValue); + LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( request.ComputeUri(), request.Headers, @@ -132,7 +137,8 @@ protected override async Task HandleResponseAsync( doNotThrow: false, mtlsCertificate: null, validateServerCertificate: null, - cancellationToken: cancellationToken) + cancellationToken: cancellationToken, + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index a63f9298be..d48ed8eeb5 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -41,6 +41,10 @@ internal class OAuth2Client private readonly IDictionary _bodyParameters = new Dictionary(); private readonly IHttpManager _httpManager; private readonly X509Certificate2 _mtlsCertificate; + private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager, X509Certificate2 mtlsCertificate) { @@ -139,7 +143,9 @@ internal async Task ExecuteRequestAsync( logger: requestContext.Logger, doNotThrow: false, mtlsCertificate: _mtlsCertificate, - validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken) + validateServerCertificate: null, + cancellationToken: requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); } else @@ -152,7 +158,9 @@ internal async Task ExecuteRequestAsync( logger: requestContext.Logger, doNotThrow: false, mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken) + validateServerCertificate: null, + cancellationToken: requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); } } diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index 7c8ab739fd..e6a8edc10d 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -21,6 +21,10 @@ namespace Microsoft.Identity.Client.WsTrust internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; + private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); public WsTrustWebRequestManager(IHttpManager httpManager) { @@ -44,15 +48,17 @@ public async Task GetMexDocumentAsync(string federationMetadataUrl, var uri = new UriBuilder(federationMetadataUrl); HttpResponse httpResponse = await _httpManager.SendRequestAsync( - uri.Uri, - msalIdParams, - body: null, - method: HttpMethod.Get, - logger: requestContext.Logger, - doNotThrow: false, - mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken) - .ConfigureAwait(false); + uri.Uri, + msalIdParams, + body: null, + method: HttpMethod.Get, + logger: requestContext.Logger, + doNotThrow: false, + mtlsCertificate: null, + validateServerCertificate: null, + cancellationToken: requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK) { @@ -100,15 +106,17 @@ public async Task GetWsTrustResponseAsync( Encoding.UTF8, "application/soap+xml"); HttpResponse resp = await _httpManager.SendRequestAsync( - wsTrustEndpoint.Uri, - headers, - body: body, - method: HttpMethod.Post, - logger: requestContext.Logger, - doNotThrow: true, - mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken) - .ConfigureAwait(false); + wsTrustEndpoint.Uri, + headers, + body: body, + method: HttpMethod.Post, + logger: requestContext.Logger, + doNotThrow: true, + mtlsCertificate: null, + validateServerCertificate: null, + cancellationToken: requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); if (resp.StatusCode != System.Net.HttpStatusCode.OK) { @@ -175,15 +183,17 @@ public async Task GetUserRealmAsync( var uri = new UriBuilder(userRealmUriPrefix + userName + "?api-version=1.0").Uri; var httpResponse = await _httpManager.SendRequestAsync( - uri, - msalIdParams, - body: null, - method: HttpMethod.Get, - logger: requestContext.Logger, - doNotThrow: false, - mtlsCertificate: null, - validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken) - .ConfigureAwait(false); + uri, + msalIdParams, + body: null, + method: HttpMethod.Get, + logger: requestContext.Logger, + doNotThrow: false, + mtlsCertificate: null, + validateServerCertificate: null, + cancellationToken: requestContext.UserCancellationToken, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK) { diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 46b45d1107..3bd6046af5 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -29,29 +29,25 @@ internal sealed class MockHttpManager : IHttpManager, private readonly IHttpManager _httpManager; public MockHttpManager(string testName = null, - bool isManagedIdentity = false, Func messageHandlerFunc = null, Func validateServerCertificateCallback = null, bool invokeNonMtlsHttpManagerFactory = false) : - this(true, testName, isManagedIdentity, messageHandlerFunc, invokeNonMtlsHttpManagerFactory) + this(true, testName, messageHandlerFunc, invokeNonMtlsHttpManagerFactory) { } public MockHttpManager( bool disableInternalRetries, string testName = null, - bool isManagedIdentity = false, Func messageHandlerFunc = null, bool invokeNonMtlsHttpManagerFactory = false) { _httpManager = invokeNonMtlsHttpManagerFactory ? HttpManagerFactory.GetHttpManager( new MockNonMtlsHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName), - isManagedIdentity, disableInternalRetries) : HttpManagerFactory.GetHttpManager( new MockHttpClientFactory(messageHandlerFunc, _httpMessageHandlerQueue, testName), - isManagedIdentity, disableInternalRetries); _testName = testName; @@ -116,7 +112,7 @@ public Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, - IRetryPolicy retryPolicy = null, + IRetryPolicy retryPolicy, int retryCount = 0) { return _httpManager.SendRequestAsync( diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs index 88ce8269c9..7520306f52 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs @@ -50,7 +50,7 @@ public async Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, - IRetryPolicy retryPolicy = null, + IRetryPolicy retryPolicy, int retryCount = 0) { //Get token for the MSIHelperService diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index 6764bbb981..c5c3403cf2 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -23,6 +23,11 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { + LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, + LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + [TestInitialize] public void TestInitialize() { @@ -52,15 +57,17 @@ public async Task MtlsCertAsync() handler.ExpectedMtlsBindingCertificate = cert; string expectedContent = mock.Content.ReadAsStringAsync().Result; var response = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), - headers: null, - body: new FormUrlEncodedContent(bodyParameters), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: cert, - validateServerCert: null, cancellationToken: default) - .ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), + headers: null, + body: new FormUrlEncodedContent(bodyParameters), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: cert, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.IsNotNull(response); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); @@ -88,16 +95,19 @@ public async Task MtlsCertAndValidateCallbackFailsAsync() using (var httpManager = new MockHttpManager()) { - await Assert.ThrowsExceptionAsync(() => httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), - headers: null, - body: new FormUrlEncodedContent(bodyParameters), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: cert, - validateServerCert: customCallback, cancellationToken: default)) - .ConfigureAwait(false); + await Assert.ThrowsExceptionAsync(() => + httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), + headers: null, + body: new FormUrlEncodedContent(bodyParameters), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: cert, + validateServerCert: customCallback, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) + .ConfigureAwait(false); } } @@ -124,15 +134,17 @@ public async Task TestHttpManagerWithValidationCallbackAsync() handler.ServerCertificateCustomValidationCallback = customCallback; string expectedContent = mock.Content.ReadAsStringAsync().Result; var response = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), - headers: null, - body: new FormUrlEncodedContent(bodyParameters), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: customCallback, cancellationToken: default) - .ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), + headers: null, + body: new FormUrlEncodedContent(bodyParameters), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: customCallback, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.IsNotNull(response); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); @@ -150,15 +162,17 @@ public async Task TestSendPostNullHeaderNullBodyAsync() httpManager.AddResponseMockHandlerForPost(mock); var response = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token"), - headers: null, - body: null, - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default) - .ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token"), + headers: null, + body: null, + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.IsNotNull(response); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); @@ -190,15 +204,17 @@ public async Task TestSendPostNoFailureAsync() httpManager.AddResponseMockHandlerForPost(mock, bodyParameters, headers); var response = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), - headers: null, - body: new FormUrlEncodedContent(bodyParameters), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default) - .ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/v2.0/token?key1=qp1&key2=qp2"), + headers: null, + body: new FormUrlEncodedContent(bodyParameters), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.IsNotNull(response); Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); @@ -220,14 +236,16 @@ public async Task TestSendGetNoFailureAsync() httpManager.AddSuccessTokenResponseMockHandlerForGet(queryParameters: queryParams); var response = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token?key1=qp1&key2=qp2"), - headers: null, - body: null, - method: HttpMethod.Get, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default) + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token?key1=qp1&key2=qp2"), + headers: null, + body: null, + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -252,18 +270,18 @@ public async Task TestSendGetWithCanceledTokenAsync() CancellationTokenSource cts = new CancellationTokenSource(); cts.Cancel(); - await Assert.ThrowsExceptionAsync( - () => - + await Assert.ThrowsExceptionAsync(() => httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token?key1=qp1&key2=qp2"), - headers: queryParams, - body: null, - method: HttpMethod.Get, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: cts.Token)) + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token?key1=qp1&key2=qp2"), + headers: queryParams, + body: null, + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: cts.Token, + retryPolicy: _linearRetryPolicy)) .ConfigureAwait(false); } } @@ -271,14 +289,12 @@ await Assert.ThrowsExceptionAsync( [TestMethod] public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() { - using (var httpManager = new MockHttpManager(retry: false)) + using (var httpManager = new MockHttpManager()) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); - var ex = await Assert.ThrowsExceptionAsync( - () => - - httpManager.SendRequestAsync( + var ex = await Assert.ThrowsExceptionAsync(() => + httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: null, body: null, @@ -286,11 +302,12 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() logger: Substitute.For(), doNotThrow: false, mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)) + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); - } } @@ -302,19 +319,19 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.InternalServerError); - var ex = await Assert.ThrowsExceptionAsync( - () => - - httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: null, - method: HttpMethod.Get, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)) - .ConfigureAwait(false); + var ex = await Assert.ThrowsExceptionAsync(() => + httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: null, + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) + .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); } @@ -333,17 +350,18 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet new System.Net.Http.Headers.RetryConditionHeaderValue(TimeSpan.FromSeconds(1)) : new System.Net.Http.Headers.RetryConditionHeaderValue(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2)); - var exc = await AssertException.TaskThrowsAsync( - () => - httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: null, - method: HttpMethod.Get, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)) + var exc = await AssertException.TaskThrowsAsync(() => + httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: null, + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); @@ -360,14 +378,17 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.BadGateway); var msalHttpResponse = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: new StringContent("body"), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: true, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default).ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: new StringContent("body"), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: true, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); } @@ -381,9 +402,8 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.GatewayTimeout); httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - var exc = await AssertException.TaskThrowsAsync( - () => - httpManager.SendRequestAsync( + var exc = await AssertException.TaskThrowsAsync(() => + httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: null, body: null, @@ -391,7 +411,10 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() logger: Substitute.For(), doNotThrow: false, mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)).ConfigureAwait(false); + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) + .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); } @@ -405,17 +428,19 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); - var exc = await AssertException.TaskThrowsAsync( - () => - httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: null, - method: HttpMethod.Get, - logger: Substitute.For(), - doNotThrow: false, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)).ConfigureAwait(false); + var exc = await AssertException.TaskThrowsAsync(() => + httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: null, + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: false, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) + .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); @@ -430,9 +455,8 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); - var exc = await AssertException.TaskThrowsAsync( - () => - httpManager.SendRequestAsync( + var exc = await AssertException.TaskThrowsAsync(() => + httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: new Dictionary(), body: new FormUrlEncodedContent(new Dictionary()), @@ -440,7 +464,10 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() logger: Substitute.For(), doNotThrow: false, mtlsCertificate: null, - validateServerCert: null, cancellationToken: default)).ConfigureAwait(false); + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy)) + .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); } @@ -451,13 +478,13 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() [DataRow(false, false)] [DataRow(true, true)] [DataRow(false, true)] - public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool retry, bool isManagedIdentity) + public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInternalRetries, bool isManagedIdentity) { - using (var httpManager = new MockHttpManager(retry, isManagedIdentity: isManagedIdentity)) + using (var httpManager = new MockHttpManager(disableInternalRetries)) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - if (retry) + if (!disableInternalRetries) { //Adding second response for retry httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); @@ -472,14 +499,17 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool retry, bool is } var msalHttpResponse = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: new StringContent("body"), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: true, - mtlsCertificate: null, - validateServerCert: null, cancellationToken: default).ConfigureAwait(false); + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: new StringContent("body"), + method: HttpMethod.Post, + logger: Substitute.For(), + doNotThrow: true, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: _linearRetryPolicy) + .ConfigureAwait(false); Assert.IsNotNull(msalHttpResponse); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, msalHttpResponse.StatusCode); diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs index 4a0f5d0f44..740b1f7a03 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs @@ -40,7 +40,7 @@ public async Task SendRequestAsync( X509Certificate2 mtlsCertificate, Func validateServerCert, CancellationToken cancellationToken, - IRetryPolicy retryPolicy = null, + IRetryPolicy retryPolicy, int retryCount = 0) { Interlocked.Increment(ref _requestCount); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs index 5b7ca38f3d..8ed55e6b4d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs @@ -27,7 +27,7 @@ public class AppServiceTests : TestBase public async Task AppServiceInvalidEndpointAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, "127.0.0.1:41564/msi/token"); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs index 40f5d68564..fdc1bde4f9 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AzureArcTests.cs @@ -30,7 +30,7 @@ public class AzureArcTests : TestBase public async Task AzureArcUserAssignedManagedIdentityNotSupportedAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AzureArc, ManagedIdentityTests.AzureArcEndpoint); @@ -54,7 +54,7 @@ await mi.AcquireTokenForManagedIdentity("scope") public async Task AzureArcAuthHeaderMissingAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AzureArc, ManagedIdentityTests.AzureArcEndpoint); @@ -86,7 +86,7 @@ await mi.AcquireTokenForManagedIdentity("scope") public async Task AzureArcAuthHeaderInvalidAsync(string filename, string errorMessage) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AzureArc, ManagedIdentityTests.AzureArcEndpoint); @@ -115,7 +115,7 @@ await mi.AcquireTokenForManagedIdentity("scope") public async Task AzureArcInvalidEndpointAsync() { using(new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AzureArc, "localhost/token"); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs index bf96544c49..8463839fb4 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/CloudShellTests.cs @@ -28,7 +28,7 @@ public class CloudShellTests : TestBase public async Task CloudShellUserAssignedManagedIdentityNotSupportedAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.CloudShell, ManagedIdentityTests.CloudShellEndpoint); @@ -52,7 +52,7 @@ await mi.AcquireTokenForManagedIdentity("scope") public async Task CloudShellInvalidEndpointAsync() { using(new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.CloudShell, "localhost/token"); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 7d117f7de7..6fe43611ee 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -24,7 +24,7 @@ public class ImdsTests : TestBase public async Task ImdsErrorHandlingTestAsync(HttpStatusCode statusCode, string expectedErrorSubstring, int expectedAttempts) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, "http://169.254.169.254"); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/MachineLearningTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/MachineLearningTests.cs index bc9ea5e844..a63c469b17 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/MachineLearningTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/MachineLearningTests.cs @@ -25,7 +25,7 @@ public class MachineLearningTests : TestBase public async Task MachineLearningTestsInvalidEndpointAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.MachineLearning, "127.0.0.1:41564/msi/token"); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index d79753492f..8472e28c28 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -18,8 +18,6 @@ using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -82,7 +80,7 @@ public async Task ManagedIdentityHappyPathAsync( ManagedIdentitySource managedIdentitySource) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -135,7 +133,7 @@ public async Task ManagedIdentityUserAssignedHappyPathAsync( UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -182,7 +180,7 @@ public async Task ManagedIdentityDifferentScopesTestAsync( ManagedIdentitySource managedIdentitySource) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -242,7 +240,7 @@ public async Task ManagedIdentityForceRefreshTestAsync( ManagedIdentitySource managedIdentitySource) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -303,7 +301,7 @@ public async Task ManagedIdentityWithClaimsAndCapabilitiesTestAsync( ManagedIdentitySource managedIdentitySource) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -365,7 +363,7 @@ public async Task ManagedIdentityWithClaimsTestAsync( ManagedIdentitySource managedIdentitySource) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -436,7 +434,7 @@ public async Task ManagedIdentityWithClaimsTestAsync( public async Task ManagedIdentityTestWrongScopeAsync(string resource, ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -479,7 +477,7 @@ await mi.AcquireTokenForManagedIdentity(resource) public async Task ManagedIdentityTestErrorResponseParsing(string errorResponse, string[] expectedInErrorResponse) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -522,7 +520,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) public async Task ManagedIdentityTestNullOrEmptyScopeAsync(string resource, ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -544,7 +542,7 @@ await mi.AcquireTokenForManagedIdentity(resource) public async Task ManagedIdentityErrorResponseNoPayloadTestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -586,7 +584,7 @@ await mi.AcquireTokenForManagedIdentity("scope") public async Task ManagedIdentityNullResponseAsync(ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -626,7 +624,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) public async Task ManagedIdentityUnreachableNetworkAsync(ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -656,7 +654,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) public async Task SystemAssignedManagedIdentityApiIdTestAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -690,7 +688,7 @@ public async Task SystemAssignedManagedIdentityApiIdTestAsync() public async Task UserAssignedManagedIdentityApiIdTestAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -725,7 +723,7 @@ public async Task UserAssignedManagedIdentityApiIdTestAsync() public async Task ManagedIdentityCacheTestAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -771,7 +769,7 @@ public async Task ManagedIdentityCacheTestAsync() public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool refreshOnHasValue, bool useIsoFormat) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -807,7 +805,7 @@ public async Task ManagedIdentityExpiresOnTestAsync(int expiresInHours, bool ref public async Task ManagedIdentityInvalidRefreshOnThrowsAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -836,7 +834,7 @@ public async Task ManagedIdentityInvalidRefreshOnThrowsAsync() public async Task ManagedIdentityIsProActivelyRefreshedAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -905,7 +903,7 @@ public async Task ProactiveRefresh_CancelsSuccessfully_Async() bool wasErrorLogged = false; using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -962,7 +960,7 @@ public async Task ParallelRequests_CallTokenEndpointOnceAsync() int cacheHits = 0; using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -1039,7 +1037,7 @@ await AssertException.TaskThrowsAsync( public async Task InvalidJsonResponseHandling(ManagedIdentitySource managedIdentitySource, string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); @@ -1082,7 +1080,7 @@ public async Task ManagedIdentityRequestTokensForDifferentScopesTestAsync( string endpoint) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(source, endpoint); @@ -1164,7 +1162,7 @@ await mi.AcquireTokenForManagedIdentity("https://management.azure.com") public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.AppService, AppServiceEndpoint); @@ -1258,7 +1256,7 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( HttpStatusCode statusCode) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs index 004421378e..91905f4897 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ServiceFabricTests.cs @@ -34,7 +34,7 @@ public void TestInitialize() public async Task ServiceFabricInvalidEndpointAsync() { using(new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.ServiceFabric, "localhost/token"); @@ -67,7 +67,7 @@ public void ValidateServerCertificateCallback_ServerCertificateValidationCallbac string thumbprint, SslPolicyErrors sslPolicyErrors, bool expectedValidationResult) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.ServiceFabric, "http://localhost:40342/metadata/identity/oauth2/token", thumbprint: thumbprint); var certificate = new X509Certificate2(ResourceHelper.GetTestResourceRelativePath("testCert.crtfile"), TestConstants.TestCertPassword); @@ -95,7 +95,7 @@ public void ValidateServerCertificateCallback_ServerCertificateValidationCallbac public async Task SFThrowsWhenGetHttpClientWithValidationIsNotImplementedAsync() { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.ServiceFabric, "http://localhost:40342/metadata/identity/oauth2/token"); var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index c0fd27651c..943c57dcc5 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -22,7 +22,7 @@ public class RetryPolicyTests : TestBase [TestMethod] public async Task RetryPolicyAsync() { - using (var httpManager = new MockHttpManager(retry: false)) + using (var httpManager = new MockHttpManager()) { var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) diff --git a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs index 7e2958f525..2fe90904f8 100644 --- a/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/TelemetryTests/OTelInstrumentationTests.cs @@ -153,7 +153,7 @@ public async Task ProactiveTokenRefresh_ValidResponse_MSI_Async() string resource = "https://management.azure.com/"; using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityTestUtil.SetEnvironmentVariables(ManagedIdentitySource.AppService, appServiceEndpoint); From 657672250c50183d64e1354c0baad655318af70c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 14:02:27 -0400 Subject: [PATCH 05/68] Fixed broken unit tests, update public API --- .../AppConfig/ApplicationConfiguration.cs | 2 - .../BaseAbstractApplicationBuilder.cs | 6 +-- .../PublicApi/net462/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net472/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 2 +- .../netstandard2.0/PublicAPI.Shipped.txt | 2 +- .../Core/Mocks/MockHttpAndServiceBundle.cs | 2 +- .../Core/Mocks/MockHttpManager.cs | 11 +---- .../CoreTests/HttpTests/HttpManagerTests.cs | 43 +++++++++------- .../ManagedIdentityTests.cs | 49 ++++++------------- .../PublicApiTests/RetryPolicyTests.cs | 6 +-- 11 files changed, 51 insertions(+), 76 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 0ca48cb7e5..9bcae31599 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -114,8 +114,6 @@ public string ClientVersion public bool CacheSynchronizationEnabled { get; internal set; } = true; public bool MultiCloudSupportEnabled { get; set; } = false; - public bool RetryOnServerErrors { get; set; } = true; - public ManagedIdentityId ManagedIdentityId { get; internal set; } public bool DisableInternalRetries { get; internal set; } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index ad42a6cf88..0b397177fe 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -60,7 +60,7 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// or setting the Agent. /// /// HTTP client factory - /// Configures MSAL to retry on 5xx server errors. When enabled (on by default), MSAL will wait 1 second after receiving + /// Configures MSAL to ignore the provided retry policy. /// a 5xx error and then retry the http request again. /// MSAL does not guarantee that it will not modify the HttpClient, for example by adding new headers. /// Prior to the changes needed in order to make MSAL's httpClients thread safe (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/2046/files), @@ -70,10 +70,10 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// If you only want to configure the retryOnceOn5xx parameter, set httpClientFactory to null and MSAL will use the default http client. /// /// The builder to chain the .With methods - public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) + public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) { Config.HttpClientFactory = httpClientFactory; - Config.RetryOnServerErrors = retryOnceOn5xx; + Config.DisableInternalRetries = disableInternalRetries; return (T)this; } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt index ff692e2c0b..42feff1494 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt index ff692e2c0b..42feff1494 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt index a09bc452f1..b4422dca2d 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index cc4d5e1dd0..69604253b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs index 4406491080..e3cb1fac57 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpAndServiceBundle.cs @@ -26,7 +26,7 @@ public MockHttpAndServiceBundle( bool isInstanceDiscoveryEnabled = true, IPlatformProxy platformProxy = null) { - HttpManager = new MockHttpManager(testName); + HttpManager = new MockHttpManager(testName: testName); ServiceBundle = TestCommon.CreateServiceBundleWithCustomHttpManager( HttpManager, logCallback: logCallback, diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 3bd6046af5..232a152869 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -28,18 +28,11 @@ internal sealed class MockHttpManager : IHttpManager, private readonly IHttpManager _httpManager; - public MockHttpManager(string testName = null, - Func messageHandlerFunc = null, - Func validateServerCertificateCallback = null, - bool invokeNonMtlsHttpManagerFactory = false) : - this(true, testName, messageHandlerFunc, invokeNonMtlsHttpManagerFactory) - { - } - public MockHttpManager( - bool disableInternalRetries, + bool disableInternalRetries = false, string testName = null, Func messageHandlerFunc = null, + Func validateServerCertificateCallback = null, bool invokeNonMtlsHttpManagerFactory = false) { _httpManager = invokeNonMtlsHttpManagerFactory diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index c5c3403cf2..9a57772ca5 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -23,7 +24,7 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { - LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( + LinearRetryPolicy _stsLinearRetryPolicy = new LinearRetryPolicy( LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, HttpRetryConditions.Sts); @@ -66,7 +67,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -106,7 +107,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); } } @@ -143,7 +144,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -171,7 +172,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -213,7 +214,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -245,7 +246,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -281,7 +282,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); } } @@ -289,7 +290,7 @@ await Assert.ThrowsExceptionAsync(() => [TestMethod] public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() { - using (var httpManager = new MockHttpManager()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); @@ -304,7 +305,7 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -330,7 +331,7 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -361,7 +362,7 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); @@ -387,7 +388,7 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: _stsLinearRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); @@ -413,7 +414,7 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); @@ -439,7 +440,7 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); @@ -466,7 +467,7 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy)) + retryPolicy: _stsLinearRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); @@ -480,7 +481,7 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() [DataRow(false, true)] public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInternalRetries, bool isManagedIdentity) { - using (var httpManager = new MockHttpManager(disableInternalRetries)) + using (var httpManager = new MockHttpManager(disableInternalRetries: disableInternalRetries)) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); @@ -495,9 +496,13 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); } - } + LinearRetryPolicy retryPolicy = isManagedIdentity ? new LinearRetryPolicy( + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, + HttpRetryConditions.ManagedIdentity) : _stsLinearRetryPolicy; + var msalHttpResponse = await httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: null, @@ -508,7 +513,7 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _linearRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); Assert.IsNotNull(msalHttpResponse); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 8472e28c28..547c6f33f4 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -1269,8 +1269,8 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( var mi = miBuilder.Build(); // Simulate permanent errors (to trigger the maximum number of retries) - const int NUM_ERRORS = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_ERRORS; i++) + const int NumErrors = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -1279,24 +1279,17 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( managedIdentitySource, statusCode: statusCode); } - - MsalServiceException msalException = null; - try - { + + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + Assert.IsNotNull(ex); // 4 total: request + 3 retries Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); - for (int i = 0; i < NUM_ERRORS; i++) + for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -1306,24 +1299,17 @@ await mi.AcquireTokenForManagedIdentity(Resource) statusCode: HttpStatusCode.InternalServerError); } - msalException = null; - try - { + ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + Assert.IsNotNull(ex); // 4 total: request + 3 retries // (numRetries would be x2 if retry policy was NOT per request) Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); - for (int i = 0; i < NUM_ERRORS; i++) + for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -1333,17 +1319,10 @@ await mi.AcquireTokenForManagedIdentity(Resource) statusCode: HttpStatusCode.InternalServerError); } - msalException = null; - try - { + ex = await Assert.ThrowsExceptionAsync(async () => await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + Assert.IsNotNull(ex); // 4 total: request + 3 retries // (numRetries would be x3 if retry policy was NOT per request) diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index 943c57dcc5..3636ca3009 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -17,8 +17,8 @@ namespace Microsoft.Identity.Test.Unit.PublicApiTests [TestClass] public class RetryPolicyTests : TestBase { -// This test is expensive, as it has to wait 1 second - run it only on latest .NET -#if NET8_0_OR_GREATER + // This test is expensive, as it has to wait 1 second - run it only on latest .NET +#if NET8_0_OR_GREATER [TestMethod] public async Task RetryPolicyAsync() { @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: false) + disableInternalRetries: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From 6d89f1c2e7acd6a9326b553d58635f909c3068f1 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 14:32:22 -0400 Subject: [PATCH 06/68] Undid public API changes --- .../AppConfig/BaseAbstractApplicationBuilder.cs | 8 ++++---- .../PublicApi/net462/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net472/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 2 +- .../PublicApi/netstandard2.0/PublicAPI.Shipped.txt | 2 +- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index 0b397177fe..52fc18a1fe 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -60,8 +60,8 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// or setting the Agent. /// /// HTTP client factory - /// Configures MSAL to ignore the provided retry policy. - /// a 5xx error and then retry the http request again. + /// Configures MSAL to ignore the internal retry policy. + /// The developer will be responsible for configuring their own retry policy in their custom IMsalHttpClientFactory. /// MSAL does not guarantee that it will not modify the HttpClient, for example by adding new headers. /// Prior to the changes needed in order to make MSAL's httpClients thread safe (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/2046/files), /// the httpClient had the possibility of throwing an exception stating "Properties can only be modified before sending the first request". @@ -70,10 +70,10 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// If you only want to configure the retryOnceOn5xx parameter, set httpClientFactory to null and MSAL will use the default http client. /// /// The builder to chain the .With methods - public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) + public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) { Config.HttpClientFactory = httpClientFactory; - Config.DisableInternalRetries = disableInternalRetries; + Config.DisableInternalRetries = retryOnceOn5xx; return (T)this; } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt index 42feff1494..ff692e2c0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt index 42feff1494..ff692e2c0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt index b4422dca2d..a09bc452f1 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index 69604253b5..cc4d5e1dd0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index 3636ca3009..a1ed7f08dc 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - disableInternalRetries: false) + retryOnceOn5xx: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From b68df2c887debb4d3db6c83b8767a95cee0a8ffa Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 15:19:06 -0400 Subject: [PATCH 07/68] Fixed broken unit test and clarified public api --- .../AppConfig/ApplicationConfiguration.cs | 2 +- .../AppConfig/BaseAbstractApplicationBuilder.cs | 7 ++++--- .../PublicApiTests/RetryPolicyTests.cs | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 9bcae31599..2c9fe7e587 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -115,7 +115,7 @@ public string ClientVersion public bool MultiCloudSupportEnabled { get; set; } = false; public ManagedIdentityId ManagedIdentityId { get; internal set; } - public bool DisableInternalRetries { get; internal set; } + public bool DisableInternalRetries { get; internal set; } = false; public bool IsManagedIdentity { get; } public bool IsConfidentialClient { get; } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index 52fc18a1fe..2fc2fa6f95 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -60,8 +60,9 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// or setting the Agent. /// /// HTTP client factory - /// Configures MSAL to ignore the internal retry policy. - /// The developer will be responsible for configuring their own retry policy in their custom IMsalHttpClientFactory. + /// Configures MSAL to retry on 5xx server errors. When enabled (on by default), MSAL will wait 1 second after receiving + /// a 5xx error and then retry the http request again. + /// When disabled, the developer will be responsible for configuring their own retry policy in their custom IMsalHttpClientFactory. /// MSAL does not guarantee that it will not modify the HttpClient, for example by adding new headers. /// Prior to the changes needed in order to make MSAL's httpClients thread safe (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/pull/2046/files), /// the httpClient had the possibility of throwing an exception stating "Properties can only be modified before sending the first request". @@ -73,7 +74,7 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) { Config.HttpClientFactory = httpClientFactory; - Config.DisableInternalRetries = retryOnceOn5xx; + Config.DisableInternalRetries = !retryOnceOn5xx; return (T)this; } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index a1ed7f08dc..0cc2b98875 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -22,14 +22,14 @@ public class RetryPolicyTests : TestBase [TestMethod] public async Task RetryPolicyAsync() { - using (var httpManager = new MockHttpManager()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { var app = ConfidentialClientApplicationBuilder.Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: false) + retryOnceOn5xx: true) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From aab894014faa7db74a70d8f301ed80986b409f41 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 15:26:43 -0400 Subject: [PATCH 08/68] removed comment, adjusted unit test --- .../ManagedIdentity/ImdsManagedIdentitySource.cs | 3 --- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 9c8ca08e8b..6cfb8854e6 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,9 +82,6 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } - // uncomment in follow-up IMDS retry policy PR - // request.RetryPolicy = new ImdsRetryPolicy(); - return request; } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index 0cc2b98875..c21b99152f 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: true) + retryOnceOn5xx: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From 4aedf333d3fd3cc914140651ddb634e821774eb5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 14:12:06 -0400 Subject: [PATCH 09/68] Changed constants to PascalCase --- .../Microsoft.Identity.Client/Http/LinearRetryPolicy.cs | 4 ++-- .../Instance/Region/RegionManager.cs | 4 ++-- .../Instance/Validation/AdfsAuthorityValidator.cs | 4 ++-- .../ManagedIdentity/AzureArcManagedIdentitySource.cs | 4 ++-- .../ManagedIdentity/ManagedIdentityRequest.cs | 8 ++++---- .../Microsoft.Identity.Client/OAuth2/OAuth2Client.cs | 4 ++-- .../WsTrust/WsTrustWebRequestManager.cs | 4 ++-- .../CoreTests/HttpTests/HttpManagerTests.cs | 8 ++++---- .../ManagedIdentityTests/ManagedIdentityTests.cs | 8 ++++---- tests/devapps/MacMauiAppWithBroker/_EmptyClass.cs | 4 ++++ 10 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 tests/devapps/MacMauiAppWithBroker/_EmptyClass.cs diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs index f75f9ce617..fc9ce94dec 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs @@ -11,9 +11,9 @@ internal class LinearRetryPolicy : IRetryPolicy { // referenced in unit tests, cannot be private public static int numRetries { get; private set; } = 0; - public const int DEFAULT_ESTS_MAX_RETRIES = 1; + public const int DefaultStsMaxRetries = 1; // this will be overridden in the unit tests so that they run faster - public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; + public static int DefaultStsRetryDelayMs { get; set; } = 1000; private int _maxRetries; private readonly Func _retryCondition; diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index f74708dc49..b2b72a4a45 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -47,8 +47,8 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static string s_regionDiscoveryDetails; private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public RegionManager( diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 3462c75b90..83eff0a534 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -30,8 +30,8 @@ public async Task ValidateAuthorityAsync( string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index 9f4338024a..a643ce7880 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -124,8 +124,8 @@ protected override async Task HandleResponseAsync( request.Headers.Add("Authorization", authHeaderValue); LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 5091d5499c..3541730c7b 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -12,9 +12,9 @@ namespace Microsoft.Identity.Client.ManagedIdentity internal class ManagedIdentityRequest { // referenced in unit tests, cannot be private - public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + public const int DefaultManagedIdentityMaxRetries = 3; // this will be overridden in the unit tests so that they run faster - public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; + public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; private readonly Uri _baseEndpoint; @@ -37,8 +37,8 @@ public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retr QueryParameters = new Dictionary(); IRetryPolicy defaultRetryPolicy = new LinearRetryPolicy( - DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS, - DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, + DefaultManagedIdentityRetryDelayMs, + DefaultManagedIdentityMaxRetries, HttpRetryConditions.ManagedIdentity); RetryPolicy = retryPolicy ?? defaultRetryPolicy; } diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index d48ed8eeb5..462ee6c4a3 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -42,8 +42,8 @@ internal class OAuth2Client private readonly IHttpManager _httpManager; private readonly X509Certificate2 _mtlsCertificate; private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager, X509Certificate2 mtlsCertificate) diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index e6a8edc10d..fb69357d42 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -22,8 +22,8 @@ internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public WsTrustWebRequestManager(IHttpManager httpManager) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index 9a57772ca5..b9c3428145 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -25,8 +25,8 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests public class HttpManagerTests { LinearRetryPolicy _stsLinearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DEFAULT_ESTS_RETRY_DELAY_MS, - LinearRetryPolicy.DEFAULT_ESTS_MAX_RETRIES, + LinearRetryPolicy.DefaultStsRetryDelayMs, + LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); [TestInitialize] @@ -499,8 +499,8 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna } LinearRetryPolicy retryPolicy = isManagedIdentity ? new LinearRetryPolicy( - ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, - ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES, + ManagedIdentityRequest.DefaultManagedIdentityRetryDelayMs, + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries, HttpRetryConditions.ManagedIdentity) : _stsLinearRetryPolicy; var msalHttpResponse = await httpManager.SendRequestAsync( diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 547c6f33f4..84d399a51d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -1269,7 +1269,7 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( var mi = miBuilder.Build(); // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + const int NumErrors = ManagedIdentityRequest.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( @@ -1286,7 +1286,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 4 total: request + 3 retries - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1306,7 +1306,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1326,7 +1326,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } diff --git a/tests/devapps/MacMauiAppWithBroker/_EmptyClass.cs b/tests/devapps/MacMauiAppWithBroker/_EmptyClass.cs new file mode 100644 index 0000000000..1a340b9864 --- /dev/null +++ b/tests/devapps/MacMauiAppWithBroker/_EmptyClass.cs @@ -0,0 +1,4 @@ +namespace MacMauiAppWithBroker +{ + internal class EmptyClass {} +} From 4266a145a03c84bf303190053f3c81cae019510b Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 14:19:30 -0400 Subject: [PATCH 10/68] Deleted file --- .../MacMauiAppWithBroker/MacMauiAppWithBroker.csproj | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj index 0518b810b7..f838fcc973 100644 --- a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj +++ b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj @@ -112,11 +112,7 @@ namespace MacMauiAppWithBroker } - + @@ -128,5 +124,8 @@ namespace MacMauiAppWithBroker + + + From 2153fc390bf54445a27888550020540daa2ee385 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 14:23:02 -0400 Subject: [PATCH 11/68] Undid file change from main --- .../MacMauiAppWithBroker/MacMauiAppWithBroker.csproj | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj index f838fcc973..0518b810b7 100644 --- a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj +++ b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj @@ -112,7 +112,11 @@ namespace MacMauiAppWithBroker } - + @@ -124,8 +128,5 @@ namespace MacMauiAppWithBroker - - - From b82f8722917262d26939396ded1174c82dd89b38 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 11 Apr 2025 16:18:11 -0400 Subject: [PATCH 12/68] Reworked retry policy functionality. Created IMDS retry policy. --- .../Http/DefaultRetryPolicy.cs | 49 +++++++++++++++ .../Http/ExponentialRetryStrategy.cs | 55 ++++++++++++++++ .../Http/HttpManager.cs | 7 ++- .../Http/HttpManagerFactory.cs | 5 ++ .../Http/HttpRetryCondition.cs | 24 ++++++- .../Http/IRetryPolicy.cs | 7 +-- .../Http/ImdsRetryPolicy.cs | 63 +++++++++++++++++++ .../Http/LinearRetryPolicy.cs | 37 ----------- .../Http/LinearRetryStrategy.cs | 41 ++++++++++++ .../Http/NoRetryPolicy.cs | 10 +-- 10 files changed, 244 insertions(+), 54 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs delete mode 100644 src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs new file mode 100644 index 0000000000..12abebdc08 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; + +namespace Microsoft.Identity.Client.Http +{ + class DefaultRetryPolicy : IRetryPolicy + { + // this will be overridden in the unit tests so that they run faster + public static int RETRY_DELAY_MS { get; set; } + + private int _maxRetries; + + private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); + + private readonly Func _retryCondition; + + public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) + { + RETRY_DELAY_MS = retryDelayMs; + _maxRetries = maxRetries; + _retryCondition = retryCondition; + } + + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) + { + // Check if the status code is retriable and if the current retry count is less than max retries + if (_retryCondition(response, exception) && + retryCount < _maxRetries) + { + // Use HeadersAsDictionary to check for "Retry-After" header + response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); + + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, RETRY_DELAY_MS); + + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + + // Pause execution for the calculated delay + await Task.Delay(retryAfterDelay).ConfigureAwait(false); + } + + // If the status code is not retriable or max retries have been reached, do not retry + return false; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs new file mode 100644 index 0000000000..3299f245fb --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.Client.Http +{ + internal class ExponentialRetryStrategy + { + // Minimum backoff time in milliseconds + private int _minExponentialBackoff; + // Maximum backoff time in milliseconds + private int _maxExponentialBackoff; + // Maximum backoff time in milliseconds + private int _exponentialDeltaBackoff; + + public ExponentialRetryStrategy(int minExponentialBackoff, int maxExponentialBackoff, int exponentialDeltaBackoff) + { + _minExponentialBackoff = minExponentialBackoff; + _maxExponentialBackoff = maxExponentialBackoff; + _exponentialDeltaBackoff = exponentialDeltaBackoff; + } + + /// + /// Calculates the exponential delay based on the current retry attempt. + /// + /// The current retry attempt number. + /// The calculated exponential delay in milliseconds. + /// + /// The delay is calculated using the formula: + /// - If is 0, it returns the minimum backoff time. + /// - Otherwise, it calculates the delay as the minimum of: + /// - (2^(currentRetry - 1)) * deltaBackoff + /// - maxBackoff + /// This ensures that the delay increases exponentially with each retry attempt, + /// but does not exceed the maximum backoff time. + /// + public int calculateDelay(int currentRetry) + { + // Attempt 1 + if (currentRetry == 0) + { + return _minExponentialBackoff; + } + + // Attempt 2+ + int exponentialDelay = Math.Min( + (int)(Math.Pow(2, currentRetry - 1) * _exponentialDeltaBackoff), + _maxExponentialBackoff + ); + + return exponentialDelay; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 4e54e426f5..c8f45813ab 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -110,10 +110,11 @@ public async Task SendRequestAsync( logger.Error("The HTTP request failed. " + exception.Message); timeoutException = exception; } - - while (!_disableInternalRetries && retryPolicy.PauseForRetry(response, timeoutException, retryCount)) + + while (!_disableInternalRetries && await retryPolicy.PauseForRetryAsync(response, timeoutException, retryCount, logger).ConfigureAwait(false)) { - logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms."); + retryCount++; + return await SendRequestAsync( endpoint, headers, diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 120735fbeb..9a473a0b54 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,6 +8,11 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { + private const int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = 1000; + private const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + private const int DEFAULT_ESTS_RETRY_DELAY_MS = 1000; + private const int DEFAULT_ESTS_MAX_RETRIES = 1; + public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries = false) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs index cf83c25516..d6e8c1fa8d 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs @@ -12,7 +12,7 @@ internal static class HttpRetryConditions /// Retry policy specific to managed identity flow. /// Avoid changing this, as it's breaking change. /// - public static bool ManagedIdentity(HttpResponse response, Exception exception) + public static bool DefaultManagedIdentity(HttpResponse response, Exception exception) { if (exception != null) { @@ -21,12 +21,32 @@ public static bool ManagedIdentity(HttpResponse response, Exception exception) return (int)response.StatusCode switch { - //Not Found + // Not Found, Request Timeout, Too Many Requests, Server Error, Service Unavailable, Gateway Timeout 404 or 408 or 429 or 500 or 503 or 504 => true, _ => false, }; } + /// + /// Retry policy specific to IMDS Managed Identity. + /// + public static bool Imds(HttpResponse response, Exception exception) + { + if (exception != null) + { + return exception is TaskCanceledException ? true : false; + } + + return (int)response.StatusCode switch + { + // Not Found, Request Timeout, Gone, Too Many Requests + 404 or 408 or 410 or 429 => true, + // Server Error range + >= 500 and <= 599 => true, + _ => false, + }; + } + /// /// Retry condition for /token and /authorize endpoints /// diff --git a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs index db3b466759..cb3b74c45c 100644 --- a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs @@ -2,16 +2,13 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; namespace Microsoft.Identity.Client.Http { internal interface IRetryPolicy { - int DelayInMilliseconds { get; } - bool PauseForRetry(HttpResponse response, Exception exception, int retryCount); + Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger); } } diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs new file mode 100644 index 0000000000..d5bbcb75f9 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; + +namespace Microsoft.Identity.Client.Http +{ + internal class ImdsRetryPolicy : IRetryPolicy + { + private const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; + private const int LINEAR_STRATEGY_NUM_RETRIES = 7; + private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + + // these will be overridden in the unit tests so that they run faster + public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; + public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; + public static int EXPONENTIAL_DELTA_BACKOFF_MS { get; set; } = 2000; + public static int HTTP_STATUS_GONE_RETRY_AFTER_MS { get; set; } = HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL; + + private int _maxRetries; + + private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS + ); + + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) + { + int httpStatusCode = (int)response.StatusCode; + + if (retryCount == 0) + { + // Calculate the maxRetries based on the status code, once per request + _maxRetries = httpStatusCode == (int)HttpStatusCode.Gone + ? LINEAR_STRATEGY_NUM_RETRIES + : EXPONENTIAL_STRATEGY_NUM_RETRIES; + } + + // Check if the status code is retriable and if the current retry count is less than max retries + if (HttpRetryConditions.Imds(response, exception) && + retryCount < _maxRetries) + { + int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone + ? HTTP_STATUS_GONE_RETRY_AFTER_MS + : _exponentialRetryStrategy.calculateDelay(retryCount); + + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + + // Pause execution for the calculated delay + await Task.Delay(retryAfterDelay).ConfigureAwait(false); + + return true; + } + + // If the status code is not retriable or max retries have been reached, do not retry + return false; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs deleted file mode 100644 index fc9ce94dec..0000000000 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Identity.Client.Http -{ - internal class LinearRetryPolicy : IRetryPolicy - { - // referenced in unit tests, cannot be private - public static int numRetries { get; private set; } = 0; - public const int DefaultStsMaxRetries = 1; - // this will be overridden in the unit tests so that they run faster - public static int DefaultStsRetryDelayMs { get; set; } = 1000; - - private int _maxRetries; - private readonly Func _retryCondition; - public int DelayInMilliseconds { private set; get; } - - public LinearRetryPolicy(int delayMilliseconds, int maxRetries, Func retryCondition) - { - DelayInMilliseconds = delayMilliseconds; - _maxRetries = maxRetries; - _retryCondition = retryCondition; - } - - public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount) - { - // referenced in the unit tests - numRetries = retryCount + 1; - - return retryCount < _maxRetries && _retryCondition(response, exception); - } - } -} diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs new file mode 100644 index 0000000000..cecb38840f --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.Client.Http +{ + internal class LinearRetryStrategy + { + /// + /// Calculates the number of milliseconds to sleep based on the `Retry-After` HTTP header. + /// + /// The value of the `Retry-After` HTTP header. This can be either a number of seconds or an HTTP date string. + /// The minimum delay in milliseconds to return if the header is not present or invalid. + /// The number of milliseconds to sleep before retrying the request. + public int calculateDelay(string retryHeader, int minimumDelay) + { + if (string.IsNullOrEmpty(retryHeader)) + { + return minimumDelay; + } + + // Try parsing the retry-after header as seconds + if (double.TryParse(retryHeader, out double seconds)) + { + int millisToSleep = (int)Math.Round(seconds * 1000); + return Math.Max(minimumDelay, millisToSleep); + } + + // If parsing as seconds fails, try parsing as an HTTP date + if (DateTime.TryParse(retryHeader, out DateTime retryDate)) + { + int millisToSleep = (int)(retryDate - DateTime.UtcNow).TotalMilliseconds; + return Math.Max(minimumDelay, millisToSleep); + } + + // If all parsing fails, return the minimum delay + return minimumDelay; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs index 3562d4c3ca..800913245e 100644 --- a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs @@ -2,20 +2,16 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; namespace Microsoft.Identity.Client.Http { internal class NoRetryPolicy : IRetryPolicy { - public int DelayInMilliseconds { get => throw new NotImplementedException(); } - - public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount) + public Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { - return false; + throw new NotImplementedException(); } } } From 8b37d0847eca7d2ef1ca02c6efb526758f1f5861 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 14 Apr 2025 18:55:26 -0400 Subject: [PATCH 13/68] Implemented GitHub Feedback --- .../Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs | 2 +- src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs | 2 +- src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs index 3299f245fb..5893c03ad0 100644 --- a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs @@ -35,7 +35,7 @@ public ExponentialRetryStrategy(int minExponentialBackoff, int maxExponentialBac /// This ensures that the delay increases exponentially with each retry attempt, /// but does not exceed the maximum backoff time. /// - public int calculateDelay(int currentRetry) + public int CalculateDelay(int currentRetry) { // Attempt 1 if (currentRetry == 0) diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index d5bbcb75f9..4694e1b929 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -46,7 +46,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce { int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone ? HTTP_STATUS_GONE_RETRY_AFTER_MS - : _exponentialRetryStrategy.calculateDelay(retryCount); + : _exponentialRetryStrategy.CalculateDelay(retryCount); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); diff --git a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs index 800913245e..d3b09be50c 100644 --- a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs @@ -11,7 +11,7 @@ internal class NoRetryPolicy : IRetryPolicy { public Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { - throw new NotImplementedException(); + return Task.FromResult(false); } } } From abe860f16ab3680fedbfa8bcdf7b9b92b3ee7c71 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 15 Apr 2025 20:24:17 -0400 Subject: [PATCH 14/68] Created first retry unit test. The rest of the tests will be based off of this one. --- .../TestConstants.cs | 3 +- .../DefaultRetryPolicy.cs | 15 +++ .../ManagedIdentityTests/ImdsTests.cs | 99 ++++++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index e40f68a97a..32bfbd3ea1 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -202,7 +202,8 @@ public static HashSet s_scope public const string Region = "centralus"; public const string InvalidRegion = "invalidregion"; public const int TimeoutInMs = 2000; - public const string ImdsUrl = "http://169.254.169.254/metadata/instance/compute/location"; + public const string ImdsHost = "169.254.169.254"; + public const string ImdsUrl = $"http://{ImdsHost}/metadata/instance/compute/location"; public const string UserAssertion = "fake_access_token"; public const string CodeVerifier = "someCodeVerifier"; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs new file mode 100644 index 0000000000..2cf5c17888 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests +{ + class DefaultRetryPolicy + { + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 6fe43611ee..43a35f4578 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; +using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -17,6 +19,101 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { + private const double ONE_HUNDRED_TIMES_FASTER = 0.01; + private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 -> 2 + + private static int _originalMinBackoff; + private static int _originalMaxBackoff; + private static int _originalDeltaBackoff; + private static int _originalGoneRetryAfter; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalMinBackoff = ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS; + _originalMaxBackoff = ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS; + _originalDeltaBackoff = ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS; + _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; + + // Speed up retry delays by 100x + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = _originalMinBackoff; + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = _originalMaxBackoff; + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = _originalDeltaBackoff; + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = _originalGoneRetryAfter; + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404S = 2; + for (int i = 0; i < NUM_404S; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // ensure that each retry followed the exponential backoff strategy + // 2 x exponential backoff (1 second -> 2 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] From 0eef7c62a33d7b2e96cdac63e6519d2e6c12c8bd Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 16 Apr 2025 17:27:47 -0400 Subject: [PATCH 15/68] Added more ImdsRetryPolicy unit tests. Fixed bug discovered by unit tests. --- global.json | 8 +- .../Http/ImdsRetryPolicy.cs | 4 +- .../ManagedIdentityTests/ImdsTests.cs | 175 +++++++++++++++++- 3 files changed, 176 insertions(+), 11 deletions(-) diff --git a/global.json b/global.json index 66e4a5c8a7..cfbcf3eaec 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "8.0.404", - "rollForward": "latestFeature" - } + "sdk": { + "version": "9.0.201", + "rollForward": "latestFeature" + } } diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 4694e1b929..8421df131c 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,8 +10,8 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - private const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; - private const int LINEAR_STRATEGY_NUM_RETRIES = 7; + public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; // referenced in unit tests + public const int LINEAR_STRATEGY_NUM_RETRIES = 7; // referenced in unit tests private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds // these will be overridden in the unit tests so that they run faster diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 43a35f4578..efae53f033 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenTelemetry.Resources; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -20,7 +21,8 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests public class ImdsTests : TestBase { private const double ONE_HUNDRED_TIMES_FASTER = 0.01; - private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 -> 2 + private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds + private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds private static int _originalMinBackoff; private static int _originalMaxBackoff; @@ -75,8 +77,8 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U var mi = miBuilder.Build(); // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404S = 2; - for (int i = 0; i < NUM_404S; i++) + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -105,8 +107,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U stopwatch.Stop(); - // ensure that each retry followed the exponential backoff strategy - // 2 x exponential backoff (1 second -> 2 seconds) + // exponential backoff (1 second -> 2 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries @@ -114,6 +115,170 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U } } + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 5); // request + 4 retries + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + var stopwatch = Stopwatch.StartNew(); + + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + + stopwatch.Stop(); + + Assert.IsNotNull(ex); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 8); // request + 7 retries + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + /// Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + var stopwatch = Stopwatch.StartNew(); + + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + + stopwatch.Stop(); + + Assert.IsNotNull(ex); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 4); // request + 2 retries + } + } + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] From 9faf28748bbf3b4eb88e649deb1bff74d2ae0dc8 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 17 Apr 2025 14:45:55 -0400 Subject: [PATCH 16/68] Finished all ImdsRetryPolicy unit tests --- global.json | 8 +- .../ManagedIdentityTests/ImdsTests.cs | 258 +++++++++++++++--- 2 files changed, 225 insertions(+), 41 deletions(-) diff --git a/global.json b/global.json index cfbcf3eaec..66e4a5c8a7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "9.0.201", - "rollForward": "latestFeature" - } + "sdk": { + "version": "8.0.404", + "rollForward": "latestFeature" + } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index efae53f033..617e585035 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks; @@ -8,11 +9,9 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; -using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenTelemetry.Resources; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -110,7 +109,9 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U // exponential backoff (1 second -> 2 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -170,7 +171,9 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI // linear backoff (10 seconds * 4 retries) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 5); // request + 4 retries + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -210,20 +213,24 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign userAssignedIdentityId: userAssignedIdentityId); } + MsalServiceException msalException = null; var stopwatch = Stopwatch.StartNew(); - - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); - + .ExecuteAsync().ConfigureAwait(false); + } catch (Exception ex) + { + msalException = ex as MsalServiceException; + } stopwatch.Stop(); - - Assert.IsNotNull(ex); + Assert.IsNotNull(msalException); // linear backoff (10 seconds * 7 retries) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 8); // request + 7 retries + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -248,7 +255,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign var mi = miBuilder.Build(); - /// Simulate permanent 504s (to trigger the maximum number of retries) + // Simulate permanent 504s (to trigger the maximum number of retries) const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) for (int i = 0; i < NUM_504; i++) { @@ -262,58 +269,235 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign userAssignedIdentityId: userAssignedIdentityId); } + MsalServiceException msalException = null; var stopwatch = Stopwatch.StartNew(); - - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); - + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } stopwatch.Stop(); - - Assert.IsNotNull(ex); + Assert.IsNotNull(msalException); // exponential backoff (1 second -> 2 seconds -> 4 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 4); // request + 2 retries + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] - [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] - [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] - [DataRow(HttpStatusCode.GatewayTimeout, ImdsManagedIdentitySource.GatewayError, 4, DisplayName = "GatewayTimeout - Gateway Error Retries")] - public async Task ImdsErrorHandlingTestAsync(HttpStatusCode statusCode, string expectedErrorSubstring, int expectedAttempts) + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, "http://169.254.169.254"); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); - // Disabling shared cache options to avoid cross test pollution. + // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; var mi = miBuilder.Build(); - // Adding multiple mock handlers to simulate retries for GatewayTimeout - for (int i = 0; i < expectedAttempts; i++) + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) { - httpManager.AddManagedIdentityMockHandler(ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, statusCode: statusCode); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } - // Expecting a MsalServiceException indicating an error - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.BadRequest, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); - Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.Imds.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); - Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); - Assert.IsTrue(ex.Message.Contains(expectedErrorSubstring), $"The error message is not as expected. Error message: {ex.Message}. Expected message should contain: {expectedErrorSubstring}"); + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); } } } From 128f628ccc202b79aefe37ffa23fe597bf30a7cf Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 16:00:32 -0400 Subject: [PATCH 17/68] Discovered bugs through unit tests, and fixed them. Improved ImdsRetryPolicy unit tests. Wrote UAMI unit tests for DefaultRetryPolicy. --- .../Http/DefaultRetryPolicy.cs | 24 +- .../Http/HttpManagerFactory.cs | 11 +- .../Http/ImdsRetryPolicy.cs | 6 +- .../TestConstants.cs | 9 + .../DefaultRetryPolicy.cs | 15 - .../DefaultRetryPolicyTests.cs | 177 +++++ .../ManagedIdentityTests/ImdsTests.cs | 672 +++++++++--------- .../ManagedIdentityTests.cs | 2 +- 8 files changed, 553 insertions(+), 363 deletions(-) delete mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 12abebdc08..79c5c06339 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -9,37 +9,37 @@ namespace Microsoft.Identity.Client.Http { class DefaultRetryPolicy : IRetryPolicy { - // this will be overridden in the unit tests so that they run faster - public static int RETRY_DELAY_MS { get; set; } - - private int _maxRetries; - private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); - private readonly Func _retryCondition; + // constants that are defined in the constructor + public static int DEFAULT_RETRY_DELAY_MS { get; set; } // this will be overridden in the unit tests so that they run faster + private int MAX_RETRIES; + private readonly Func RETRY_CONDITION; public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { - RETRY_DELAY_MS = retryDelayMs; - _maxRetries = maxRetries; - _retryCondition = retryCondition; + DEFAULT_RETRY_DELAY_MS = retryDelayMs; + MAX_RETRIES = maxRetries; + RETRY_CONDITION = retryCondition; } public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { // Check if the status code is retriable and if the current retry count is less than max retries - if (_retryCondition(response, exception) && - retryCount < _maxRetries) + if (RETRY_CONDITION(response, exception) && + retryCount < MAX_RETRIES) { // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); - int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, RETRY_DELAY_MS); + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); + + return true; } // If the status code is not retriable or max retries have been reached, do not retry diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 9a473a0b54..a1c1527913 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,10 +8,13 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { - private const int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = 1000; - private const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; - private const int DEFAULT_ESTS_RETRY_DELAY_MS = 1000; - private const int DEFAULT_ESTS_MAX_RETRIES = 1; + // referenced in unit tests, cannot be private + public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + public const int DEFAULT_ESTS_MAX_RETRIES = 1; + + // these will be overridden in the unit tests so that they run faster + public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; + public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 8421df131c..7222284e91 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,10 +10,12 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; // referenced in unit tests - public const int LINEAR_STRATEGY_NUM_RETRIES = 7; // referenced in unit tests private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + // referenced in unit tests, cannot be private + public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; + public const int LINEAR_STRATEGY_NUM_RETRIES = 7; + // these will be overridden in the unit tests so that they run faster public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index 32bfbd3ea1..b142968907 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -205,6 +205,15 @@ public static HashSet s_scope public const string ImdsHost = "169.254.169.254"; public const string ImdsUrl = $"http://{ImdsHost}/metadata/instance/compute/location"; + public const double ONE_HUNDRED_TIMES_FASTER = 0.01; + + public const string AppServiceEndpoint = "http://127.0.0.1:41564/msi/token"; + public const string AzureArcEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; + public const string CloudShellEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; + public const string ImdsEndpoint = $"http://{ImdsHost}/metadata/identity/oauth2/token"; + public const string MachineLearningEndpoint = "http://localhost:7071/msi/token"; + public const string ServiceFabricEndpoint = "https://localhost:2377/metadata/identity/oauth2/token"; + public const string UserAssertion = "fake_access_token"; public const string CodeVerifier = "someCodeVerifier"; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs deleted file mode 100644 index 2cf5c17888..0000000000 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests -{ - class DefaultRetryPolicy - { - } -} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs new file mode 100644 index 0000000000..e6c219564b --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; + +namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests +{ + /// + /// The Default Retry Policy applies to: + /// ESTS (Azure AD) + /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric + /// + [TestClass] + + public class DefaultRetryPolicyTests : TestBase + { + private static int _originalManagedIdentityRetryDelay; + private static int _originalEstsRetryDelay; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalManagedIdentityRetryDelay = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS; + _originalEstsRetryDelay = HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS; + + // Speed up retry delays by 100x + HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = (int)(_originalEstsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = _originalManagedIdentityRetryDelay; + HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = _originalEstsRetryDelay; + } + + [DataTestMethod] // see test class header: all sources that allow UAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources that allow UAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (1 second * 3 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 617e585035..a7f57917f7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -19,7 +19,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private const double ONE_HUNDRED_TIMES_FASTER = 0.01; private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds @@ -38,10 +37,10 @@ public static void ClassInitialize(TestContext _) _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; // Speed up retry delays by 100x - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] @@ -60,59 +59,61 @@ public static void ClassCleanup() public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404 = 2; - for (int i = 0; i < NUM_404; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + MockHelpers.GetMsiSuccessfulResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.NotFound, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - ManagedIdentitySource.Imds, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // exponential backoff (1 second -> 2 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + // exponential backoff (1 second -> 2 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - // ensure that exactly 3 requests were made: initial request + 2 retries - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } } } @@ -122,59 +123,61 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate four 410s (to trigger retries), then a successful response - const int NUM_410 = 4; - for (int i = 0; i < NUM_410; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + MockHelpers.GetMsiSuccessfulResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - // Final success - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - ManagedIdentitySource.Imds, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + var stopwatch = Stopwatch.StartNew(); - var stopwatch = Stopwatch.StartNew(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + stopwatch.Stop(); - stopwatch.Stop(); + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - // linear backoff (10 seconds * 4 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(httpManager.QueueSize, 0); - // ensure that exactly 5 requests were made: initial request + 4 retries - Assert.AreEqual(httpManager.QueueSize, 0); - - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } } } @@ -184,53 +187,56 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 410s (to trigger the maximum number of retries) - const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) - for (int i = 0; i < NUM_410; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(httpManager.QueueSize, 0); } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (10 seconds * 7 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 8 requests were made: initial request + 7 retries - Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -240,54 +246,56 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 504s (to trigger the maximum number of retries) - const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_504; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - msalException = ex as MsalServiceException; + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // exponential backoff (1 second -> 2 seconds -> 4 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -297,207 +305,213 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); } + } + } - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - for (int i = 0; i < NUM_500; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - msalException = null; - stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // ensure that the second request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); + var mi = miBuilder.Build(); - for (int i = 0; i < NUM_500; i++) - { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, + statusCode: HttpStatusCode.BadRequest, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - msalException = null; - stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.BadRequest, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - // ensure that only the initial request was made - Assert.AreEqual(httpManager.QueueSize, 0); - } - } + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + var mi = miBuilder.Build(); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - // ensure that only the initial request was made - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); + } } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 84d399a51d..349c48ceda 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -41,7 +41,7 @@ public class ManagedIdentityTests : TestBase [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] - [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] + [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.Imds)] [DataRow(null, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(AzureArcEndpoint, ManagedIdentitySource.AzureArc, ManagedIdentitySource.AzureArc)] [DataRow(CloudShellEndpoint, ManagedIdentitySource.CloudShell, ManagedIdentitySource.CloudShell)] From 69a157a76e4ef1159f24e19286116e596d2e99b2 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 16:07:57 -0400 Subject: [PATCH 18/68] Minor improvements to ImdsRetryPolicy tests --- .../ManagedIdentityTests/ImdsTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index a7f57917f7..682b8fe469 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -60,7 +60,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -124,7 +124,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -188,7 +188,7 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -247,7 +247,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -306,7 +306,7 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -420,7 +420,7 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -471,7 +471,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { From fe3ac7c5198f36b8c907751c9c356c14a65a488a Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 18:48:00 -0400 Subject: [PATCH 19/68] Added option retryAfter header to mockHttpManager. Added more DefaulyRetryPolicy tests. --- .../Core/Mocks/MockHttpManagerExtensions.cs | 15 ++- .../DefaultRetryPolicyTests.cs | 121 +++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index 5a04bedf5b..bde5093fd3 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -367,12 +367,19 @@ public static void AddManagedIdentityMockHandler( ManagedIdentitySource managedIdentitySourceType, string userAssignedId = null, UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None, - HttpStatusCode statusCode = HttpStatusCode.OK + HttpStatusCode statusCode = HttpStatusCode.OK, + string retryAfterHeader = null // A number of seconds (e.g., "120"), or an HTTP-date in RFC1123 format (e.g., "Fri, 19 Apr 2025 15:00:00 GMT") ) { - HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode); - HttpContent content = new StringContent(response); - responseMessage.Content = content; + HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode) + { + Content = new StringContent(response) + }; + + if (retryAfterHeader != null) + { + responseMessage.Headers.TryAddWithoutValidation("Retry-After", retryAfterHeader); + } MockHttpMessageHandler httpMessageHandler = BuildMockHandlerForManagedIdentitySource(managedIdentitySourceType, resource); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index e6c219564b..d464c67b0e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -25,9 +25,10 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric /// [TestClass] - public class DefaultRetryPolicyTests : TestBase { + private const int LINEAR_POLICY_MAX_RETRIES_IN_MS = 3000; + private static int _originalManagedIdentityRetryDelay; private static int _originalEstsRetryDelay; @@ -144,7 +145,7 @@ public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIden httpManager.AddManagedIdentityMockHandler( endpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError, userAssignedId: userAssignedId, @@ -173,5 +174,121 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } } } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // max retry time (3 seconds), but make it one hundred times faster so the test completes quickly + double retryAfterSeconds = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER; + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterSeconds.ToString()); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (LINEAR_POLICY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } } } From e73a7bb25c884f1c95e4c1906bab809df27aaeec Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 21 Apr 2025 17:56:26 -0400 Subject: [PATCH 20/68] Finished DefaultRetryPolicy unit tests. Edited some ImdsRetryPolicy unit tests. Fixed some bugs discovered by unit tests. --- .../Http/DefaultRetryPolicy.cs | 8 +- .../Http/ImdsRetryPolicy.cs | 6 + .../Http/LinearRetryStrategy.cs | 4 +- .../Core/Mocks/MockHttpManager.cs | 1 - .../DefaultRetryPolicyTests.cs | 335 +++++++++++++++++- .../ManagedIdentityTests/ImdsTests.cs | 27 +- 6 files changed, 356 insertions(+), 25 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 79c5c06339..cb59f160bd 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -16,6 +16,9 @@ class DefaultRetryPolicy : IRetryPolicy private int MAX_RETRIES; private readonly Func RETRY_CONDITION; + // referenced in the unit tests + public static int numRetries { get; private set; } = 0; + public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { DEFAULT_RETRY_DELAY_MS = retryDelayMs; @@ -29,12 +32,15 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (RETRY_CONDITION(response, exception) && retryCount < MAX_RETRIES) { + // used below in the log statement, also referenced in the unit tests + numRetries = retryCount + 1; + // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); - logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 7222284e91..82cd122fea 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -15,6 +15,7 @@ internal class ImdsRetryPolicy : IRetryPolicy // referenced in unit tests, cannot be private public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; public const int LINEAR_STRATEGY_NUM_RETRIES = 7; + public static int numRetries { get; private set; } = 0; // these will be overridden in the unit tests so that they run faster public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; @@ -24,6 +25,8 @@ internal class ImdsRetryPolicy : IRetryPolicy private int _maxRetries; + + private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, @@ -46,6 +49,9 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (HttpRetryConditions.Imds(response, exception) && retryCount < _maxRetries) { + // used below in the log statement, also referenced in the unit tests + numRetries = retryCount + 1; + int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone ? HTTP_STATUS_GONE_RETRY_AFTER_MS : _exponentialRetryStrategy.CalculateDelay(retryCount); diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs index cecb38840f..bc2edb8bbb 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs @@ -30,7 +30,9 @@ public int calculateDelay(string retryHeader, int minimumDelay) // If parsing as seconds fails, try parsing as an HTTP date if (DateTime.TryParse(retryHeader, out DateTime retryDate)) { - int millisToSleep = (int)(retryDate - DateTime.UtcNow).TotalMilliseconds; + DateTime.TryParse(DateTime.UtcNow.ToString("R"), out DateTime nowDate); + + int millisToSleep = (int)(retryDate - nowDate).TotalMilliseconds; return Math.Max(minimumDelay, millisToSleep); } diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 232a152869..9512304848 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -16,7 +16,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.Internal; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Common.Core.Mocks diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index d464c67b0e..b9ad25105e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -2,11 +2,8 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; @@ -27,8 +24,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class DefaultRetryPolicyTests : TestBase { - private const int LINEAR_POLICY_MAX_RETRIES_IN_MS = 3000; - private static int _originalManagedIdentityRetryDelay; private static int _originalEstsRetryDelay; @@ -107,6 +102,7 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -170,6 +166,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -224,6 +221,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -253,8 +251,8 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy var mi = miBuilder.Build(); - // max retry time (3 seconds), but make it one hundred times faster so the test completes quickly - double retryAfterSeconds = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER; + // make it one hundred times faster so the test completes quickly + double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( @@ -280,15 +278,334 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (LINEAR_POLICY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // this test can not be made one hundred times faster because it is based on a date + const int retryAfterMilliseconds = 3000; + var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterHttpDate); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + // (numRetries would be x2 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + // (numRetries would be x3 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.BadRequest); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 682b8fe469..3cfab68045 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -110,6 +110,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_404); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -174,6 +175,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_410); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -235,6 +237,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -294,6 +297,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -336,7 +340,6 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync } MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -346,10 +349,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // ensure that the first request was made and retried 3 times + // ensure that the third request was made and retried 3 times + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NUM_500; i++) @@ -365,7 +368,6 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } msalException = null; - stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -375,10 +377,11 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // ensure that the second request was made and retried 3 times + // ensure that the third request was made and retried 3 times + // (numRetries would be x2 if retry policy was NOT per request) + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NUM_500; i++) @@ -394,7 +397,6 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } msalException = null; - stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -404,10 +406,11 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that the third request was made and retried 3 times + // (numRetries would be x3 if retry policy was NOT per request) + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -445,7 +448,6 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy userAssignedIdentityId: userAssignedIdentityId); MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -455,10 +457,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -473,7 +475,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -496,7 +498,6 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin userAssignedIdentityId: userAssignedIdentityId); MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -506,10 +507,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); Assert.AreEqual(httpManager.QueueSize, 0); } } From d5f5978ad06a4fd85987da491262df0b2a340909 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 16:08:44 -0400 Subject: [PATCH 21/68] rebased branch from main to rginsburg_retry_policy_per_request --- .../Http/DefaultRetryPolicy.cs | 11 +++-- .../Http/ImdsRetryPolicy.cs | 2 - .../Instance/Region/RegionManager.cs | 12 ++--- .../Validation/AdfsAuthorityValidator.cs | 8 ++-- .../AzureArcManagedIdentitySource.cs | 8 ++-- .../ManagedIdentity/ManagedIdentityRequest.cs | 13 ++---- .../OAuth2/OAuth2Client.cs | 13 +++--- .../WsTrust/WsTrustWebRequestManager.cs | 12 ++--- .../CoreTests/HttpTests/HttpManagerTests.cs | 45 +++++++++---------- .../DefaultRetryPolicyTests.cs | 18 ++++---- .../ManagedIdentityTests/ImdsTests.cs | 14 +++--- .../ManagedIdentityTests.cs | 8 ++-- 12 files changed, 81 insertions(+), 83 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index cb59f160bd..b30fc0df80 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -11,8 +11,13 @@ class DefaultRetryPolicy : IRetryPolicy { private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); + public const int DefaultStsMaxRetries = 3; + public const int DefaultStsRetryDelayMs = 1000; + public const int DefaultManagedIdentityMaxRetries = 3; + public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster + // constants that are defined in the constructor - public static int DEFAULT_RETRY_DELAY_MS { get; set; } // this will be overridden in the unit tests so that they run faster + public static int DefaultRetryDelayMs { get; set; } // this will be overridden in the unit tests so that they run faster private int MAX_RETRIES; private readonly Func RETRY_CONDITION; @@ -21,7 +26,7 @@ class DefaultRetryPolicy : IRetryPolicy public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { - DEFAULT_RETRY_DELAY_MS = retryDelayMs; + DefaultRetryDelayMs = retryDelayMs; MAX_RETRIES = maxRetries; RETRY_CONDITION = retryCondition; } @@ -38,7 +43,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); - int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DefaultRetryDelayMs); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 82cd122fea..1ee4753e88 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -25,8 +25,6 @@ internal class ImdsRetryPolicy : IRetryPolicy private int _maxRetries; - - private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index b2b72a4a45..52f6521a94 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -46,9 +46,9 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static bool s_failedAutoDiscovery = false; private static string s_regionDiscoveryDetails; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public RegionManager( @@ -213,7 +213,7 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // A bad request occurs when the version in the IMDS call is no longer supported. @@ -231,7 +231,7 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // Call again with updated version } @@ -334,7 +334,7 @@ private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dict mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(userCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // When IMDS endpoint is called without the api version query param, bad request response comes back with latest version. diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 83eff0a534..31b7f7af2b 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -29,9 +29,9 @@ public async Task ValidateAuthorityAsync( var resource = $"https://{authorityInfo.Host}"; string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); - LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( @@ -44,7 +44,7 @@ public async Task ValidateAuthorityAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: _requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy + retryPolicy: defaultRetryPolicy ) .ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index a643ce7880..cf95fc9a0a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -123,9 +123,9 @@ protected override async Task HandleResponseAsync( _requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request."); request.Headers.Add("Authorization", authHeaderValue); - LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, HttpRetryConditions.Sts); response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( @@ -138,7 +138,7 @@ protected override async Task HandleResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: cancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 3541730c7b..0218beeb4a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -11,11 +11,6 @@ namespace Microsoft.Identity.Client.ManagedIdentity { internal class ManagedIdentityRequest { - // referenced in unit tests, cannot be private - public const int DefaultManagedIdentityMaxRetries = 3; - // this will be overridden in the unit tests so that they run faster - public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; - private readonly Uri _baseEndpoint; public HttpMethod Method { get; } @@ -36,10 +31,10 @@ public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retr BodyParameters = new Dictionary(); QueryParameters = new Dictionary(); - IRetryPolicy defaultRetryPolicy = new LinearRetryPolicy( - DefaultManagedIdentityRetryDelayMs, - DefaultManagedIdentityMaxRetries, - HttpRetryConditions.ManagedIdentity); + IRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, + HttpRetryConditions.DefaultManagedIdentity); RetryPolicy = retryPolicy ?? defaultRetryPolicy; } diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 462ee6c4a3..0eb1e08948 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -41,10 +41,6 @@ internal class OAuth2Client private readonly IDictionary _bodyParameters = new Dictionary(); private readonly IHttpManager _httpManager; private readonly X509Certificate2 _mtlsCertificate; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager, X509Certificate2 mtlsCertificate) { @@ -123,6 +119,11 @@ internal async Task ExecuteRequestAsync( using (requestContext.Logger.LogBlockDuration($"[Oauth2Client] Sending {method} request ")) { + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, + HttpRetryConditions.Sts); + try { if (method == HttpMethod.Post) @@ -145,7 +146,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: _mtlsCertificate, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); } else @@ -160,7 +161,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); } } diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index fb69357d42..4a026cdeef 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -21,9 +21,9 @@ namespace Microsoft.Identity.Client.WsTrust internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public WsTrustWebRequestManager(IHttpManager httpManager) @@ -57,7 +57,7 @@ public async Task GetMexDocumentAsync(string federationMetadataUrl, mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK) @@ -115,7 +115,7 @@ public async Task GetWsTrustResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (resp.StatusCode != System.Net.HttpStatusCode.OK) @@ -192,7 +192,7 @@ public async Task GetUserRealmAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index b9c3428145..be96aab846 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -12,7 +12,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -24,9 +23,9 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { - LinearRetryPolicy _stsLinearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); [TestInitialize] @@ -67,7 +66,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -107,7 +106,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); } } @@ -144,7 +143,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -172,7 +171,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -214,7 +213,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -246,7 +245,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -282,7 +281,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); } } @@ -305,7 +304,7 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -331,7 +330,7 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -362,7 +361,7 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); @@ -388,7 +387,7 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); @@ -414,7 +413,7 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); @@ -440,7 +439,7 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); @@ -467,7 +466,7 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); @@ -498,10 +497,10 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna } } - LinearRetryPolicy retryPolicy = isManagedIdentity ? new LinearRetryPolicy( - ManagedIdentityRequest.DefaultManagedIdentityRetryDelayMs, - ManagedIdentityRequest.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.ManagedIdentity) : _stsLinearRetryPolicy; + DefaultRetryPolicy defaultRetryPolicy = isManagedIdentity ? new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, + HttpRetryConditions.DefaultManagedIdentity) : _defaultRetryPolicy; var msalHttpResponse = await httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), @@ -513,7 +512,7 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: retryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(msalHttpResponse); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index b9ad25105e..430459ea06 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -57,7 +57,7 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { string userAssignedId = TestConstants.ClientId; UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; @@ -120,7 +120,7 @@ public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIden { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { string userAssignedId = TestConstants.ClientId; UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; @@ -184,7 +184,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -241,7 +241,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -302,7 +302,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -364,7 +364,7 @@ public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentityS { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -417,7 +417,7 @@ public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -524,7 +524,7 @@ public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -572,7 +572,7 @@ public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(Manag { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 3cfab68045..b6d4a15af0 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -62,7 +62,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -127,7 +127,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -192,7 +192,7 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -252,7 +252,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -312,7 +312,7 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -425,7 +425,7 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -475,7 +475,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 349c48ceda..6ab73bedf6 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -1269,7 +1269,7 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( var mi = miBuilder.Build(); // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = ManagedIdentityRequest.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + const int NumErrors = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( @@ -1286,7 +1286,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 4 total: request + 3 retries - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1306,7 +1306,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1326,7 +1326,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From c61c3e6a071da523dba2e9c54337c5db6f500bf6 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 20:10:31 -0400 Subject: [PATCH 22/68] Implemented final feedback --- .../Http/NoRetryPolicy.cs | 21 ------------------- .../Validation/AdfsAuthorityValidator.cs | 2 +- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs diff --git a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs deleted file mode 100644 index 3562d4c3ca..0000000000 --- a/src/client/Microsoft.Identity.Client/Http/NoRetryPolicy.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Identity.Client.Http -{ - internal class NoRetryPolicy : IRetryPolicy - { - public int DelayInMilliseconds { get => throw new NotImplementedException(); } - - public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount) - { - return false; - } - } -} diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 83eff0a534..471f79ef38 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -34,7 +34,7 @@ public async Task ValidateAuthorityAsync( LinearRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); - Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( + Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( new Uri(webFingerUrl), null, body: null, From 42ea075207c2480fc44977bf308ba4ba873e567b Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 21:54:16 -0400 Subject: [PATCH 23/68] Fixed some broken unit tests --- .../Http/DefaultRetryPolicy.cs | 24 ++++++------ .../ManagedIdentityTests.cs | 37 ++++++++++++++----- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index b30fc0df80..79678b047c 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -16,26 +16,28 @@ class DefaultRetryPolicy : IRetryPolicy public const int DefaultManagedIdentityMaxRetries = 3; public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster - // constants that are defined in the constructor - public static int DefaultRetryDelayMs { get; set; } // this will be overridden in the unit tests so that they run faster - private int MAX_RETRIES; - private readonly Func RETRY_CONDITION; - - // referenced in the unit tests + // used for comparison, in the unit tests public static int numRetries { get; private set; } = 0; - public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) + public static int DefaultRetryDelayMs;// { get; set; } // this will be overridden in the unit tests so that they run faster + private int MaxRetries; + private readonly Func RetryCondition; + + public DefaultRetryPolicy( + int retryDelayMs, + int maxRetries, + Func retryCondition) { DefaultRetryDelayMs = retryDelayMs; - MAX_RETRIES = maxRetries; - RETRY_CONDITION = retryCondition; + MaxRetries = maxRetries; + RetryCondition = retryCondition; } public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { // Check if the status code is retriable and if the current retry count is less than max retries - if (RETRY_CONDITION(response, exception) && - retryCount < MAX_RETRIES) + if (RetryCondition(response, exception) && + retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests numRetries = retryCount + 1; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 6ab73bedf6..36a819d23e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -38,10 +38,29 @@ public class ManagedIdentityTests : TestBase internal const string ExpectedErrorCode = "ErrorCode"; internal const string ExpectedCorrelationId = "Some GUID"; + private static int _originalDefaultManagedIdentityRetryDelayMs; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalDefaultManagedIdentityRetryDelayMs = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; + + // Speed up retry delays by 100x + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalDefaultManagedIdentityRetryDelayMs * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry delay values after all tests + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalDefaultManagedIdentityRetryDelayMs; + } + [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] - [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.Imds)] + [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(null, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(AzureArcEndpoint, ManagedIdentitySource.AzureArc, ManagedIdentitySource.AzureArc)] [DataRow(CloudShellEndpoint, ManagedIdentitySource.CloudShell, ManagedIdentitySource.CloudShell)] @@ -1241,7 +1260,7 @@ public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() [DataTestMethod] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.NotFound)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.RequestTimeout)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, 429)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, 429)] // not defined in HttpStatusCode enum [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.InternalServerError)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.ServiceUnavailable)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.GatewayTimeout)] @@ -1285,8 +1304,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1304,9 +1323,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries (DefaultRetryPolicy.numRetries would be 6 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1324,9 +1342,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries (DefaultRetryPolicy.numRetries would be 9 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From 6fefdf2c1377ae1b1cdd11b48c5a441ff06168cf Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 16:15:04 -0400 Subject: [PATCH 24/68] Fixed all broken unit tests and made STS tests 100 times faster --- .../Http/DefaultRetryPolicy.cs | 24 +- .../Http/HttpManagerFactory.cs | 8 - .../Http/ImdsRetryPolicy.cs | 43 +- .../ImdsManagedIdentitySource.cs | 2 + .../CoreTests/HttpTests/HttpManagerTests.cs | 172 +++-- .../DefaultRetryPolicyTests.cs | 707 ++++++++---------- .../ManagedIdentityTests/ImdsTests.cs | 647 +++++++--------- .../ManagedIdentityTests.cs | 13 +- 8 files changed, 720 insertions(+), 896 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 79678b047c..3f5b5dd21a 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -11,15 +11,19 @@ class DefaultRetryPolicy : IRetryPolicy { private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); - public const int DefaultStsMaxRetries = 3; - public const int DefaultStsRetryDelayMs = 1000; + // referenced in unit tests + public const int DefaultStsMaxRetries = 1; public const int DefaultManagedIdentityMaxRetries = 3; - public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster + + // overridden in the unit tests so that they run faster + public static int DefaultStsRetryDelayMs { get; set; } = 1000; + public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // used for comparison, in the unit tests - public static int numRetries { get; private set; } = 0; + // will be reset after every test + public static int NumRetries { get; set; } = 0; - public static int DefaultRetryDelayMs;// { get; set; } // this will be overridden in the unit tests so that they run faster + public static int DefaultRetryDelayMs; private int MaxRetries; private readonly Func RetryCondition; @@ -40,14 +44,18 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests - numRetries = retryCount + 1; + NumRetries = retryCount + 1; // Use HeadersAsDictionary to check for "Retry-After" header - response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); + string retryAfter = string.Empty; + if (response?.HeadersAsDictionary != null) + { + response.HeadersAsDictionary.TryGetValue("Retry-After", out retryAfter); + } int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DefaultRetryDelayMs); - logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {NumRetries})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index a1c1527913..120735fbeb 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,14 +8,6 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { - // referenced in unit tests, cannot be private - public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; - public const int DEFAULT_ESTS_MAX_RETRIES = 1; - - // these will be overridden in the unit tests so that they run faster - public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; - public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; - public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries = false) diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 1ee4753e88..d9f4ece919 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,25 +10,28 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + private const int HttpStatusGoneRetryAfterMsInternal = 10 * 1000; // 10 seconds - // referenced in unit tests, cannot be private - public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; - public const int LINEAR_STRATEGY_NUM_RETRIES = 7; - public static int numRetries { get; private set; } = 0; + // referenced in unit tests + public const int ExponentialStrategyNumRetries = 3; + public const int LinearStrategyNumRetries = 7; - // these will be overridden in the unit tests so that they run faster - public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; - public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; - public static int EXPONENTIAL_DELTA_BACKOFF_MS { get; set; } = 2000; - public static int HTTP_STATUS_GONE_RETRY_AFTER_MS { get; set; } = HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL; + // used for comparison, in the unit tests + // will be reset after every test + public static int NumRetries { get; set; } = 0; - private int _maxRetries; + // overridden in the unit tests so that they run faster + public static int MinExponentialBackoffMs { get; set; } = 1000; + public static int MaxExponentialBackoffMs { get; set; } = 4000; + public static int ExponentialDeltaBackoffMs { get; set; } = 2000; + public static int HttpStatusGoneRetryAfterMs { get; set; } = HttpStatusGoneRetryAfterMsInternal; + + private int MaxRetries; private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS + ImdsRetryPolicy.MinExponentialBackoffMs, + ImdsRetryPolicy.MaxExponentialBackoffMs, + ImdsRetryPolicy.ExponentialDeltaBackoffMs ); public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) @@ -38,20 +41,20 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (retryCount == 0) { // Calculate the maxRetries based on the status code, once per request - _maxRetries = httpStatusCode == (int)HttpStatusCode.Gone - ? LINEAR_STRATEGY_NUM_RETRIES - : EXPONENTIAL_STRATEGY_NUM_RETRIES; + MaxRetries = httpStatusCode == (int)HttpStatusCode.Gone + ? LinearStrategyNumRetries + : ExponentialStrategyNumRetries; } // Check if the status code is retriable and if the current retry count is less than max retries if (HttpRetryConditions.Imds(response, exception) && - retryCount < _maxRetries) + retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests - numRetries = retryCount + 1; + NumRetries = retryCount + 1; int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone - ? HTTP_STATUS_GONE_RETRY_AFTER_MS + ? HttpStatusGoneRetryAfterMs : _exponentialRetryStrategy.CalculateDelay(retryCount); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 6cfb8854e6..59daa01a41 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,6 +82,8 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } + request.RetryPolicy = new ImdsRetryPolicy(); + return request; } diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index be96aab846..bee5fb4dd9 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -23,15 +23,37 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { - DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + private static int _originalStsRetryDelay; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalStsRetryDelay = DefaultRetryPolicy.DefaultStsRetryDelayMs; + + // Speed up retry delays by 100x + DefaultRetryPolicy.DefaultStsRetryDelayMs = (int)(_originalStsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + DefaultRetryPolicy.DefaultStsRetryDelayMs = _originalStsRetryDelay; + } + + private DefaultRetryPolicy StsRetryPolicy; [TestInitialize] public void TestInitialize() { TestCommon.ResetInternalStaticCaches(); + + DefaultRetryPolicy.NumRetries = 0; + StsRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, + HttpRetryConditions.Sts); } [TestMethod] @@ -66,7 +88,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -106,7 +128,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); } } @@ -143,7 +165,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -171,7 +193,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -213,7 +235,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -245,7 +267,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -281,13 +303,13 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); } } [TestMethod] - public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() + public async Task TestSendGetWithHttp500TypeFailureWithInternalRetriesDisabledAsync() { using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { @@ -304,10 +326,11 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); } } @@ -316,8 +339,12 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.InternalServerError); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); + } var ex = await Assert.ThrowsExceptionAsync(() => httpManager.SendRequestAsync( @@ -330,10 +357,12 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -361,11 +390,38 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + } + } + + [TestMethod] + public async Task NoResiliencyIfHttpErrorNotRetriableAsync() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadRequest); + + var msalHttpResponse = await httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: new StringContent("body"), + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: true, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: StsRetryPolicy) + .ConfigureAwait(false); + + Assert.AreEqual(HttpStatusCode.BadRequest, msalHttpResponse.StatusCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); } } @@ -374,23 +430,29 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.BadGateway); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.BadGateway); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadGateway); + } var msalHttpResponse = await httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: null, body: new StringContent("body"), - method: HttpMethod.Post, + method: HttpMethod.Get, logger: Substitute.For(), doNotThrow: true, mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -399,8 +461,12 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.GatewayTimeout); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); + } var exc = await AssertException.TaskThrowsAsync(() => httpManager.SendRequestAsync( @@ -413,10 +479,12 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -439,11 +507,12 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); } } @@ -466,60 +535,11 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); - } - } - - [TestMethod] - [DataRow(true, false)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(false, true)] - public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInternalRetries, bool isManagedIdentity) - { - using (var httpManager = new MockHttpManager(disableInternalRetries: disableInternalRetries)) - { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - - if (!disableInternalRetries) - { - //Adding second response for retry - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - - // Add 2 more response for the managed identity flow since 3 retries happen in this scenario - if (isManagedIdentity) - { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - } - } - - DefaultRetryPolicy defaultRetryPolicy = isManagedIdentity ? new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, - DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.DefaultManagedIdentity) : _defaultRetryPolicy; - - var msalHttpResponse = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: new StringContent("body"), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: true, - mtlsCertificate: null, - validateServerCert: null, - cancellationToken: default, - retryPolicy: defaultRetryPolicy) - .ConfigureAwait(false); - - Assert.IsNotNull(msalHttpResponse); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, msalHttpResponse.StatusCode); - //If a second request is sent when retry is configured to false, the test will fail since - //the MockHttpManager will not be able to serve another response. - //The MockHttpManager will also check for unused responses which will check if the retry did not occur when it should have. + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index 430459ea06..f8d05c4c91 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -18,95 +18,99 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests { /// /// The Default Retry Policy applies to: - /// ESTS (Azure AD) + /// STS (Azure AD) (Tested in HttpManagerTests.cs) /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric /// [TestClass] public class DefaultRetryPolicyTests : TestBase { private static int _originalManagedIdentityRetryDelay; - private static int _originalEstsRetryDelay; [ClassInitialize] public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalManagedIdentityRetryDelay = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS; - _originalEstsRetryDelay = HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS; + _originalManagedIdentityRetryDelay = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; // Speed up retry delays by 100x - HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); - HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = (int)(_originalEstsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = _originalManagedIdentityRetryDelay; - HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = _originalEstsRetryDelay; + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalManagedIdentityRetryDelay; + } + + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + + DefaultRetryPolicy.NumRetries = 0; } [DataTestMethod] // see test class header: all sources that allow UAMI [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task UAMIFails500OnceThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) - { - string userAssignedId = TestConstants.ClientId; - UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -114,61 +118,61 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task UAMIFails500PermanentlyAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - using (var httpManager = new MockHttpManager()) + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) { - string userAssignedId = TestConstants.ClientId; - UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - - ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (1 second * 3 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (1 second * 3 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -178,54 +182,54 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); - - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -235,58 +239,58 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); + // make it one hundred times faster so the test completes quickly + double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; - // make it one hundred times faster so the test completes quickly - double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterSeconds.ToString()); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterSeconds.ToString()); - - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -296,59 +300,59 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); + // this test can not be made one hundred times faster because it is based on a date + const int retryAfterMilliseconds = 3000; + var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); - // this test can not be made one hundred times faster because it is based on a date - const int retryAfterMilliseconds = 3000; - var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterHttpDate); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterHttpDate); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); - - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -358,157 +362,50 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500Permanently( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); } - } - } - - [DataTestMethod] // see test class header: all sources allow SAMI - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the second request was made and retried 3 times - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + msalException = ex as MsalServiceException; } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -518,45 +415,45 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.BadRequest); - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.BadRequest); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -566,45 +463,45 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(disableInternalRetries: true)) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index b6d4a15af0..17d575a5d7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -19,9 +20,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds - private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds - private static int _originalMinBackoff; private static int _originalMaxBackoff; private static int _originalDeltaBackoff; @@ -31,488 +29,385 @@ public class ImdsTests : TestBase public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalMinBackoff = ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS; - _originalMaxBackoff = ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS; - _originalDeltaBackoff = ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS; - _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; + _originalMinBackoff = ImdsRetryPolicy.MinExponentialBackoffMs; + _originalMaxBackoff = ImdsRetryPolicy.MaxExponentialBackoffMs; + _originalDeltaBackoff = ImdsRetryPolicy.ExponentialDeltaBackoffMs; + _originalGoneRetryAfter = ImdsRetryPolicy.HttpStatusGoneRetryAfterMs; // Speed up retry delays by 100x - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MinExponentialBackoffMs = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MaxExponentialBackoffMs = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.ExponentialDeltaBackoffMs = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = _originalMinBackoff; - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = _originalMaxBackoff; - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = _originalDeltaBackoff; - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = _originalGoneRetryAfter; + ImdsRetryPolicy.MinExponentialBackoffMs = _originalMinBackoff; + ImdsRetryPolicy.MaxExponentialBackoffMs = _originalMaxBackoff; + ImdsRetryPolicy.ExponentialDeltaBackoffMs = _originalDeltaBackoff; + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = _originalGoneRetryAfter; + } + + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + + ImdsRetryPolicy.NumRetries = 0; } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails404TwiceThenSucceeds200Async( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager()) + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404 = 2; - for (int i = 0; i < NUM_404; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.NotFound, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), + MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - var stopwatch = Stopwatch.StartNew(); + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); - stopwatch.Stop(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - // exponential backoff (1 second -> 2 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + stopwatch.Stop(); - // ensure that exactly 3 requests were made: initial request + 2 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_404); - Assert.AreEqual(httpManager.QueueSize, 0); + // exponential backoff (1 second -> 2 seconds) + const int ImdsExponentialStrategyTwoRetriesInMs = 3000; + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyTwoRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_404); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails410FourTimesThenSucceeds200Async( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); - using (var httpManager = new MockHttpManager()) + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate four 410s (to trigger retries), then a successful response - const int NUM_410 = 4; - for (int i = 0; i < NUM_410; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), + MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - var stopwatch = Stopwatch.StartNew(); + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); - stopwatch.Stop(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - // linear backoff (10 seconds * 4 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + stopwatch.Stop(); - // ensure that exactly 5 requests were made: initial request + 4 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_410); - Assert.AreEqual(httpManager.QueueSize, 0); + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_410); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails410PermanentlyAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 410s (to trigger the maximum number of retries) - const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) - for (int i = 0; i < NUM_410; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (10 seconds * 7 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 8 requests were made: initial request + 7 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - } - } - } + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + var mi = miBuilder.Build(); - using (var httpManager = new MockHttpManager()) + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LinearStrategyNumRetries + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 504s (to trigger the maximum number of retries) - const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_504; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // exponential backoff (1 second -> 2 seconds -> 4 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } - } - } - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - - using (var httpManager = new MockHttpManager()) + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * ImdsRetryPolicy.LinearStrategyNumRetries * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.LinearStrategyNumRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails504PermanentlyAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); + // Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.ExponentialStrategyNumRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.BadRequest, + statusCode: HttpStatusCode.GatewayTimeout, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + const int ImdsExponentialStrategyMaxRetriesInMs = 7000; + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyMaxRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.ExponentialStrategyNumRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - - using (var httpManager = new MockHttpManager(disableInternalRetries: true)) + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.BadRequest, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + try { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 36a819d23e..5af8dbfcf7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -57,6 +57,13 @@ public static void ClassCleanup() DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalDefaultManagedIdentityRetryDelayMs; } + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + ImdsRetryPolicy.NumRetries = 0; + } + [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] @@ -1305,7 +1312,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1324,7 +1331,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries (DefaultRetryPolicy.numRetries would be 6 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1343,7 +1350,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries (DefaultRetryPolicy.numRetries would be 9 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From 667127176965d0c5b4935e22375d68ece392c994 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 18:25:25 -0400 Subject: [PATCH 25/68] whitespace --- .../ManagedIdentityTests/ManagedIdentityTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 5af8dbfcf7..e913433baa 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -61,6 +61,7 @@ public static void ClassCleanup() public override void TestInitialize() { base.TestInitialize(); + ImdsRetryPolicy.NumRetries = 0; } From 990847e42148ca2461beb666172f54633e7602a7 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 23 Apr 2025 17:17:39 -0400 Subject: [PATCH 26/68] Retry policies are now pre request instead of per HttpManager. --- .../Http/HttpManager.cs | 16 ++++++++++++++++ .../ManagedIdentity/ImdsManagedIdentitySource.cs | 3 +++ 2 files changed, 19 insertions(+) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 4e54e426f5..bc643b2dab 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -26,6 +26,11 @@ namespace Microsoft.Identity.Client.Http /// internal class HttpManager : IHttpManager { + // referenced in unit tests, cannot be private + public const int DEFAULT_ESTS_MAX_RETRIES = 1; + // this will be overridden in the unit tests so that they run faster + public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; + protected readonly IMsalHttpClientFactory _httpClientFactory; private readonly bool _disableInternalRetries; public long LastRequestDurationInMs { get; private set; } @@ -65,6 +70,17 @@ public async Task SendRequestAsync( IRetryPolicy retryPolicy, int retryCount = 0) { + // Use the default STS retry policy if the request is not for managed identity + // and a non-default STS retry policy is not provided. + // Skip this if statement the dev indicated that they do not want retry logic. + if (!_isManagedIdentity && retryPolicy == null && _withRetry) + { + retryPolicy = new LinearRetryPolicy( + DEFAULT_ESTS_RETRY_DELAY_MS, + DEFAULT_ESTS_MAX_RETRIES, + HttpRetryConditions.Sts); + } + Exception timeoutException = null; HttpResponse response = null; diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 6cfb8854e6..9c8ca08e8b 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,6 +82,9 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } + // uncomment in follow-up IMDS retry policy PR + // request.RetryPolicy = new ImdsRetryPolicy(); + return request; } From fc7f97ce3b0b46d88aa9981d82b8e89c012833b9 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 25 Apr 2025 15:32:16 -0400 Subject: [PATCH 27/68] Addressed some GitHub feedback --- src/client/Microsoft.Identity.Client/Http/HttpManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index bc643b2dab..67e1c4b6d6 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -73,7 +73,7 @@ public async Task SendRequestAsync( // Use the default STS retry policy if the request is not for managed identity // and a non-default STS retry policy is not provided. // Skip this if statement the dev indicated that they do not want retry logic. - if (!_isManagedIdentity && retryPolicy == null && _withRetry) + if (!_isManagedIdentity && retryPolicy == null && !_disableInternalRetries) { retryPolicy = new LinearRetryPolicy( DEFAULT_ESTS_RETRY_DELAY_MS, From b3cdc4b85da695dce9c7692fb7d49676b9bb39e8 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 29 Apr 2025 16:29:47 -0400 Subject: [PATCH 28/68] Implemented GitHub feedback --- .../Http/HttpManager.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 67e1c4b6d6..4e54e426f5 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -26,11 +26,6 @@ namespace Microsoft.Identity.Client.Http /// internal class HttpManager : IHttpManager { - // referenced in unit tests, cannot be private - public const int DEFAULT_ESTS_MAX_RETRIES = 1; - // this will be overridden in the unit tests so that they run faster - public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; - protected readonly IMsalHttpClientFactory _httpClientFactory; private readonly bool _disableInternalRetries; public long LastRequestDurationInMs { get; private set; } @@ -70,17 +65,6 @@ public async Task SendRequestAsync( IRetryPolicy retryPolicy, int retryCount = 0) { - // Use the default STS retry policy if the request is not for managed identity - // and a non-default STS retry policy is not provided. - // Skip this if statement the dev indicated that they do not want retry logic. - if (!_isManagedIdentity && retryPolicy == null && !_disableInternalRetries) - { - retryPolicy = new LinearRetryPolicy( - DEFAULT_ESTS_RETRY_DELAY_MS, - DEFAULT_ESTS_MAX_RETRIES, - HttpRetryConditions.Sts); - } - Exception timeoutException = null; HttpResponse response = null; From 7ccb8c8363b19e55ed273764ea3ee345dd1c08fe Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 14:02:27 -0400 Subject: [PATCH 29/68] Fixed broken unit tests, update public API --- .../AppConfig/BaseAbstractApplicationBuilder.cs | 2 +- .../PublicApi/net462/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net472/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 2 +- .../PublicApi/netstandard2.0/PublicAPI.Shipped.txt | 2 +- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index 2fc2fa6f95..189ac38dcf 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -71,7 +71,7 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// If you only want to configure the retryOnceOn5xx parameter, set httpClientFactory to null and MSAL will use the default http client. /// /// The builder to chain the .With methods - public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) + public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) { Config.HttpClientFactory = httpClientFactory; Config.DisableInternalRetries = !retryOnceOn5xx; diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt index ff692e2c0b..42feff1494 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt index ff692e2c0b..42feff1494 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt index a09bc452f1..b4422dca2d 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index cc4d5e1dd0..69604253b5 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index c21b99152f..18513f7ef9 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: false) + disableInternalRetries: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From 848987f14c5aaa4aadb57b6ed16539e59fc6642f Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 14:32:22 -0400 Subject: [PATCH 30/68] Undid public API changes --- .../AppConfig/BaseAbstractApplicationBuilder.cs | 2 +- .../PublicApi/net462/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net472/PublicAPI.Shipped.txt | 2 +- .../PublicApi/net8.0/PublicAPI.Shipped.txt | 2 +- .../PublicApi/netstandard2.0/PublicAPI.Shipped.txt | 2 +- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index 189ac38dcf..2fc2fa6f95 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -71,7 +71,7 @@ public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory) /// If you only want to configure the retryOnceOn5xx parameter, set httpClientFactory to null and MSAL will use the default http client. /// /// The builder to chain the .With methods - public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) + public T WithHttpClientFactory(IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) { Config.HttpClientFactory = httpClientFactory; Config.DisableInternalRetries = !retryOnceOn5xx; diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt index 42feff1494..ff692e2c0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt index 42feff1494..ff692e2c0b 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Shipped.txt @@ -396,7 +396,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt index b4422dca2d..a09bc452f1 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt index 69604253b5..cc4d5e1dd0 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -393,7 +393,7 @@ Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithClientVersion(st Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithDebugLoggingCallback(Microsoft.Identity.Client.LogLevel logLevel = Microsoft.Identity.Client.LogLevel.Info, bool enablePiiLogging = false, bool withDefaultPlatformLoggingEnabled = false) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithExperimentalFeatures(bool enableExperimentalFeatures = true) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory) -> T -Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries) -> T +Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithHttpClientFactory(Microsoft.Identity.Client.IMsalHttpClientFactory httpClientFactory, bool retryOnceOn5xx) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.Identity.Client.LogCallback loggingCallback, Microsoft.Identity.Client.LogLevel? logLevel = null, bool? enablePiiLogging = null, bool? enableDefaultPlatformLogging = null) -> T Microsoft.Identity.Client.BaseAbstractApplicationBuilder.WithLogging(Microsoft.IdentityModel.Abstractions.IIdentityLogger identityLogger, bool enablePiiLogging = false) -> T Microsoft.Identity.Client.BaseApplicationOptions diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index 18513f7ef9..c21b99152f 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - disableInternalRetries: false) + retryOnceOn5xx: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From 383d318eb379ba577e790625c7990b90694b7387 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 15:19:06 -0400 Subject: [PATCH 31/68] Fixed broken unit test and clarified public api --- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index c21b99152f..0cc2b98875 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: false) + retryOnceOn5xx: true) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From ae69c1d2cb5d4744dc2f917c3fd40dce89394bd8 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 30 Apr 2025 15:26:43 -0400 Subject: [PATCH 32/68] removed comment, adjusted unit test --- .../ManagedIdentity/ImdsManagedIdentitySource.cs | 3 --- .../PublicApiTests/RetryPolicyTests.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 9c8ca08e8b..6cfb8854e6 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,9 +82,6 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } - // uncomment in follow-up IMDS retry policy PR - // request.RetryPolicy = new ImdsRetryPolicy(); - return request; } diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs index 0cc2b98875..c21b99152f 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/RetryPolicyTests.cs @@ -29,7 +29,7 @@ public async Task RetryPolicyAsync() .WithHttpManager(httpManager) .WithHttpClientFactory( httpClientFactory: null, - retryOnceOn5xx: true) + retryOnceOn5xx: false) .BuildConcrete(); httpManager.AddInstanceDiscoveryMockHandler(); From f0d98ed7c63a4a80c771c11f4bcc6d6216e672d7 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 14:19:30 -0400 Subject: [PATCH 33/68] Deleted file --- .../MacMauiAppWithBroker/MacMauiAppWithBroker.csproj | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj index 0518b810b7..f838fcc973 100644 --- a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj +++ b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj @@ -112,11 +112,7 @@ namespace MacMauiAppWithBroker } - + @@ -128,5 +124,8 @@ namespace MacMauiAppWithBroker + + + From e13e0d844e43870db3bc9200b246c24b129f1b5f Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 14:23:02 -0400 Subject: [PATCH 34/68] Undid file change from main --- .../MacMauiAppWithBroker/MacMauiAppWithBroker.csproj | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj index f838fcc973..0518b810b7 100644 --- a/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj +++ b/tests/devapps/MacMauiAppWithBroker/MacMauiAppWithBroker.csproj @@ -112,7 +112,11 @@ namespace MacMauiAppWithBroker } - + @@ -124,8 +128,5 @@ namespace MacMauiAppWithBroker - - - From 39164fb479d16cf9c5db4331400e2e96d08910ca Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 11 Apr 2025 16:18:11 -0400 Subject: [PATCH 35/68] Reworked retry policy functionality. Created IMDS retry policy. --- .../Http/DefaultRetryPolicy.cs | 49 +++++++++++++++ .../Http/ExponentialRetryStrategy.cs | 55 ++++++++++++++++ .../Http/HttpManager.cs | 7 ++- .../Http/HttpManagerFactory.cs | 5 ++ .../Http/HttpRetryCondition.cs | 24 ++++++- .../Http/IRetryPolicy.cs | 7 +-- .../Http/ImdsRetryPolicy.cs | 63 +++++++++++++++++++ .../Http/LinearRetryPolicy.cs | 37 ----------- .../Http/LinearRetryStrategy.cs | 41 ++++++++++++ 9 files changed, 241 insertions(+), 47 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs delete mode 100644 src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs new file mode 100644 index 0000000000..12abebdc08 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; + +namespace Microsoft.Identity.Client.Http +{ + class DefaultRetryPolicy : IRetryPolicy + { + // this will be overridden in the unit tests so that they run faster + public static int RETRY_DELAY_MS { get; set; } + + private int _maxRetries; + + private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); + + private readonly Func _retryCondition; + + public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) + { + RETRY_DELAY_MS = retryDelayMs; + _maxRetries = maxRetries; + _retryCondition = retryCondition; + } + + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) + { + // Check if the status code is retriable and if the current retry count is less than max retries + if (_retryCondition(response, exception) && + retryCount < _maxRetries) + { + // Use HeadersAsDictionary to check for "Retry-After" header + response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); + + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, RETRY_DELAY_MS); + + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + + // Pause execution for the calculated delay + await Task.Delay(retryAfterDelay).ConfigureAwait(false); + } + + // If the status code is not retriable or max retries have been reached, do not retry + return false; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs new file mode 100644 index 0000000000..3299f245fb --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.Client.Http +{ + internal class ExponentialRetryStrategy + { + // Minimum backoff time in milliseconds + private int _minExponentialBackoff; + // Maximum backoff time in milliseconds + private int _maxExponentialBackoff; + // Maximum backoff time in milliseconds + private int _exponentialDeltaBackoff; + + public ExponentialRetryStrategy(int minExponentialBackoff, int maxExponentialBackoff, int exponentialDeltaBackoff) + { + _minExponentialBackoff = minExponentialBackoff; + _maxExponentialBackoff = maxExponentialBackoff; + _exponentialDeltaBackoff = exponentialDeltaBackoff; + } + + /// + /// Calculates the exponential delay based on the current retry attempt. + /// + /// The current retry attempt number. + /// The calculated exponential delay in milliseconds. + /// + /// The delay is calculated using the formula: + /// - If is 0, it returns the minimum backoff time. + /// - Otherwise, it calculates the delay as the minimum of: + /// - (2^(currentRetry - 1)) * deltaBackoff + /// - maxBackoff + /// This ensures that the delay increases exponentially with each retry attempt, + /// but does not exceed the maximum backoff time. + /// + public int calculateDelay(int currentRetry) + { + // Attempt 1 + if (currentRetry == 0) + { + return _minExponentialBackoff; + } + + // Attempt 2+ + int exponentialDelay = Math.Min( + (int)(Math.Pow(2, currentRetry - 1) * _exponentialDeltaBackoff), + _maxExponentialBackoff + ); + + return exponentialDelay; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index 4e54e426f5..c8f45813ab 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -110,10 +110,11 @@ public async Task SendRequestAsync( logger.Error("The HTTP request failed. " + exception.Message); timeoutException = exception; } - - while (!_disableInternalRetries && retryPolicy.PauseForRetry(response, timeoutException, retryCount)) + + while (!_disableInternalRetries && await retryPolicy.PauseForRetryAsync(response, timeoutException, retryCount, logger).ConfigureAwait(false)) { - logger.Warning($"Retry condition met. Retry count: {retryCount++} after waiting {retryPolicy.DelayInMilliseconds}ms."); + retryCount++; + return await SendRequestAsync( endpoint, headers, diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 120735fbeb..9a473a0b54 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,6 +8,11 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { + private const int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = 1000; + private const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + private const int DEFAULT_ESTS_RETRY_DELAY_MS = 1000; + private const int DEFAULT_ESTS_MAX_RETRIES = 1; + public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries = false) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs index cf83c25516..d6e8c1fa8d 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs @@ -12,7 +12,7 @@ internal static class HttpRetryConditions /// Retry policy specific to managed identity flow. /// Avoid changing this, as it's breaking change. /// - public static bool ManagedIdentity(HttpResponse response, Exception exception) + public static bool DefaultManagedIdentity(HttpResponse response, Exception exception) { if (exception != null) { @@ -21,12 +21,32 @@ public static bool ManagedIdentity(HttpResponse response, Exception exception) return (int)response.StatusCode switch { - //Not Found + // Not Found, Request Timeout, Too Many Requests, Server Error, Service Unavailable, Gateway Timeout 404 or 408 or 429 or 500 or 503 or 504 => true, _ => false, }; } + /// + /// Retry policy specific to IMDS Managed Identity. + /// + public static bool Imds(HttpResponse response, Exception exception) + { + if (exception != null) + { + return exception is TaskCanceledException ? true : false; + } + + return (int)response.StatusCode switch + { + // Not Found, Request Timeout, Gone, Too Many Requests + 404 or 408 or 410 or 429 => true, + // Server Error range + >= 500 and <= 599 => true, + _ => false, + }; + } + /// /// Retry condition for /token and /authorize endpoints /// diff --git a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs index db3b466759..cb3b74c45c 100644 --- a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs @@ -2,16 +2,13 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; namespace Microsoft.Identity.Client.Http { internal interface IRetryPolicy { - int DelayInMilliseconds { get; } - bool PauseForRetry(HttpResponse response, Exception exception, int retryCount); + Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger); } } diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs new file mode 100644 index 0000000000..d5bbcb75f9 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Identity.Client.Core; + +namespace Microsoft.Identity.Client.Http +{ + internal class ImdsRetryPolicy : IRetryPolicy + { + private const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; + private const int LINEAR_STRATEGY_NUM_RETRIES = 7; + private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + + // these will be overridden in the unit tests so that they run faster + public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; + public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; + public static int EXPONENTIAL_DELTA_BACKOFF_MS { get; set; } = 2000; + public static int HTTP_STATUS_GONE_RETRY_AFTER_MS { get; set; } = HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL; + + private int _maxRetries; + + private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS + ); + + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) + { + int httpStatusCode = (int)response.StatusCode; + + if (retryCount == 0) + { + // Calculate the maxRetries based on the status code, once per request + _maxRetries = httpStatusCode == (int)HttpStatusCode.Gone + ? LINEAR_STRATEGY_NUM_RETRIES + : EXPONENTIAL_STRATEGY_NUM_RETRIES; + } + + // Check if the status code is retriable and if the current retry count is less than max retries + if (HttpRetryConditions.Imds(response, exception) && + retryCount < _maxRetries) + { + int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone + ? HTTP_STATUS_GONE_RETRY_AFTER_MS + : _exponentialRetryStrategy.calculateDelay(retryCount); + + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + + // Pause execution for the calculated delay + await Task.Delay(retryAfterDelay).ConfigureAwait(false); + + return true; + } + + // If the status code is not retriable or max retries have been reached, do not retry + return false; + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs deleted file mode 100644 index fc9ce94dec..0000000000 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryPolicy.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.Identity.Client.Http -{ - internal class LinearRetryPolicy : IRetryPolicy - { - // referenced in unit tests, cannot be private - public static int numRetries { get; private set; } = 0; - public const int DefaultStsMaxRetries = 1; - // this will be overridden in the unit tests so that they run faster - public static int DefaultStsRetryDelayMs { get; set; } = 1000; - - private int _maxRetries; - private readonly Func _retryCondition; - public int DelayInMilliseconds { private set; get; } - - public LinearRetryPolicy(int delayMilliseconds, int maxRetries, Func retryCondition) - { - DelayInMilliseconds = delayMilliseconds; - _maxRetries = maxRetries; - _retryCondition = retryCondition; - } - - public bool PauseForRetry(HttpResponse response, Exception exception, int retryCount) - { - // referenced in the unit tests - numRetries = retryCount + 1; - - return retryCount < _maxRetries && _retryCondition(response, exception); - } - } -} diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs new file mode 100644 index 0000000000..cecb38840f --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Identity.Client.Http +{ + internal class LinearRetryStrategy + { + /// + /// Calculates the number of milliseconds to sleep based on the `Retry-After` HTTP header. + /// + /// The value of the `Retry-After` HTTP header. This can be either a number of seconds or an HTTP date string. + /// The minimum delay in milliseconds to return if the header is not present or invalid. + /// The number of milliseconds to sleep before retrying the request. + public int calculateDelay(string retryHeader, int minimumDelay) + { + if (string.IsNullOrEmpty(retryHeader)) + { + return minimumDelay; + } + + // Try parsing the retry-after header as seconds + if (double.TryParse(retryHeader, out double seconds)) + { + int millisToSleep = (int)Math.Round(seconds * 1000); + return Math.Max(minimumDelay, millisToSleep); + } + + // If parsing as seconds fails, try parsing as an HTTP date + if (DateTime.TryParse(retryHeader, out DateTime retryDate)) + { + int millisToSleep = (int)(retryDate - DateTime.UtcNow).TotalMilliseconds; + return Math.Max(minimumDelay, millisToSleep); + } + + // If all parsing fails, return the minimum delay + return minimumDelay; + } + } +} From 4cab624ecddd745997d629045d81e7ce8facae95 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 14 Apr 2025 18:55:26 -0400 Subject: [PATCH 36/68] Implemented GitHub Feedback --- .../Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs | 2 +- src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs index 3299f245fb..5893c03ad0 100644 --- a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs @@ -35,7 +35,7 @@ public ExponentialRetryStrategy(int minExponentialBackoff, int maxExponentialBac /// This ensures that the delay increases exponentially with each retry attempt, /// but does not exceed the maximum backoff time. /// - public int calculateDelay(int currentRetry) + public int CalculateDelay(int currentRetry) { // Attempt 1 if (currentRetry == 0) diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index d5bbcb75f9..4694e1b929 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -46,7 +46,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce { int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone ? HTTP_STATUS_GONE_RETRY_AFTER_MS - : _exponentialRetryStrategy.calculateDelay(retryCount); + : _exponentialRetryStrategy.CalculateDelay(retryCount); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); From 39180d9ea75a1b43c2e6e2adf6c0ae0a9943479f Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 15 Apr 2025 20:24:17 -0400 Subject: [PATCH 37/68] Created first retry unit test. The rest of the tests will be based off of this one. --- .../TestConstants.cs | 3 +- .../DefaultRetryPolicy.cs | 15 +++ .../ManagedIdentityTests/ImdsTests.cs | 99 ++++++++++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index e40f68a97a..32bfbd3ea1 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -202,7 +202,8 @@ public static HashSet s_scope public const string Region = "centralus"; public const string InvalidRegion = "invalidregion"; public const int TimeoutInMs = 2000; - public const string ImdsUrl = "http://169.254.169.254/metadata/instance/compute/location"; + public const string ImdsHost = "169.254.169.254"; + public const string ImdsUrl = $"http://{ImdsHost}/metadata/instance/compute/location"; public const string UserAssertion = "fake_access_token"; public const string CodeVerifier = "someCodeVerifier"; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs new file mode 100644 index 0000000000..2cf5c17888 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests +{ + class DefaultRetryPolicy + { + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 6fe43611ee..43a35f4578 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; +using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -17,6 +19,101 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { + private const double ONE_HUNDRED_TIMES_FASTER = 0.01; + private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 -> 2 + + private static int _originalMinBackoff; + private static int _originalMaxBackoff; + private static int _originalDeltaBackoff; + private static int _originalGoneRetryAfter; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalMinBackoff = ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS; + _originalMaxBackoff = ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS; + _originalDeltaBackoff = ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS; + _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; + + // Speed up retry delays by 100x + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = _originalMinBackoff; + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = _originalMaxBackoff; + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = _originalDeltaBackoff; + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = _originalGoneRetryAfter; + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404S = 2; + for (int i = 0; i < NUM_404S; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // ensure that each retry followed the exponential backoff strategy + // 2 x exponential backoff (1 second -> 2 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] From d2a01a13c7c4d1c880ff95e22239faa2f86f7b8c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 16 Apr 2025 17:27:47 -0400 Subject: [PATCH 38/68] Added more ImdsRetryPolicy unit tests. Fixed bug discovered by unit tests. --- global.json | 8 +- .../Http/ImdsRetryPolicy.cs | 4 +- .../ManagedIdentityTests/ImdsTests.cs | 175 +++++++++++++++++- 3 files changed, 176 insertions(+), 11 deletions(-) diff --git a/global.json b/global.json index 66e4a5c8a7..cfbcf3eaec 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "8.0.404", - "rollForward": "latestFeature" - } + "sdk": { + "version": "9.0.201", + "rollForward": "latestFeature" + } } diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 4694e1b929..8421df131c 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,8 +10,8 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - private const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; - private const int LINEAR_STRATEGY_NUM_RETRIES = 7; + public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; // referenced in unit tests + public const int LINEAR_STRATEGY_NUM_RETRIES = 7; // referenced in unit tests private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds // these will be overridden in the unit tests so that they run faster diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 43a35f4578..efae53f033 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -12,6 +12,7 @@ using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenTelemetry.Resources; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -20,7 +21,8 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests public class ImdsTests : TestBase { private const double ONE_HUNDRED_TIMES_FASTER = 0.01; - private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 -> 2 + private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds + private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds private static int _originalMinBackoff; private static int _originalMaxBackoff; @@ -75,8 +77,8 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U var mi = miBuilder.Build(); // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404S = 2; - for (int i = 0; i < NUM_404S; i++) + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -105,8 +107,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U stopwatch.Stop(); - // ensure that each retry followed the exponential backoff strategy - // 2 x exponential backoff (1 second -> 2 seconds) + // exponential backoff (1 second -> 2 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries @@ -114,6 +115,170 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U } } + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 5); // request + 4 retries + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + var stopwatch = Stopwatch.StartNew(); + + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + + stopwatch.Stop(); + + Assert.IsNotNull(ex); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 8); // request + 7 retries + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + /// Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + var stopwatch = Stopwatch.StartNew(); + + MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + + stopwatch.Stop(); + + Assert.IsNotNull(ex); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + + Assert.AreEqual(httpManager.ExecutedRequestCount, 4); // request + 2 retries + } + } + [DataTestMethod] [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] From 57afaf88b65eaf23ca14219680f1ce3a19a408e3 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 17 Apr 2025 14:45:55 -0400 Subject: [PATCH 39/68] Finished all ImdsRetryPolicy unit tests --- global.json | 8 +- .../ManagedIdentityTests/ImdsTests.cs | 258 +++++++++++++++--- 2 files changed, 225 insertions(+), 41 deletions(-) diff --git a/global.json b/global.json index cfbcf3eaec..66e4a5c8a7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "9.0.201", - "rollForward": "latestFeature" - } + "sdk": { + "version": "8.0.404", + "rollForward": "latestFeature" + } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index efae53f033..617e585035 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Diagnostics; using System.Net; using System.Threading.Tasks; @@ -8,11 +9,9 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; -using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; -using OpenTelemetry.Resources; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests @@ -110,7 +109,9 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U // exponential backoff (1 second -> 2 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 3); // request + 2 retries + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -170,7 +171,9 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI // linear backoff (10 seconds * 4 retries) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 5); // request + 4 retries + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -210,20 +213,24 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign userAssignedIdentityId: userAssignedIdentityId); } + MsalServiceException msalException = null; var stopwatch = Stopwatch.StartNew(); - - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); - + .ExecuteAsync().ConfigureAwait(false); + } catch (Exception ex) + { + msalException = ex as MsalServiceException; + } stopwatch.Stop(); - - Assert.IsNotNull(ex); + Assert.IsNotNull(msalException); // linear backoff (10 seconds * 7 retries) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 8); // request + 7 retries + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -248,7 +255,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign var mi = miBuilder.Build(); - /// Simulate permanent 504s (to trigger the maximum number of retries) + // Simulate permanent 504s (to trigger the maximum number of retries) const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) for (int i = 0; i < NUM_504; i++) { @@ -262,58 +269,235 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign userAssignedIdentityId: userAssignedIdentityId); } + MsalServiceException msalException = null; var stopwatch = Stopwatch.StartNew(); - - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); - + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } stopwatch.Stop(); - - Assert.IsNotNull(ex); + Assert.IsNotNull(msalException); // exponential backoff (1 second -> 2 seconds -> 4 seconds) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(httpManager.ExecutedRequestCount, 4); // request + 2 retries + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] - [DataRow(HttpStatusCode.BadRequest, ImdsManagedIdentitySource.IdentityUnavailableError, 1, DisplayName = "BadRequest - Identity Unavailable")] - [DataRow(HttpStatusCode.BadGateway, ImdsManagedIdentitySource.GatewayError, 1, DisplayName = "BadGateway - Gateway Error")] - [DataRow(HttpStatusCode.GatewayTimeout, ImdsManagedIdentitySource.GatewayError, 4, DisplayName = "GatewayTimeout - Gateway Error Retries")] - public async Task ImdsErrorHandlingTestAsync(HttpStatusCode statusCode, string expectedErrorSubstring, int expectedAttempts) + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, "http://169.254.169.254"); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); - // Disabling shared cache options to avoid cross test pollution. + // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; var mi = miBuilder.Build(); - // Adding multiple mock handlers to simulate retries for GatewayTimeout - for (int i = 0; i < expectedAttempts; i++) + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) { - httpManager.AddManagedIdentityMockHandler(ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, statusCode: statusCode); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } - // Expecting a MsalServiceException indicating an error - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.BadRequest, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); - Assert.IsNotNull(ex); - Assert.AreEqual(ManagedIdentitySource.Imds.ToString(), ex.AdditionalExceptionData[MsalException.ManagedIdentitySource]); - Assert.AreEqual(MsalError.ManagedIdentityRequestFailed, ex.ErrorCode); - Assert.IsTrue(ex.Message.Contains(expectedErrorSubstring), $"The error message is not as expected. Error message: {ex.Message}. Expected message should contain: {expectedErrorSubstring}"); + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); } } } From f629dfcf86e444717018892b7cbfb9f26516a0da Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 16:00:32 -0400 Subject: [PATCH 40/68] Discovered bugs through unit tests, and fixed them. Improved ImdsRetryPolicy unit tests. Wrote UAMI unit tests for DefaultRetryPolicy. --- .../Http/DefaultRetryPolicy.cs | 24 +- .../Http/HttpManagerFactory.cs | 11 +- .../Http/ImdsRetryPolicy.cs | 6 +- .../TestConstants.cs | 9 + .../DefaultRetryPolicy.cs | 15 - .../DefaultRetryPolicyTests.cs | 177 +++++ .../ManagedIdentityTests/ImdsTests.cs | 672 +++++++++--------- .../ManagedIdentityTests.cs | 2 +- 8 files changed, 553 insertions(+), 363 deletions(-) delete mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 12abebdc08..79c5c06339 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -9,37 +9,37 @@ namespace Microsoft.Identity.Client.Http { class DefaultRetryPolicy : IRetryPolicy { - // this will be overridden in the unit tests so that they run faster - public static int RETRY_DELAY_MS { get; set; } - - private int _maxRetries; - private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); - private readonly Func _retryCondition; + // constants that are defined in the constructor + public static int DEFAULT_RETRY_DELAY_MS { get; set; } // this will be overridden in the unit tests so that they run faster + private int MAX_RETRIES; + private readonly Func RETRY_CONDITION; public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { - RETRY_DELAY_MS = retryDelayMs; - _maxRetries = maxRetries; - _retryCondition = retryCondition; + DEFAULT_RETRY_DELAY_MS = retryDelayMs; + MAX_RETRIES = maxRetries; + RETRY_CONDITION = retryCondition; } public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { // Check if the status code is retriable and if the current retry count is less than max retries - if (_retryCondition(response, exception) && - retryCount < _maxRetries) + if (RETRY_CONDITION(response, exception) && + retryCount < MAX_RETRIES) { // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); - int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, RETRY_DELAY_MS); + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); + + return true; } // If the status code is not retriable or max retries have been reached, do not retry diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index 9a473a0b54..a1c1527913 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,10 +8,13 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { - private const int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = 1000; - private const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; - private const int DEFAULT_ESTS_RETRY_DELAY_MS = 1000; - private const int DEFAULT_ESTS_MAX_RETRIES = 1; + // referenced in unit tests, cannot be private + public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; + public const int DEFAULT_ESTS_MAX_RETRIES = 1; + + // these will be overridden in the unit tests so that they run faster + public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; + public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 8421df131c..7222284e91 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,10 +10,12 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; // referenced in unit tests - public const int LINEAR_STRATEGY_NUM_RETRIES = 7; // referenced in unit tests private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + // referenced in unit tests, cannot be private + public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; + public const int LINEAR_STRATEGY_NUM_RETRIES = 7; + // these will be overridden in the unit tests so that they run faster public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index 32bfbd3ea1..b142968907 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -205,6 +205,15 @@ public static HashSet s_scope public const string ImdsHost = "169.254.169.254"; public const string ImdsUrl = $"http://{ImdsHost}/metadata/instance/compute/location"; + public const double ONE_HUNDRED_TIMES_FASTER = 0.01; + + public const string AppServiceEndpoint = "http://127.0.0.1:41564/msi/token"; + public const string AzureArcEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; + public const string CloudShellEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; + public const string ImdsEndpoint = $"http://{ImdsHost}/metadata/identity/oauth2/token"; + public const string MachineLearningEndpoint = "http://localhost:7071/msi/token"; + public const string ServiceFabricEndpoint = "https://localhost:2377/metadata/identity/oauth2/token"; + public const string UserAssertion = "fake_access_token"; public const string CodeVerifier = "someCodeVerifier"; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs deleted file mode 100644 index 2cf5c17888..0000000000 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicy.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests -{ - class DefaultRetryPolicy - { - } -} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs new file mode 100644 index 0000000000..e6c219564b --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; + +namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests +{ + /// + /// The Default Retry Policy applies to: + /// ESTS (Azure AD) + /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric + /// + [TestClass] + + public class DefaultRetryPolicyTests : TestBase + { + private static int _originalManagedIdentityRetryDelay; + private static int _originalEstsRetryDelay; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalManagedIdentityRetryDelay = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS; + _originalEstsRetryDelay = HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS; + + // Speed up retry delays by 100x + HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = (int)(_originalEstsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = _originalManagedIdentityRetryDelay; + HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = _originalEstsRetryDelay; + } + + [DataTestMethod] // see test class header: all sources that allow UAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources that allow UAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (1 second * 3 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 617e585035..a7f57917f7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -19,7 +19,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private const double ONE_HUNDRED_TIMES_FASTER = 0.01; private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds @@ -38,10 +37,10 @@ public static void ClassInitialize(TestContext _) _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; // Speed up retry delays by 100x - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] @@ -60,59 +59,61 @@ public static void ClassCleanup() public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404 = 2; - for (int i = 0; i < NUM_404; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + MockHelpers.GetMsiSuccessfulResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.NotFound, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - ManagedIdentitySource.Imds, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // exponential backoff (1 second -> 2 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); + // exponential backoff (1 second -> 2 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - // ensure that exactly 3 requests were made: initial request + 2 retries - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } } } @@ -122,59 +123,61 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate four 410s (to trigger retries), then a successful response - const int NUM_410 = 4; - for (int i = 0; i < NUM_410; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + MockHelpers.GetMsiSuccessfulResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - // Final success - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - ManagedIdentitySource.Imds, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + var stopwatch = Stopwatch.StartNew(); - var stopwatch = Stopwatch.StartNew(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + stopwatch.Stop(); - stopwatch.Stop(); + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - // linear backoff (10 seconds * 4 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(httpManager.QueueSize, 0); - // ensure that exactly 5 requests were made: initial request + 4 retries - Assert.AreEqual(httpManager.QueueSize, 0); - - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } } } @@ -184,53 +187,56 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 410s (to trigger the maximum number of retries) - const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) - for (int i = 0; i < NUM_410; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(httpManager.QueueSize, 0); } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (10 seconds * 7 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * 4 * ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 8 requests were made: initial request + 7 retries - Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -240,54 +246,56 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 504s (to trigger the maximum number of retries) - const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_504; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - msalException = ex as MsalServiceException; + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(httpManager.QueueSize, 0); } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // exponential backoff (1 second -> 2 seconds -> 4 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -297,207 +305,213 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + } + + msalException = null; + stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + Assert.AreEqual(httpManager.QueueSize, 0); } + } + } - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - for (int i = 0; i < NUM_500; i++) + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - msalException = null; - stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // ensure that the second request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); + var mi = miBuilder.Build(); - for (int i = 0; i < NUM_500; i++) - { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, + statusCode: HttpStatusCode.BadRequest, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - } - msalException = null; - stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.BadRequest, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - // ensure that only the initial request was made - Assert.AreEqual(httpManager.QueueSize, 0); - } - } + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + var mi = miBuilder.Build(); - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - // ensure that only the initial request was made - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(httpManager.QueueSize, 0); + } } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 84d399a51d..349c48ceda 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -41,7 +41,7 @@ public class ManagedIdentityTests : TestBase [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] - [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] + [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.Imds)] [DataRow(null, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(AzureArcEndpoint, ManagedIdentitySource.AzureArc, ManagedIdentitySource.AzureArc)] [DataRow(CloudShellEndpoint, ManagedIdentitySource.CloudShell, ManagedIdentitySource.CloudShell)] From 62c0b09535ecf33e59494d9b1ec3b4176ece82c1 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 16:07:57 -0400 Subject: [PATCH 41/68] Minor improvements to ImdsRetryPolicy tests --- .../ManagedIdentityTests/ImdsTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index a7f57917f7..682b8fe469 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -60,7 +60,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -124,7 +124,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -188,7 +188,7 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -247,7 +247,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -306,7 +306,7 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -420,7 +420,7 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { @@ -471,7 +471,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { using (new EnvVariableContext()) { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsHost); + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); using (var httpManager = new MockHttpManager(isManagedIdentity: true)) { From c79f23d92674d6171b9bfc40ffa3a187807eab4c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 18 Apr 2025 18:48:00 -0400 Subject: [PATCH 42/68] Added option retryAfter header to mockHttpManager. Added more DefaulyRetryPolicy tests. --- .../Core/Mocks/MockHttpManagerExtensions.cs | 15 ++- .../DefaultRetryPolicyTests.cs | 121 +++++++++++++++++- 2 files changed, 130 insertions(+), 6 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs index 5a04bedf5b..bde5093fd3 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManagerExtensions.cs @@ -367,12 +367,19 @@ public static void AddManagedIdentityMockHandler( ManagedIdentitySource managedIdentitySourceType, string userAssignedId = null, UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.None, - HttpStatusCode statusCode = HttpStatusCode.OK + HttpStatusCode statusCode = HttpStatusCode.OK, + string retryAfterHeader = null // A number of seconds (e.g., "120"), or an HTTP-date in RFC1123 format (e.g., "Fri, 19 Apr 2025 15:00:00 GMT") ) { - HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode); - HttpContent content = new StringContent(response); - responseMessage.Content = content; + HttpResponseMessage responseMessage = new HttpResponseMessage(statusCode) + { + Content = new StringContent(response) + }; + + if (retryAfterHeader != null) + { + responseMessage.Headers.TryAddWithoutValidation("Retry-After", retryAfterHeader); + } MockHttpMessageHandler httpMessageHandler = BuildMockHandlerForManagedIdentitySource(managedIdentitySourceType, resource); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index e6c219564b..d464c67b0e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -25,9 +25,10 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric /// [TestClass] - public class DefaultRetryPolicyTests : TestBase { + private const int LINEAR_POLICY_MAX_RETRIES_IN_MS = 3000; + private static int _originalManagedIdentityRetryDelay; private static int _originalEstsRetryDelay; @@ -144,7 +145,7 @@ public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIden httpManager.AddManagedIdentityMockHandler( endpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), + "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError, userAssignedId: userAssignedId, @@ -173,5 +174,121 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } } } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // max retry time (3 seconds), but make it one hundred times faster so the test completes quickly + double retryAfterSeconds = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER; + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterSeconds.ToString()); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (LINEAR_POLICY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } } } From 25ce41b528cd78c636af90e883a1e9e9baddc4fa Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 21 Apr 2025 17:56:26 -0400 Subject: [PATCH 43/68] Finished DefaultRetryPolicy unit tests. Edited some ImdsRetryPolicy unit tests. Fixed some bugs discovered by unit tests. --- .../Http/DefaultRetryPolicy.cs | 8 +- .../Http/ImdsRetryPolicy.cs | 6 + .../Http/LinearRetryStrategy.cs | 4 +- .../Core/Mocks/MockHttpManager.cs | 1 - .../DefaultRetryPolicyTests.cs | 335 +++++++++++++++++- .../ManagedIdentityTests/ImdsTests.cs | 27 +- 6 files changed, 356 insertions(+), 25 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 79c5c06339..cb59f160bd 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -16,6 +16,9 @@ class DefaultRetryPolicy : IRetryPolicy private int MAX_RETRIES; private readonly Func RETRY_CONDITION; + // referenced in the unit tests + public static int numRetries { get; private set; } = 0; + public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { DEFAULT_RETRY_DELAY_MS = retryDelayMs; @@ -29,12 +32,15 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (RETRY_CONDITION(response, exception) && retryCount < MAX_RETRIES) { + // used below in the log statement, also referenced in the unit tests + numRetries = retryCount + 1; + // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); - logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 7222284e91..82cd122fea 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -15,6 +15,7 @@ internal class ImdsRetryPolicy : IRetryPolicy // referenced in unit tests, cannot be private public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; public const int LINEAR_STRATEGY_NUM_RETRIES = 7; + public static int numRetries { get; private set; } = 0; // these will be overridden in the unit tests so that they run faster public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; @@ -24,6 +25,8 @@ internal class ImdsRetryPolicy : IRetryPolicy private int _maxRetries; + + private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, @@ -46,6 +49,9 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (HttpRetryConditions.Imds(response, exception) && retryCount < _maxRetries) { + // used below in the log statement, also referenced in the unit tests + numRetries = retryCount + 1; + int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone ? HTTP_STATUS_GONE_RETRY_AFTER_MS : _exponentialRetryStrategy.CalculateDelay(retryCount); diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs index cecb38840f..bc2edb8bbb 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs @@ -30,7 +30,9 @@ public int calculateDelay(string retryHeader, int minimumDelay) // If parsing as seconds fails, try parsing as an HTTP date if (DateTime.TryParse(retryHeader, out DateTime retryDate)) { - int millisToSleep = (int)(retryDate - DateTime.UtcNow).TotalMilliseconds; + DateTime.TryParse(DateTime.UtcNow.ToString("R"), out DateTime nowDate); + + int millisToSleep = (int)(retryDate - nowDate).TotalMilliseconds; return Math.Max(minimumDelay, millisToSleep); } diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 232a152869..9512304848 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -16,7 +16,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.Internal; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Common.Core.Mocks diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index d464c67b0e..b9ad25105e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -2,11 +2,8 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net; -using System.Text; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; @@ -27,8 +24,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class DefaultRetryPolicyTests : TestBase { - private const int LINEAR_POLICY_MAX_RETRIES_IN_MS = 3000; - private static int _originalManagedIdentityRetryDelay; private static int _originalEstsRetryDelay; @@ -107,6 +102,7 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -170,6 +166,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -224,6 +221,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -253,8 +251,8 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy var mi = miBuilder.Build(); - // max retry time (3 seconds), but make it one hundred times faster so the test completes quickly - double retryAfterSeconds = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER; + // make it one hundred times faster so the test completes quickly + double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( @@ -280,15 +278,334 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (LINEAR_POLICY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // this test can not be made one hundred times faster because it is based on a date + const int retryAfterMilliseconds = 3000; + var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); + + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterHttpDate); + + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); + + var stopwatch = Stopwatch.StartNew(); + + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); + + stopwatch.Stop(); + + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); + + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the second request was made and retried 3 times + // (numRetries would be x2 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + + for (int i = 0; i < NUM_500; i++) + { + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + } + + msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that the third request was made and retried 3 times + // (numRetries would be x3 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.BadRequest); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } + + [DataTestMethod] // see test class header: all sources allow SAMI + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] + public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + { + using (new EnvVariableContext()) + { + SetEnvironmentVariables(managedIdentitySource, endpoint); + + using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + { + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } + } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 682b8fe469..3cfab68045 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -110,6 +110,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_404); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -174,6 +175,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_410); Assert.AreEqual(httpManager.QueueSize, 0); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); @@ -235,6 +237,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -294,6 +297,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -336,7 +340,6 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync } MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -346,10 +349,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // ensure that the first request was made and retried 3 times + // ensure that the third request was made and retried 3 times + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NUM_500; i++) @@ -365,7 +368,6 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } msalException = null; - stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -375,10 +377,11 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // ensure that the second request was made and retried 3 times + // ensure that the third request was made and retried 3 times + // (numRetries would be x2 if retry policy was NOT per request) + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NUM_500; i++) @@ -394,7 +397,6 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } msalException = null; - stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -404,10 +406,11 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that the third request was made and retried 3 times + // (numRetries would be x3 if retry policy was NOT per request) + Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -445,7 +448,6 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy userAssignedIdentityId: userAssignedIdentityId); MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -455,10 +457,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -473,7 +475,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -496,7 +498,6 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin userAssignedIdentityId: userAssignedIdentityId); MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) @@ -506,10 +507,10 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); Assert.AreEqual(httpManager.QueueSize, 0); } } From b33059cd6bd90828c63257946ff55d115480ceb2 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 16:08:44 -0400 Subject: [PATCH 44/68] rebased branch from main to rginsburg_retry_policy_per_request --- .../Http/DefaultRetryPolicy.cs | 11 +++-- .../Http/ImdsRetryPolicy.cs | 2 - .../Instance/Region/RegionManager.cs | 12 ++--- .../Validation/AdfsAuthorityValidator.cs | 8 ++-- .../AzureArcManagedIdentitySource.cs | 8 ++-- .../ManagedIdentity/ManagedIdentityRequest.cs | 13 ++---- .../OAuth2/OAuth2Client.cs | 13 +++--- .../WsTrust/WsTrustWebRequestManager.cs | 12 ++--- .../CoreTests/HttpTests/HttpManagerTests.cs | 45 +++++++++---------- .../DefaultRetryPolicyTests.cs | 18 ++++---- .../ManagedIdentityTests/ImdsTests.cs | 14 +++--- .../ManagedIdentityTests.cs | 8 ++-- 12 files changed, 81 insertions(+), 83 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index cb59f160bd..b30fc0df80 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -11,8 +11,13 @@ class DefaultRetryPolicy : IRetryPolicy { private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); + public const int DefaultStsMaxRetries = 3; + public const int DefaultStsRetryDelayMs = 1000; + public const int DefaultManagedIdentityMaxRetries = 3; + public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster + // constants that are defined in the constructor - public static int DEFAULT_RETRY_DELAY_MS { get; set; } // this will be overridden in the unit tests so that they run faster + public static int DefaultRetryDelayMs { get; set; } // this will be overridden in the unit tests so that they run faster private int MAX_RETRIES; private readonly Func RETRY_CONDITION; @@ -21,7 +26,7 @@ class DefaultRetryPolicy : IRetryPolicy public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) { - DEFAULT_RETRY_DELAY_MS = retryDelayMs; + DefaultRetryDelayMs = retryDelayMs; MAX_RETRIES = maxRetries; RETRY_CONDITION = retryCondition; } @@ -38,7 +43,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce // Use HeadersAsDictionary to check for "Retry-After" header response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); - int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DEFAULT_RETRY_DELAY_MS); + int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DefaultRetryDelayMs); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 82cd122fea..1ee4753e88 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -25,8 +25,6 @@ internal class ImdsRetryPolicy : IRetryPolicy private int _maxRetries; - - private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index b2b72a4a45..52f6521a94 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -46,9 +46,9 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static bool s_failedAutoDiscovery = false; private static string s_regionDiscoveryDetails; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public RegionManager( @@ -213,7 +213,7 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // A bad request occurs when the version in the IMDS call is no longer supported. @@ -231,7 +231,7 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // Call again with updated version } @@ -334,7 +334,7 @@ private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dict mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(userCancellationToken), - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); // When IMDS endpoint is called without the api version query param, bad request response comes back with latest version. diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 471f79ef38..ac44304c5b 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -29,9 +29,9 @@ public async Task ValidateAuthorityAsync( var resource = $"https://{authorityInfo.Host}"; string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); - LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( @@ -44,7 +44,7 @@ public async Task ValidateAuthorityAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: _requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy + retryPolicy: defaultRetryPolicy ) .ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index a643ce7880..cf95fc9a0a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -123,9 +123,9 @@ protected override async Task HandleResponseAsync( _requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request."); request.Headers.Add("Authorization", authHeaderValue); - LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, HttpRetryConditions.Sts); response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( @@ -138,7 +138,7 @@ protected override async Task HandleResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: cancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 3541730c7b..0218beeb4a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -11,11 +11,6 @@ namespace Microsoft.Identity.Client.ManagedIdentity { internal class ManagedIdentityRequest { - // referenced in unit tests, cannot be private - public const int DefaultManagedIdentityMaxRetries = 3; - // this will be overridden in the unit tests so that they run faster - public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; - private readonly Uri _baseEndpoint; public HttpMethod Method { get; } @@ -36,10 +31,10 @@ public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retr BodyParameters = new Dictionary(); QueryParameters = new Dictionary(); - IRetryPolicy defaultRetryPolicy = new LinearRetryPolicy( - DefaultManagedIdentityRetryDelayMs, - DefaultManagedIdentityMaxRetries, - HttpRetryConditions.ManagedIdentity); + IRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, + HttpRetryConditions.DefaultManagedIdentity); RetryPolicy = retryPolicy ?? defaultRetryPolicy; } diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 462ee6c4a3..0eb1e08948 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -41,10 +41,6 @@ internal class OAuth2Client private readonly IDictionary _bodyParameters = new Dictionary(); private readonly IHttpManager _httpManager; private readonly X509Certificate2 _mtlsCertificate; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); public OAuth2Client(ILoggerAdapter logger, IHttpManager httpManager, X509Certificate2 mtlsCertificate) { @@ -123,6 +119,11 @@ internal async Task ExecuteRequestAsync( using (requestContext.Logger.LogBlockDuration($"[Oauth2Client] Sending {method} request ")) { + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, + HttpRetryConditions.Sts); + try { if (method == HttpMethod.Post) @@ -145,7 +146,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: _mtlsCertificate, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); } else @@ -160,7 +161,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); } } diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index fb69357d42..4a026cdeef 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -21,9 +21,9 @@ namespace Microsoft.Identity.Client.WsTrust internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; - private readonly LinearRetryPolicy _linearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); public WsTrustWebRequestManager(IHttpManager httpManager) @@ -57,7 +57,7 @@ public async Task GetMexDocumentAsync(string federationMetadataUrl, mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK) @@ -115,7 +115,7 @@ public async Task GetWsTrustResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (resp.StatusCode != System.Net.HttpStatusCode.OK) @@ -192,7 +192,7 @@ public async Task GetUserRealmAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _linearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index b9c3428145..be96aab846 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -12,7 +12,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; @@ -24,9 +23,9 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { - LinearRetryPolicy _stsLinearRetryPolicy = new LinearRetryPolicy( - LinearRetryPolicy.DefaultStsRetryDelayMs, - LinearRetryPolicy.DefaultStsMaxRetries, + DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, HttpRetryConditions.Sts); [TestInitialize] @@ -67,7 +66,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -107,7 +106,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); } } @@ -144,7 +143,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -172,7 +171,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -214,7 +213,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -246,7 +245,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -282,7 +281,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); } } @@ -305,7 +304,7 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -331,7 +330,7 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); @@ -362,7 +361,7 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); @@ -388,7 +387,7 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy) + retryPolicy: _defaultRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); @@ -414,7 +413,7 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); @@ -440,7 +439,7 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); @@ -467,7 +466,7 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _stsLinearRetryPolicy)) + retryPolicy: _defaultRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); @@ -498,10 +497,10 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna } } - LinearRetryPolicy retryPolicy = isManagedIdentity ? new LinearRetryPolicy( - ManagedIdentityRequest.DefaultManagedIdentityRetryDelayMs, - ManagedIdentityRequest.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.ManagedIdentity) : _stsLinearRetryPolicy; + DefaultRetryPolicy defaultRetryPolicy = isManagedIdentity ? new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, + HttpRetryConditions.DefaultManagedIdentity) : _defaultRetryPolicy; var msalHttpResponse = await httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), @@ -513,7 +512,7 @@ public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInterna mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: retryPolicy) + retryPolicy: defaultRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(msalHttpResponse); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index b9ad25105e..430459ea06 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -57,7 +57,7 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { string userAssignedId = TestConstants.ClientId; UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; @@ -120,7 +120,7 @@ public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIden { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { string userAssignedId = TestConstants.ClientId; UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; @@ -184,7 +184,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -241,7 +241,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -302,7 +302,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -364,7 +364,7 @@ public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentityS { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -417,7 +417,7 @@ public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -524,7 +524,7 @@ public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); @@ -572,7 +572,7 @@ public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(Manag { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 3cfab68045..b6d4a15af0 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -62,7 +62,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, U { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -127,7 +127,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedI { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -192,7 +192,7 @@ public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssign { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -252,7 +252,7 @@ public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssign { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -312,7 +312,7 @@ public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -425,7 +425,7 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(isManagedIdentity: true)) + using (var httpManager = new MockHttpManager()) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned @@ -475,7 +475,7 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(strin { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager(retry: false, isManagedIdentity: true)) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 349c48ceda..6ab73bedf6 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -1269,7 +1269,7 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( var mi = miBuilder.Build(); // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = ManagedIdentityRequest.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + const int NumErrors = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) for (int i = 0; i < NumErrors; i++) { httpManager.AddManagedIdentityMockHandler( @@ -1286,7 +1286,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 4 total: request + 3 retries - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1306,7 +1306,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1326,7 +1326,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) // 4 total: request + 3 retries // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(LinearRetryPolicy.numRetries, 1 + ManagedIdentityRequest.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From 248779966b6e3fc29cfabb68e9cc757283b294ae Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 1 May 2025 21:54:16 -0400 Subject: [PATCH 45/68] Fixed some broken unit tests --- .../Http/DefaultRetryPolicy.cs | 24 ++++++------ .../ManagedIdentityTests.cs | 37 ++++++++++++++----- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index b30fc0df80..79678b047c 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -16,26 +16,28 @@ class DefaultRetryPolicy : IRetryPolicy public const int DefaultManagedIdentityMaxRetries = 3; public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster - // constants that are defined in the constructor - public static int DefaultRetryDelayMs { get; set; } // this will be overridden in the unit tests so that they run faster - private int MAX_RETRIES; - private readonly Func RETRY_CONDITION; - - // referenced in the unit tests + // used for comparison, in the unit tests public static int numRetries { get; private set; } = 0; - public DefaultRetryPolicy(int retryDelayMs, int maxRetries, Func retryCondition) + public static int DefaultRetryDelayMs;// { get; set; } // this will be overridden in the unit tests so that they run faster + private int MaxRetries; + private readonly Func RetryCondition; + + public DefaultRetryPolicy( + int retryDelayMs, + int maxRetries, + Func retryCondition) { DefaultRetryDelayMs = retryDelayMs; - MAX_RETRIES = maxRetries; - RETRY_CONDITION = retryCondition; + MaxRetries = maxRetries; + RetryCondition = retryCondition; } public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { // Check if the status code is retriable and if the current retry count is less than max retries - if (RETRY_CONDITION(response, exception) && - retryCount < MAX_RETRIES) + if (RetryCondition(response, exception) && + retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests numRetries = retryCount + 1; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 6ab73bedf6..36a819d23e 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -38,10 +38,29 @@ public class ManagedIdentityTests : TestBase internal const string ExpectedErrorCode = "ErrorCode"; internal const string ExpectedCorrelationId = "Some GUID"; + private static int _originalDefaultManagedIdentityRetryDelayMs; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalDefaultManagedIdentityRetryDelayMs = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; + + // Speed up retry delays by 100x + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalDefaultManagedIdentityRetryDelayMs * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry delay values after all tests + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalDefaultManagedIdentityRetryDelayMs; + } + [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] - [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.Imds)] + [DataRow(ImdsEndpoint, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(null, ManagedIdentitySource.Imds, ManagedIdentitySource.DefaultToImds)] [DataRow(AzureArcEndpoint, ManagedIdentitySource.AzureArc, ManagedIdentitySource.AzureArc)] [DataRow(CloudShellEndpoint, ManagedIdentitySource.CloudShell, ManagedIdentitySource.CloudShell)] @@ -1241,7 +1260,7 @@ public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() [DataTestMethod] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.NotFound)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.RequestTimeout)] - [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, 429)] + [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, 429)] // not defined in HttpStatusCode enum [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.InternalServerError)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.ServiceUnavailable)] [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.GatewayTimeout)] @@ -1285,8 +1304,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1304,9 +1323,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries (DefaultRetryPolicy.numRetries would be 6 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1324,9 +1342,8 @@ await mi.AcquireTokenForManagedIdentity(Resource) .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); Assert.IsNotNull(ex); - // 4 total: request + 3 retries - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1 + DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + // 3 retries (DefaultRetryPolicy.numRetries would be 9 if retry policy was NOT per request) + Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From eb785d6221537a5160fabe7c3418fa7bee43d1f5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 16:15:04 -0400 Subject: [PATCH 46/68] Fixed all broken unit tests and made STS tests 100 times faster --- .../Http/DefaultRetryPolicy.cs | 24 +- .../Http/HttpManagerFactory.cs | 8 - .../Http/ImdsRetryPolicy.cs | 43 +- .../ImdsManagedIdentitySource.cs | 2 + .../CoreTests/HttpTests/HttpManagerTests.cs | 172 +++-- .../DefaultRetryPolicyTests.cs | 707 ++++++++---------- .../ManagedIdentityTests/ImdsTests.cs | 647 +++++++--------- .../ManagedIdentityTests.cs | 13 +- 8 files changed, 720 insertions(+), 896 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 79678b047c..3f5b5dd21a 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -11,15 +11,19 @@ class DefaultRetryPolicy : IRetryPolicy { private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); - public const int DefaultStsMaxRetries = 3; - public const int DefaultStsRetryDelayMs = 1000; + // referenced in unit tests + public const int DefaultStsMaxRetries = 1; public const int DefaultManagedIdentityMaxRetries = 3; - public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // this will be overridden in the unit tests so that they run faster + + // overridden in the unit tests so that they run faster + public static int DefaultStsRetryDelayMs { get; set; } = 1000; + public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; // used for comparison, in the unit tests - public static int numRetries { get; private set; } = 0; + // will be reset after every test + public static int NumRetries { get; set; } = 0; - public static int DefaultRetryDelayMs;// { get; set; } // this will be overridden in the unit tests so that they run faster + public static int DefaultRetryDelayMs; private int MaxRetries; private readonly Func RetryCondition; @@ -40,14 +44,18 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests - numRetries = retryCount + 1; + NumRetries = retryCount + 1; // Use HeadersAsDictionary to check for "Retry-After" header - response.HeadersAsDictionary.TryGetValue("Retry-After", out string retryAfter); + string retryAfter = string.Empty; + if (response?.HeadersAsDictionary != null) + { + response.HeadersAsDictionary.TryGetValue("Retry-After", out retryAfter); + } int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DefaultRetryDelayMs); - logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {numRetries})"); + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {NumRetries})"); // Pause execution for the calculated delay await Task.Delay(retryAfterDelay).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs index a1c1527913..120735fbeb 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManagerFactory.cs @@ -8,14 +8,6 @@ namespace Microsoft.Identity.Client.Http /// internal sealed class HttpManagerFactory { - // referenced in unit tests, cannot be private - public const int DEFAULT_MANAGED_IDENTITY_MAX_RETRIES = 3; - public const int DEFAULT_ESTS_MAX_RETRIES = 1; - - // these will be overridden in the unit tests so that they run faster - public static int DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS { get; set; } = 1000; - public static int DEFAULT_ESTS_RETRY_DELAY_MS { get; set; } = 1000; - public static IHttpManager GetHttpManager( IMsalHttpClientFactory httpClientFactory, bool disableInternalRetries = false) diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs index 1ee4753e88..d9f4ece919 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs @@ -10,25 +10,28 @@ namespace Microsoft.Identity.Client.Http { internal class ImdsRetryPolicy : IRetryPolicy { - private const int HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL = 10 * 1000; // 10 seconds + private const int HttpStatusGoneRetryAfterMsInternal = 10 * 1000; // 10 seconds - // referenced in unit tests, cannot be private - public const int EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; - public const int LINEAR_STRATEGY_NUM_RETRIES = 7; - public static int numRetries { get; private set; } = 0; + // referenced in unit tests + public const int ExponentialStrategyNumRetries = 3; + public const int LinearStrategyNumRetries = 7; - // these will be overridden in the unit tests so that they run faster - public static int MIN_EXPONENTIAL_BACKOFF_MS { get; set; } = 1000; - public static int MAX_EXPONENTIAL_BACKOFF_MS { get; set; } = 4000; - public static int EXPONENTIAL_DELTA_BACKOFF_MS { get; set; } = 2000; - public static int HTTP_STATUS_GONE_RETRY_AFTER_MS { get; set; } = HTTP_STATUS_GONE_RETRY_AFTER_MS_INTERNAL; + // used for comparison, in the unit tests + // will be reset after every test + public static int NumRetries { get; set; } = 0; - private int _maxRetries; + // overridden in the unit tests so that they run faster + public static int MinExponentialBackoffMs { get; set; } = 1000; + public static int MaxExponentialBackoffMs { get; set; } = 4000; + public static int ExponentialDeltaBackoffMs { get; set; } = 2000; + public static int HttpStatusGoneRetryAfterMs { get; set; } = HttpStatusGoneRetryAfterMsInternal; + + private int MaxRetries; private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS + ImdsRetryPolicy.MinExponentialBackoffMs, + ImdsRetryPolicy.MaxExponentialBackoffMs, + ImdsRetryPolicy.ExponentialDeltaBackoffMs ); public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) @@ -38,20 +41,20 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (retryCount == 0) { // Calculate the maxRetries based on the status code, once per request - _maxRetries = httpStatusCode == (int)HttpStatusCode.Gone - ? LINEAR_STRATEGY_NUM_RETRIES - : EXPONENTIAL_STRATEGY_NUM_RETRIES; + MaxRetries = httpStatusCode == (int)HttpStatusCode.Gone + ? LinearStrategyNumRetries + : ExponentialStrategyNumRetries; } // Check if the status code is retriable and if the current retry count is less than max retries if (HttpRetryConditions.Imds(response, exception) && - retryCount < _maxRetries) + retryCount < MaxRetries) { // used below in the log statement, also referenced in the unit tests - numRetries = retryCount + 1; + NumRetries = retryCount + 1; int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone - ? HTTP_STATUS_GONE_RETRY_AFTER_MS + ? HttpStatusGoneRetryAfterMs : _exponentialRetryStrategy.CalculateDelay(retryCount); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 6cfb8854e6..59daa01a41 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -82,6 +82,8 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } + request.RetryPolicy = new ImdsRetryPolicy(); + return request; } diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index be96aab846..bee5fb4dd9 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -23,15 +23,37 @@ namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests [TestClass] public class HttpManagerTests { - DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + private static int _originalStsRetryDelay; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + // Backup original retry delay values + _originalStsRetryDelay = DefaultRetryPolicy.DefaultStsRetryDelayMs; + + // Speed up retry delays by 100x + DefaultRetryPolicy.DefaultStsRetryDelayMs = (int)(_originalStsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + } + + [ClassCleanup] + public static void ClassCleanup() + { + // Restore retry policy values after each test + DefaultRetryPolicy.DefaultStsRetryDelayMs = _originalStsRetryDelay; + } + + private DefaultRetryPolicy StsRetryPolicy; [TestInitialize] public void TestInitialize() { TestCommon.ResetInternalStaticCaches(); + + DefaultRetryPolicy.NumRetries = 0; + StsRetryPolicy = new DefaultRetryPolicy( + DefaultRetryPolicy.DefaultStsRetryDelayMs, + DefaultRetryPolicy.DefaultStsMaxRetries, + HttpRetryConditions.Sts); } [TestMethod] @@ -66,7 +88,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -106,7 +128,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); } } @@ -143,7 +165,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -171,7 +193,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -213,7 +235,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -245,7 +267,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -281,13 +303,13 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); } } [TestMethod] - public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() + public async Task TestSendGetWithHttp500TypeFailureWithInternalRetriesDisabledAsync() { using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { @@ -304,10 +326,11 @@ public async Task TestSendGetWithRetryFalseHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); } } @@ -316,8 +339,12 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.InternalServerError); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); + } var ex = await Assert.ThrowsExceptionAsync(() => httpManager.SendRequestAsync( @@ -330,10 +357,12 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -361,11 +390,38 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + } + } + + [TestMethod] + public async Task NoResiliencyIfHttpErrorNotRetriableAsync() + { + using (var httpManager = new MockHttpManager()) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadRequest); + + var msalHttpResponse = await httpManager.SendRequestAsync( + new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), + headers: null, + body: new StringContent("body"), + method: HttpMethod.Get, + logger: Substitute.For(), + doNotThrow: true, + mtlsCertificate: null, + validateServerCert: null, + cancellationToken: default, + retryPolicy: StsRetryPolicy) + .ConfigureAwait(false); + + Assert.AreEqual(HttpStatusCode.BadRequest, msalHttpResponse.StatusCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); } } @@ -374,23 +430,29 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.BadGateway); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.BadGateway); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadGateway); + } var msalHttpResponse = await httpManager.SendRequestAsync( new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), headers: null, body: new StringContent("body"), - method: HttpMethod.Post, + method: HttpMethod.Get, logger: Substitute.For(), doNotThrow: true, mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy) + retryPolicy: StsRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -399,8 +461,12 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.GatewayTimeout); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); + } var exc = await AssertException.TaskThrowsAsync(() => httpManager.SendRequestAsync( @@ -413,10 +479,12 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); + Assert.AreEqual(httpManager.QueueSize, 0); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); } } @@ -439,11 +507,12 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); } } @@ -466,60 +535,11 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: _defaultRetryPolicy)) + retryPolicy: StsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); - } - } - - [TestMethod] - [DataRow(true, false)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(false, true)] - public async Task TestRetryConfigWithHttp500TypeFailureAsync(bool disableInternalRetries, bool isManagedIdentity) - { - using (var httpManager = new MockHttpManager(disableInternalRetries: disableInternalRetries)) - { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - - if (!disableInternalRetries) - { - //Adding second response for retry - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - - // Add 2 more response for the managed identity flow since 3 retries happen in this scenario - if (isManagedIdentity) - { - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); - } - } - - DefaultRetryPolicy defaultRetryPolicy = isManagedIdentity ? new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, - DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.DefaultManagedIdentity) : _defaultRetryPolicy; - - var msalHttpResponse = await httpManager.SendRequestAsync( - new Uri(TestConstants.AuthorityHomeTenant + "oauth2/token"), - headers: null, - body: new StringContent("body"), - method: HttpMethod.Post, - logger: Substitute.For(), - doNotThrow: true, - mtlsCertificate: null, - validateServerCert: null, - cancellationToken: default, - retryPolicy: defaultRetryPolicy) - .ConfigureAwait(false); - - Assert.IsNotNull(msalHttpResponse); - Assert.AreEqual(HttpStatusCode.ServiceUnavailable, msalHttpResponse.StatusCode); - //If a second request is sent when retry is configured to false, the test will fail since - //the MockHttpManager will not be able to serve another response. - //The MockHttpManager will also check for unused responses which will check if the retry did not occur when it should have. + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index 430459ea06..f8d05c4c91 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -18,95 +18,99 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests { /// /// The Default Retry Policy applies to: - /// ESTS (Azure AD) + /// STS (Azure AD) (Tested in HttpManagerTests.cs) /// Managed Identity Sources: App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric /// [TestClass] public class DefaultRetryPolicyTests : TestBase { private static int _originalManagedIdentityRetryDelay; - private static int _originalEstsRetryDelay; [ClassInitialize] public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalManagedIdentityRetryDelay = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS; - _originalEstsRetryDelay = HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS; + _originalManagedIdentityRetryDelay = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; // Speed up retry delays by 100x - HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); - HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = (int)(_originalEstsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS = _originalManagedIdentityRetryDelay; - HttpManagerFactory.DEFAULT_ESTS_RETRY_DELAY_MS = _originalEstsRetryDelay; + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalManagedIdentityRetryDelay; + } + + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + + DefaultRetryPolicy.NumRetries = 0; } [DataTestMethod] // see test class header: all sources that allow UAMI [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task UAMIFails500OnceThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) - { - string userAssignedId = TestConstants.ClientId; - UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -114,61 +118,61 @@ public async Task UAMIFails500OnceThenSucceeds200Async(ManagedIdentitySource man [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task UAMIFails500PermanentlyAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task UAMIFails500PermanentlyAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + string userAssignedId = TestConstants.ClientId; + UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - using (var httpManager = new MockHttpManager()) + ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + const int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) { - string userAssignedId = TestConstants.ClientId; - UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; - - ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (1 second * 3 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } + + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (1 second * 3 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -178,54 +182,54 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); - - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // linear backoff (1 second * 1 retry) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS); + // linear backoff (1 second * 1 retry) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -235,58 +239,58 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async(Man [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); + // make it one hundred times faster so the test completes quickly + double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; - // make it one hundred times faster so the test completes quickly - double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterSeconds.ToString()); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterSeconds.ToString()); - - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -296,59 +300,59 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); + + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + var mi = miBuilder.Build(); - var mi = miBuilder.Build(); + // this test can not be made one hundred times faster because it is based on a date + const int retryAfterMilliseconds = 3000; + var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); - // this test can not be made one hundred times faster because it is based on a date - const int retryAfterMilliseconds = 3000; - var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); + // Initial request fails with 500 + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError, + retryAfterHeader: retryAfterHttpDate); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterHttpDate); + // Final success + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + managedIdentitySource); - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); - - var stopwatch = Stopwatch.StartNew(); + var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - stopwatch.Stop(); + stopwatch.Stop(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.numRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); + // ensure that exactly 2 requests were made: initial request + 1 retry + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } @@ -358,157 +362,50 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500Permanently(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500Permanently( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate permanent 500s (to trigger the maximum number of retries) + int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_500; i++) { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); } - } - } - - [DataTestMethod] // see test class header: all sources allow SAMI - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(ManagedIdentitySource managedIdentitySource, string endpoint) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the first request was made and retried 3 times - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the second request was made and retried 3 times - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, HttpManagerFactory.DEFAULT_MANAGED_IDENTITY_MAX_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + msalException = ex as MsalServiceException; } + Assert.IsNotNull(msalException); + + // ensure that the first request was made and retried 3 times + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -518,45 +415,45 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager()) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.BadRequest); - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.BadRequest); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } @@ -566,45 +463,45 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(ManagedIdentitySource managedIdentitySource, string endpoint) + public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( + ManagedIdentitySource managedIdentitySource, + string endpoint) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) { SetEnvironmentVariables(managedIdentitySource, endpoint); - using (var httpManager = new MockHttpManager(disableInternalRetries: true)) - { - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager); - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + httpManager.AddManagedIdentityMockHandler( + endpoint, + ManagedIdentityTests.Resource, + "", + managedIdentitySource, + statusCode: HttpStatusCode.InternalServerError); - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index b6d4a15af0..17d575a5d7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; +using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -19,9 +20,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private const int IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds - private const int IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds - private static int _originalMinBackoff; private static int _originalMaxBackoff; private static int _originalDeltaBackoff; @@ -31,488 +29,385 @@ public class ImdsTests : TestBase public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalMinBackoff = ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS; - _originalMaxBackoff = ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS; - _originalDeltaBackoff = ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS; - _originalGoneRetryAfter = ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS; + _originalMinBackoff = ImdsRetryPolicy.MinExponentialBackoffMs; + _originalMaxBackoff = ImdsRetryPolicy.MaxExponentialBackoffMs; + _originalDeltaBackoff = ImdsRetryPolicy.ExponentialDeltaBackoffMs; + _originalGoneRetryAfter = ImdsRetryPolicy.HttpStatusGoneRetryAfterMs; // Speed up retry delays by 100x - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MinExponentialBackoffMs = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MaxExponentialBackoffMs = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.ExponentialDeltaBackoffMs = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS = _originalMinBackoff; - ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS = _originalMaxBackoff; - ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS = _originalDeltaBackoff; - ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS = _originalGoneRetryAfter; + ImdsRetryPolicy.MinExponentialBackoffMs = _originalMinBackoff; + ImdsRetryPolicy.MaxExponentialBackoffMs = _originalMaxBackoff; + ImdsRetryPolicy.ExponentialDeltaBackoffMs = _originalDeltaBackoff; + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = _originalGoneRetryAfter; + } + + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + + ImdsRetryPolicy.NumRetries = 0; } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails404TwiceThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails404TwiceThenSucceeds200Async( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - using (var httpManager = new MockHttpManager()) + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + // Simulate two 404s (to trigger retries), then a successful response + const int NUM_404 = 2; + for (int i = 0; i < NUM_404; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404 = 2; - for (int i = 0; i < NUM_404; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.NotFound, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), + MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.NotFound, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - var stopwatch = Stopwatch.StartNew(); + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); - stopwatch.Stop(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - // exponential backoff (1 second -> 2 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + stopwatch.Stop(); - // ensure that exactly 3 requests were made: initial request + 2 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_404); - Assert.AreEqual(httpManager.QueueSize, 0); + // exponential backoff (1 second -> 2 seconds) + const int ImdsExponentialStrategyTwoRetriesInMs = 3000; + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyTwoRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + // ensure that exactly 3 requests were made: initial request + 2 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_404); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails410FourTimesThenSucceeds200Async(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails410FourTimesThenSucceeds200Async( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); - using (var httpManager = new MockHttpManager()) + // Simulate four 410s (to trigger retries), then a successful response + const int NUM_410 = 4; + for (int i = 0; i < NUM_410; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate four 410s (to trigger retries), then a successful response - const int NUM_410 = 4; - for (int i = 0; i < NUM_410; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - // Final success httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), + MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - var stopwatch = Stopwatch.StartNew(); + // Final success + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.Imds, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); - stopwatch.Stop(); + var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); - // linear backoff (10 seconds * 4 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + stopwatch.Stop(); - // ensure that exactly 5 requests were made: initial request + 4 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, NUM_410); - Assert.AreEqual(httpManager.QueueSize, 0); + // linear backoff (10 seconds * 4 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } + // ensure that exactly 5 requests were made: initial request + 4 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_410); + Assert.AreEqual(httpManager.QueueSize, 0); + + Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails410PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails410PermanentlyAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 410s (to trigger the maximum number of retries) - const int NUM_410 = ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (7) - for (int i = 0; i < NUM_410; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.Gone, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // linear backoff (10 seconds * 7 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 8 requests were made: initial request + 7 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.LINEAR_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - } - } - } + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails504PermanentlyAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + var mi = miBuilder.Build(); - using (var httpManager = new MockHttpManager()) + // Simulate permanent 410s (to trigger the maximum number of retries) + const int NUM_410 = ImdsRetryPolicy.LinearStrategyNumRetries + 1; // initial request + maximum number of retries (7) + for (int i = 0; i < NUM_410; i++) { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 504s (to trigger the maximum number of retries) - const int NUM_504 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_504; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - stopwatch.Stop(); - Assert.IsNotNull(msalException); - - // exponential backoff (1 second -> 2 seconds -> 4 seconds) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.Gone, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); } - } - } - [DataTestMethod] - [DataRow(null, null)] // SAMI - [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500PermanentlyAndRetryPolicyLifeTimeIsPerRequestAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) - { - using (new EnvVariableContext()) - { - SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - - using (var httpManager = new MockHttpManager()) + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); - - // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x2 if retry policy was NOT per request) - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); - - for (int i = 0; i < NUM_500; i++) - { - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.GatewayTimeout, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); - } - - msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that the third request was made and retried 3 times - // (numRetries would be x3 if retry policy was NOT per request) - Assert.AreEqual(ImdsRetryPolicy.numRetries, ImdsRetryPolicy.EXPONENTIAL_STRATEGY_NUM_RETRIES); - Assert.AreEqual(httpManager.QueueSize, 0); + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // linear backoff (10 seconds * 7 retries) + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * ImdsRetryPolicy.LinearStrategyNumRetries * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 8 requests were made: initial request + 7 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.LinearStrategyNumRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails504PermanentlyAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); - using (var httpManager = new MockHttpManager()) - { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + var mi = miBuilder.Build(); + // Simulate permanent 504s (to trigger the maximum number of retries) + const int NUM_504 = ImdsRetryPolicy.ExponentialStrategyNumRetries + 1; // initial request + maximum number of retries (3) + for (int i = 0; i < NUM_504; i++) + { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, ManagedIdentityTests.Resource, MockHelpers.GetMsiImdsErrorResponse(), ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.BadRequest, + statusCode: HttpStatusCode.GatewayTimeout, userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); + } - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + MsalServiceException msalException = null; + var stopwatch = Stopwatch.StartNew(); + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; } + stopwatch.Stop(); + Assert.IsNotNull(msalException); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + const int ImdsExponentialStrategyMaxRetriesInMs = 7000; + Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyMaxRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); + + // ensure that exactly 4 requests were made: initial request + 3 retries + Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.ExponentialStrategyNumRetries); + Assert.AreEqual(httpManager.QueueSize, 0); } } [DataTestMethod] [DataRow(null, null)] // SAMI [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI - public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync(string userAssignedId, UserAssignedIdentityId userAssignedIdentityId) + public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) { using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); - - using (var httpManager = new MockHttpManager(disableInternalRetries: true)) + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.BadRequest, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + try { - ManagedIdentityId managedIdentityId = userAssignedId == null - ? ManagedIdentityId.SystemAssigned - : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - var mi = miBuilder.Build(); + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); - httpManager.AddManagedIdentityMockHandler( - ManagedIdentityTests.ImdsEndpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiImdsErrorResponse(), - ManagedIdentitySource.Imds, - statusCode: HttpStatusCode.InternalServerError, - userAssignedId: userAssignedId, - userAssignedIdentityId: userAssignedIdentityId); + // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); + } + } - MsalServiceException msalException = null; - try - { - await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - msalException = ex as MsalServiceException; - } - Assert.IsNotNull(msalException); - - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.numRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + [DataTestMethod] + [DataRow(null, null)] // SAMI + [DataRow(TestConstants.ClientId, UserAssignedIdentityId.ClientId)] // UAMI + public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( + string userAssignedId, + UserAssignedIdentityId userAssignedIdentityId) + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager(disableInternalRetries: true)) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityId managedIdentityId = userAssignedId == null + ? ManagedIdentityId.SystemAssigned + : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); + var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + .WithHttpManager(httpManager); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + var mi = miBuilder.Build(); + + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.InternalServerError, + userAssignedId: userAssignedId, + userAssignedIdentityId: userAssignedIdentityId); + + MsalServiceException msalException = null; + try + { + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync().ConfigureAwait(false); } + catch (Exception ex) + { + msalException = ex as MsalServiceException; + } + Assert.IsNotNull(msalException); + + // ensure that only the initial request was made + Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); + Assert.AreEqual(httpManager.QueueSize, 0); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 36a819d23e..5af8dbfcf7 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -57,6 +57,13 @@ public static void ClassCleanup() DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalDefaultManagedIdentityRetryDelayMs; } + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + ImdsRetryPolicy.NumRetries = 0; + } + [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] [DataRow(AppServiceEndpoint, ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] @@ -1305,7 +1312,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1324,7 +1331,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries (DefaultRetryPolicy.numRetries would be 6 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); for (int i = 0; i < NumErrors; i++) @@ -1343,7 +1350,7 @@ await mi.AcquireTokenForManagedIdentity(Resource) Assert.IsNotNull(ex); // 3 retries (DefaultRetryPolicy.numRetries would be 9 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.numRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); + Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); Assert.AreEqual(httpManager.QueueSize, 0); } } From f5668c742433738e048645a2baae409f91954c10 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 18:25:25 -0400 Subject: [PATCH 47/68] whitespace --- .../ManagedIdentityTests/ManagedIdentityTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index 5af8dbfcf7..e913433baa 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -61,6 +61,7 @@ public static void ClassCleanup() public override void TestInitialize() { base.TestInitialize(); + ImdsRetryPolicy.NumRetries = 0; } From 4cd60edf037e8f0cd7bfbffc5fdf89df2531f9eb Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 19:15:17 -0400 Subject: [PATCH 48/68] Final adjustment of unit tests --- global.json | 8 +++---- .../CoreTests/HttpTests/HttpManagerTests.cs | 22 +++++++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/global.json b/global.json index 66e4a5c8a7..cfbcf3eaec 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "8.0.404", - "rollForward": "latestFeature" - } + "sdk": { + "version": "9.0.201", + "rollForward": "latestFeature" + } } diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index bee5fb4dd9..ef5fe07620 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -340,7 +340,7 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) for (int i = 0; i < NumErrors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); @@ -431,7 +431,7 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) for (int i = 0; i < NumErrors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadGateway); @@ -462,7 +462,7 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (3) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) for (int i = 0; i < NumErrors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); @@ -493,8 +493,12 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); - httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); + } var exc = await AssertException.TaskThrowsAsync(() => httpManager.SendRequestAsync( @@ -521,8 +525,12 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() { using (var httpManager = new MockHttpManager()) { - httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); - httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); + // Simulate permanent errors (to trigger the maximum number of retries) + const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) + for (int i = 0; i < NumErrors; i++) + { + httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); + } var exc = await AssertException.TaskThrowsAsync(() => httpManager.SendRequestAsync( From 1854cd329c4075aafcc78e17ab849ccbc9e53714 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Fri, 2 May 2025 19:37:18 -0400 Subject: [PATCH 49/68] Simplified params for DefaultRetryPolicy --- global.json | 8 ++--- .../Http/DefaultRetryPolicy.cs | 29 ++++++++++++++----- .../Instance/Region/RegionManager.cs | 6 ++-- .../Validation/AdfsAuthorityValidator.cs | 6 ++-- .../AzureArcManagedIdentitySource.cs | 6 ++-- .../ManagedIdentity/ManagedIdentityRequest.cs | 6 ++-- .../OAuth2/OAuth2Client.cs | 6 ++-- .../WsTrust/WsTrustWebRequestManager.cs | 9 ++---- .../CoreTests/HttpTests/HttpManagerTests.cs | 6 ++-- 9 files changed, 40 insertions(+), 42 deletions(-) diff --git a/global.json b/global.json index cfbcf3eaec..66e4a5c8a7 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "sdk": { - "version": "9.0.201", - "rollForward": "latestFeature" - } + "sdk": { + "version": "8.0.404", + "rollForward": "latestFeature" + } } diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index 3f5b5dd21a..beaa27b405 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -9,6 +9,12 @@ namespace Microsoft.Identity.Client.Http { class DefaultRetryPolicy : IRetryPolicy { + public enum RequestType + { + STS, + ManagedIdentity + } + private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); // referenced in unit tests @@ -27,14 +33,23 @@ class DefaultRetryPolicy : IRetryPolicy private int MaxRetries; private readonly Func RetryCondition; - public DefaultRetryPolicy( - int retryDelayMs, - int maxRetries, - Func retryCondition) + public DefaultRetryPolicy(RequestType requestType) { - DefaultRetryDelayMs = retryDelayMs; - MaxRetries = maxRetries; - RetryCondition = retryCondition; + switch (requestType) + { + case RequestType.ManagedIdentity: + DefaultRetryDelayMs = DefaultManagedIdentityRetryDelayMs; + MaxRetries = DefaultManagedIdentityMaxRetries; + RetryCondition = HttpRetryConditions.DefaultManagedIdentity; + break; + case RequestType.STS: + DefaultRetryDelayMs = DefaultStsRetryDelayMs; + MaxRetries = DefaultStsMaxRetries; + RetryCondition = HttpRetryConditions.Sts; + break; + default: + throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Unknown request type"); + } } public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index 52f6521a94..73dac4449a 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -13,6 +13,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Client.Region { @@ -46,10 +47,7 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static bool s_failedAutoDiscovery = false; private static string s_regionDiscoveryDetails; - private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); public RegionManager( IHttpManager httpManager, diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index ac44304c5b..9edb712b28 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -9,6 +9,7 @@ using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Client.Instance.Validation { @@ -29,10 +30,7 @@ public async Task ValidateAuthorityAsync( var resource = $"https://{authorityInfo.Host}"; string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( new Uri(webFingerUrl), diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index cf95fc9a0a..bc152e106a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -11,6 +11,7 @@ using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Shared; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -123,10 +124,7 @@ protected override async Task HandleResponseAsync( _requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request."); request.Headers.Add("Authorization", authHeaderValue); - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, - DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.Sts); + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.ManagedIdentity); response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( request.ComputeUri(), diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 0218beeb4a..b202730aeb 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -6,6 +6,7 @@ using System.Net.Http; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Utils; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -31,10 +32,7 @@ public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retr BodyParameters = new Dictionary(); QueryParameters = new Dictionary(); - IRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, - DefaultRetryPolicy.DefaultManagedIdentityMaxRetries, - HttpRetryConditions.DefaultManagedIdentity); + IRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.ManagedIdentity); RetryPolicy = retryPolicy ?? defaultRetryPolicy; } diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 0eb1e08948..25d4869fae 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -17,6 +17,7 @@ using Microsoft.Identity.Client.Utils; using Microsoft.Identity.Client.Internal.Broker; using System.Security.Cryptography.X509Certificates; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; @@ -119,10 +120,7 @@ internal async Task ExecuteRequestAsync( using (requestContext.Logger.LogBlockDuration($"[Oauth2Client] Sending {method} request ")) { - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); try { diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index 4a026cdeef..064a93d54e 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -13,18 +11,15 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; -using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.Utils; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Client.WsTrust { internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; - private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); public WsTrustWebRequestManager(IHttpManager httpManager) { diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index ef5fe07620..c6f1b0c8a7 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -17,6 +17,7 @@ using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; +using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests { @@ -50,10 +51,7 @@ public void TestInitialize() TestCommon.ResetInternalStaticCaches(); DefaultRetryPolicy.NumRetries = 0; - StsRetryPolicy = new DefaultRetryPolicy( - DefaultRetryPolicy.DefaultStsRetryDelayMs, - DefaultRetryPolicy.DefaultStsMaxRetries, - HttpRetryConditions.Sts); + StsRetryPolicy = new DefaultRetryPolicy(RequestType.STS); } [TestMethod] From 001b32de2e6fb2a190c918495c50931596c0aaf5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Tue, 20 May 2025 12:57:32 -0400 Subject: [PATCH 50/68] Implemented some GitHub feedback --- .../Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs index beaa27b405..b4eaf4ffa7 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs @@ -30,7 +30,7 @@ public enum RequestType public static int NumRetries { get; set; } = 0; public static int DefaultRetryDelayMs; - private int MaxRetries; + private int _maxRetries; private readonly Func RetryCondition; public DefaultRetryPolicy(RequestType requestType) @@ -39,12 +39,12 @@ public DefaultRetryPolicy(RequestType requestType) { case RequestType.ManagedIdentity: DefaultRetryDelayMs = DefaultManagedIdentityRetryDelayMs; - MaxRetries = DefaultManagedIdentityMaxRetries; + _maxRetries = DefaultManagedIdentityMaxRetries; RetryCondition = HttpRetryConditions.DefaultManagedIdentity; break; case RequestType.STS: DefaultRetryDelayMs = DefaultStsRetryDelayMs; - MaxRetries = DefaultStsMaxRetries; + _maxRetries = DefaultStsMaxRetries; RetryCondition = HttpRetryConditions.Sts; break; default: @@ -56,7 +56,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce { // Check if the status code is retriable and if the current retry count is less than max retries if (RetryCondition(response, exception) && - retryCount < MaxRetries) + retryCount < _maxRetries) { // used below in the log statement, also referenced in the unit tests NumRetries = retryCount + 1; From bf41d92ab7b2935e4e94cdf0f5fbb38105d24f62 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 12:14:18 -0400 Subject: [PATCH 51/68] Fixed build messages --- .../ManagedIdentityTests/ImdsTests.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 17d575a5d7..7fafe7ab40 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -20,35 +20,35 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private static int _originalMinBackoff; - private static int _originalMaxBackoff; - private static int _originalDeltaBackoff; - private static int _originalGoneRetryAfter; + private static int s_originalMinBackoff; + private static int s_originalMaxBackoff; + private static int s_originalDeltaBackoff; + private static int s_originalGoneRetryAfter; [ClassInitialize] public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalMinBackoff = ImdsRetryPolicy.MinExponentialBackoffMs; - _originalMaxBackoff = ImdsRetryPolicy.MaxExponentialBackoffMs; - _originalDeltaBackoff = ImdsRetryPolicy.ExponentialDeltaBackoffMs; - _originalGoneRetryAfter = ImdsRetryPolicy.HttpStatusGoneRetryAfterMs; + s_originalMinBackoff = ImdsRetryPolicy.MinExponentialBackoffMs; + s_originalMaxBackoff = ImdsRetryPolicy.MaxExponentialBackoffMs; + s_originalDeltaBackoff = ImdsRetryPolicy.ExponentialDeltaBackoffMs; + s_originalGoneRetryAfter = ImdsRetryPolicy.HttpStatusGoneRetryAfterMs; // Speed up retry delays by 100x - ImdsRetryPolicy.MinExponentialBackoffMs = (int)(_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MaxExponentialBackoffMs = (int)(_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.ExponentialDeltaBackoffMs = (int)(_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = (int)(_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MinExponentialBackoffMs = (int)(s_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.MaxExponentialBackoffMs = (int)(s_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.ExponentialDeltaBackoffMs = (int)(s_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = (int)(s_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - ImdsRetryPolicy.MinExponentialBackoffMs = _originalMinBackoff; - ImdsRetryPolicy.MaxExponentialBackoffMs = _originalMaxBackoff; - ImdsRetryPolicy.ExponentialDeltaBackoffMs = _originalDeltaBackoff; - ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = _originalGoneRetryAfter; + ImdsRetryPolicy.MinExponentialBackoffMs = s_originalMinBackoff; + ImdsRetryPolicy.MaxExponentialBackoffMs = s_originalMaxBackoff; + ImdsRetryPolicy.ExponentialDeltaBackoffMs = s_originalDeltaBackoff; + ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = s_originalGoneRetryAfter; } [TestInitialize] @@ -74,13 +74,13 @@ public async Task ImdsFails404TwiceThenSucceeds200Async( ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate two 404s (to trigger retries), then a successful response const int NUM_404 = 2; @@ -107,7 +107,7 @@ public async Task ImdsFails404TwiceThenSucceeds200Async( var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -140,13 +140,13 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async( ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate four 410s (to trigger retries), then a successful response const int NUM_410 = 4; @@ -173,7 +173,7 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async( var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -205,13 +205,13 @@ public async Task ImdsFails410PermanentlyAsync( ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 410s (to trigger the maximum number of retries) const int NUM_410 = ImdsRetryPolicy.LinearStrategyNumRetries + 1; // initial request + maximum number of retries (7) @@ -265,13 +265,13 @@ public async Task ImdsFails504PermanentlyAsync( ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 504s (to trigger the maximum number of retries) const int NUM_504 = ImdsRetryPolicy.ExponentialStrategyNumRetries + 1; // initial request + maximum number of retries (3) @@ -326,13 +326,13 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -376,13 +376,13 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( ManagedIdentityId managedIdentityId = userAssignedId == null ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, From d9a2dd3830d64e76daf16b950f022954d7d379d2 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 13:44:33 -0400 Subject: [PATCH 52/68] adjusted unit test delay --- .../ManagedIdentityTests/DefaultRetryPolicyTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index f8d05c4c91..dbed52006d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -319,7 +319,8 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc // this test can not be made one hundred times faster because it is based on a date const int retryAfterMilliseconds = 3000; - var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds).ToString("R"); + // an extra second has been added to account for this date operation + var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds + 1000).ToString("R"); // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( From f4433c75675b59ffd427afbd96d67cc752556524 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 13:59:53 -0400 Subject: [PATCH 53/68] Fixed build messages --- .../DefaultRetryPolicyTests.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index dbed52006d..4deca28b61 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -24,23 +24,23 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class DefaultRetryPolicyTests : TestBase { - private static int _originalManagedIdentityRetryDelay; + private static int s_originalManagedIdentityRetryDelay; [ClassInitialize] public static void ClassInitialize(TestContext _) { // Backup original retry delay values - _originalManagedIdentityRetryDelay = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; + s_originalManagedIdentityRetryDelay = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; // Speed up retry delays by 100x - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(s_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); } [ClassCleanup] public static void ClassCleanup() { // Restore retry policy values after each test - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalManagedIdentityRetryDelay; + DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = s_originalManagedIdentityRetryDelay; } [TestInitialize] @@ -68,13 +68,13 @@ public async Task UAMIFails500OnceThenSucceeds200Async( UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( @@ -97,7 +97,7 @@ public async Task UAMIFails500OnceThenSucceeds200Async( var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -131,13 +131,13 @@ public async Task UAMIFails500PermanentlyAsync( UserAssignedIdentityId userAssignedIdentityId = UserAssignedIdentityId.ClientId; ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); - var miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 500s (to trigger the maximum number of retries) const int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) @@ -190,14 +190,14 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( @@ -216,7 +216,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -247,14 +247,14 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // make it one hundred times faster so the test completes quickly double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; @@ -277,7 +277,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -308,14 +308,14 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc using (var httpManager = new MockHttpManager()) { SetEnvironmentVariables(managedIdentitySource, endpoint); - - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // this test can not be made one hundred times faster because it is based on a date const int retryAfterMilliseconds = 3000; @@ -340,7 +340,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc var stopwatch = Stopwatch.StartNew(); - var result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) .ExecuteAsync() .ConfigureAwait(false); @@ -372,13 +372,13 @@ public async Task SAMIFails500Permanently( { SetEnvironmentVariables(managedIdentitySource, endpoint); - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 500s (to trigger the maximum number of retries) int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) @@ -425,13 +425,13 @@ public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy { SetEnvironmentVariables(managedIdentitySource, endpoint); - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); httpManager.AddManagedIdentityMockHandler( endpoint, @@ -473,13 +473,13 @@ public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( { SetEnvironmentVariables(managedIdentitySource, endpoint); - var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) .WithHttpManager(httpManager); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; - var mi = miBuilder.Build(); + IManagedIdentityApplication mi = miBuilder.Build(); httpManager.AddManagedIdentityMockHandler( endpoint, From fb9653a1fa10d43d69234d644445b692e773ae1c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 14:00:27 -0400 Subject: [PATCH 54/68] removed unused import --- .../ManagedIdentityTests/ImdsTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 7fafe7ab40..a26f1b529d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -9,7 +9,6 @@ using Microsoft.Identity.Client.AppConfig; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.ManagedIdentity; -using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; From 96a65cc4c2f7137226f7e229581f4fd389fa46d6 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 21 May 2025 18:31:16 -0400 Subject: [PATCH 55/68] added temporary logs for debugging CI issues --- .../DefaultRetryPolicyTests.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index 4deca28b61..e3bc37c9c1 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -102,10 +102,13 @@ public async Task UAMIFails500OnceThenSucceeds200Async( .ConfigureAwait(false); stopwatch.Stop(); - + // linear backoff (1 second * 1 retry) + Debug.Assert( + stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs), + $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - + // ensure that exactly 2 requests were made: initial request + 1 retry Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); Assert.AreEqual(httpManager.QueueSize, 0); @@ -168,6 +171,9 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsNotNull(msalException); // linear backoff (1 second * 3 retries) + Debug.Assert( + stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries), + $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs},\nDefaultRetryPolicy.DefaultManagedIdentityMaxRetries: {DefaultRetryPolicy.DefaultManagedIdentityMaxRetries}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries)); // ensure that exactly 4 requests were made: initial request + 3 retries @@ -223,6 +229,9 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( stopwatch.Stop(); // linear backoff (1 second * 1 retry) + Debug.Assert( + stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, + $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); // ensure that exactly 2 requests were made: initial request + 1 retry @@ -284,6 +293,9 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy stopwatch.Stop(); // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Debug.Assert( + stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000), + $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nretryAfterSeconds * 1000: {retryAfterSeconds * 1000}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds // ensure that exactly 2 requests were made: initial request + 1 retry @@ -347,6 +359,9 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc stopwatch.Stop(); // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + Debug.Assert( + stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds, + $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nretryAfterMilliseconds: {retryAfterMilliseconds}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); // ensure that exactly 2 requests were made: initial request + 1 retry From 1be5bc1dcd88564db3d4283df92b86a50cec22f5 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Thu, 22 May 2025 14:20:58 -0400 Subject: [PATCH 56/68] edited temporary logs --- .../DefaultRetryPolicyTests.cs | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index e3bc37c9c1..0bc04360cc 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -102,11 +102,9 @@ public async Task UAMIFails500OnceThenSucceeds200Async( .ConfigureAwait(false); stopwatch.Stop(); - + // linear backoff (1 second * 1 retry) - Debug.Assert( - stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs), - $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); // ensure that exactly 2 requests were made: initial request + 1 retry @@ -171,9 +169,7 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) Assert.IsNotNull(msalException); // linear backoff (1 second * 3 retries) - Debug.Assert( - stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries), - $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs},\nDefaultRetryPolicy.DefaultManagedIdentityMaxRetries: {DefaultRetryPolicy.DefaultManagedIdentityMaxRetries}"); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs} * {DefaultRetryPolicy.DefaultManagedIdentityMaxRetries}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries)); // ensure that exactly 4 requests were made: initial request + 3 retries @@ -229,9 +225,7 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( stopwatch.Stop(); // linear backoff (1 second * 1 retry) - Debug.Assert( - stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs, - $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nDefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs: {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); // ensure that exactly 2 requests were made: initial request + 1 retry @@ -293,9 +287,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy stopwatch.Stop(); // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Debug.Assert( - stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000), - $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nretryAfterSeconds * 1000: {retryAfterSeconds * 1000}"); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {retryAfterSeconds} * 1000"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds // ensure that exactly 2 requests were made: initial request + 1 retry @@ -359,9 +351,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucc stopwatch.Stop(); // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Debug.Assert( - stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds, - $"stopwatch.ElapsedMilliseconds: {stopwatch.ElapsedMilliseconds},\nretryAfterMilliseconds: {retryAfterMilliseconds}"); + Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {retryAfterMilliseconds}"); Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); // ensure that exactly 2 requests were made: initial request + 1 retry From 7c87c30b40ec66982d0e4254a3f810075fde18e8 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Wed, 28 May 2025 11:46:18 -0400 Subject: [PATCH 57/68] Created "retry" folder inside of "http" and updated all file paths (#5300) --- src/client/Microsoft.Identity.Client/Http/HttpManager.cs | 1 + src/client/Microsoft.Identity.Client/Http/IHttpManager.cs | 1 + .../Http/{ => Retry}/DefaultRetryPolicy.cs | 2 +- .../Http/{ => Retry}/ExponentialRetryStrategy.cs | 2 +- .../Http/{ => Retry}/HttpRetryCondition.cs | 2 +- .../Http/{ => Retry}/IRetryPolicy.cs | 2 +- .../Http/{ => Retry}/ImdsRetryPolicy.cs | 2 +- .../Http/{ => Retry}/LinearRetryStrategy.cs | 2 +- .../Instance/Region/RegionManager.cs | 3 ++- .../Instance/Validation/AdfsAuthorityValidator.cs | 4 ++-- .../ManagedIdentity/AzureArcManagedIdentitySource.cs | 3 ++- .../ManagedIdentity/ImdsManagedIdentitySource.cs | 2 +- .../ManagedIdentity/ManagedIdentityRequest.cs | 4 ++-- src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs | 4 +++- .../WsTrust/WsTrustWebRequestManager.cs | 3 ++- .../Core/Mocks/MockHttpManager.cs | 1 + .../Infrastructure/MsiProxyHttpManager.cs | 5 +---- .../CoreTests/HttpTests/HttpManagerTests.cs | 4 ++-- .../Helpers/ParallelRequestMockHandler.cs | 1 + .../ManagedIdentityTests/DefaultRetryPolicyTests.cs | 2 +- .../ManagedIdentityTests/ImdsTests.cs | 2 +- .../ManagedIdentityTests/ManagedIdentityTests.cs | 4 +--- 22 files changed, 30 insertions(+), 26 deletions(-) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/DefaultRetryPolicy.cs (98%) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/ExponentialRetryStrategy.cs (97%) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/HttpRetryCondition.cs (98%) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/IRetryPolicy.cs (88%) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/ImdsRetryPolicy.cs (98%) rename src/client/Microsoft.Identity.Client/Http/{ => Retry}/LinearRetryStrategy.cs (97%) diff --git a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs index c8f45813ab..f50d325498 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/HttpManager.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http.Retry; namespace Microsoft.Identity.Client.Http { diff --git a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs index 87cd955a2a..04e60f0619 100644 --- a/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs +++ b/src/client/Microsoft.Identity.Client/Http/IHttpManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Http.Retry; namespace Microsoft.Identity.Client.Http { diff --git a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs similarity index 98% rename from src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs index b4eaf4ffa7..eeb88c97e6 100644 --- a/src/client/Microsoft.Identity.Client/Http/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { class DefaultRetryPolicy : IRetryPolicy { diff --git a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/ExponentialRetryStrategy.cs similarity index 97% rename from src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/ExponentialRetryStrategy.cs index 5893c03ad0..bdc4abc008 100644 --- a/src/client/Microsoft.Identity.Client/Http/ExponentialRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/ExponentialRetryStrategy.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { internal class ExponentialRetryStrategy { diff --git a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs b/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs similarity index 98% rename from src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs index d6e8c1fa8d..7d43515842 100644 --- a/src/client/Microsoft.Identity.Client/Http/HttpRetryCondition.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { internal static class HttpRetryConditions { diff --git a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs similarity index 88% rename from src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs index cb3b74c45c..194a42eed4 100644 --- a/src/client/Microsoft.Identity.Client/Http/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { internal interface IRetryPolicy { diff --git a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs similarity index 98% rename from src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs index d9f4ece919..df6eb126c7 100644 --- a/src/client/Microsoft.Identity.Client/Http/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { internal class ImdsRetryPolicy : IRetryPolicy { diff --git a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs similarity index 97% rename from src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs rename to src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs index bc2edb8bbb..847f4f5266 100644 --- a/src/client/Microsoft.Identity.Client/Http/LinearRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Identity.Client.Http +namespace Microsoft.Identity.Client.Http.Retry { internal class LinearRetryStrategy { diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index 73dac4449a..b2924cb9d3 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -10,10 +10,11 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Client.Region { diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 9edb712b28..57633d4174 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -6,10 +6,10 @@ using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Client.Instance.Validation { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index bc152e106a..6eae5abd4c 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -9,9 +9,10 @@ using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Shared; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 59daa01a41..57b35a6135 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; namespace Microsoft.Identity.Client.ManagedIdentity diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index b202730aeb..edd2590104 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -4,9 +4,9 @@ using System; using System.Collections.Generic; using System.Net.Http; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 25d4869fae..ab8c063617 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -17,7 +17,9 @@ using Microsoft.Identity.Client.Utils; using Microsoft.Identity.Client.Internal.Broker; using System.Security.Cryptography.X509Certificates; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using Microsoft.Identity.Client.Http.Retry; + #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index d5a9bec723..a79fe21c05 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -10,9 +10,10 @@ using System.Xml.Linq; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Client.WsTrust { diff --git a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs index 9512304848..9f0bf34cae 100644 --- a/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Common/Core/Mocks/MockHttpManager.cs @@ -16,6 +16,7 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.Identity.Test.Common.Core.Mocks diff --git a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs index 7520306f52..e7cfd54dcf 100644 --- a/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs +++ b/tests/Microsoft.Identity.Test.Integration.netcore/Infrastructure/MsiProxyHttpManager.cs @@ -3,19 +3,16 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web; -using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Test.LabInfrastructure; namespace Microsoft.Identity.Test.Integration.NetFx.Infrastructure diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index c6f1b0c8a7..960b026375 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -11,13 +11,13 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; -using static Microsoft.Identity.Client.Http.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests { diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs index 740b1f7a03..22431fcf9b 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/ParallelRequestMockHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.Identity.Test.Unit.RequestsTests; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index 0bc04360cc..b30de4792f 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index a26f1b529d..5c069ba023 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index dd0d9a5c39..f4eba0630d 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -6,14 +6,12 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Security; using System.Net.Sockets; -using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; From 01bcb73fefc2c8f3332b6e7c98c059581660d756 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:12:17 -0400 Subject: [PATCH 58/68] Created Production and Test Retry Policy Factories, and adjusted unit tests (#5308) * Created a prod and test retry policy factories, and adjusted unit tests * Retry Policy tests no longer rely on NumRetries static variable from individual retry policies (#5311) Replaced NumRetries static variable tests with QueueSize tests --- .../AppConfig/ApplicationConfiguration.cs | 2 + .../ManagedIdentityApplicationBuilder.cs | 33 +- .../Http/Retry/DefaultRetryPolicy.cs | 51 ++- .../Http/Retry/IRetryPolicyFactory.cs | 12 + .../Http/Retry/ImdsRetryPolicy.cs | 33 +- .../Http/Retry/RetryPolicyFactory.cs | 25 ++ .../Instance/Region/RegionManager.cs | 26 +- .../Validation/AdfsAuthorityValidator.cs | 6 +- .../Internal/Constants.cs | 6 + .../AbstractManagedIdentity.cs | 8 +- .../AzureArcManagedIdentitySource.cs | 6 +- .../ImdsManagedIdentitySource.cs | 4 +- .../ManagedIdentity/ManagedIdentityRequest.cs | 9 +- .../Microsoft.Identity.Client.csproj | 7 + .../OAuth2/OAuth2Client.cs | 11 +- .../WsTrust/WsTrustWebRequestManager.cs | 18 +- .../CoreTests/HttpTests/HttpManagerTests.cs | 124 ++++---- .../Helpers/TestRetryPolicies.cs | 31 ++ .../Helpers/TestRetryPolicyFactory.cs | 26 ++ .../DefaultRetryPolicyTests.cs | 296 +++++------------- .../ManagedIdentityTests/ImdsTests.cs | 253 ++++++++------- .../ManagedIdentityTests.cs | 83 ++--- 22 files changed, 520 insertions(+), 550 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs create mode 100644 src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs create mode 100644 tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 2c9fe7e587..8f3f6d316f 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -11,6 +11,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Http; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Instance; using Microsoft.Identity.Client.Instance.Discovery; using Microsoft.Identity.Client.Internal.Broker; @@ -208,5 +209,6 @@ public X509Certificate2 ClientCredentialCertificate public bool IsInstanceDiscoveryEnabled { get; internal set; } = true; #endregion + internal IRetryPolicyFactory RetryPolicyFactory { get; set; } } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs index 434d2764ce..0308b1c266 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs @@ -5,15 +5,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; -using Microsoft.Identity.Client.TelemetryCore; -using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; -using Microsoft.Identity.Client.Utils; using Microsoft.IdentityModel.Abstractions; namespace Microsoft.Identity.Client @@ -60,10 +54,15 @@ private static ApplicationConfiguration BuildConfiguration(ManagedIdentityId man var config = new ApplicationConfiguration(MsalClientType.ManagedIdentityClient); config.ManagedIdentityId = managedIdentityId; - config.CacheSynchronizationEnabled = false; config.AccessorOptions = CacheOptions.EnableSharedCacheOptions; + // Ensure the default retry policy factory is set if the test factory was not provided + if (config.RetryPolicyFactory == null) + { + config.RetryPolicyFactory = new RetryPolicyFactory(); + } + return config; } @@ -83,6 +82,17 @@ internal ManagedIdentityApplicationBuilder WithAppTokenCacheInternalForTest(ITok return this; } + /// + /// Internal only: Allows tests to inject a custom retry policy factory. + /// + /// The retry policy factory to use. + /// The builder for chaining. + internal ManagedIdentityApplicationBuilder WithRetryPolicyFactory(IRetryPolicyFactory factory) + { + Config.RetryPolicyFactory = factory; + return this; + } + /// /// Microsoft Identity specific OIDC extension that allows resource challenges to be resolved without interaction. /// Allows configuration of one or more client capabilities, e.g. "llt" @@ -116,6 +126,13 @@ public IManagedIdentityApplication Build() internal ManagedIdentityApplication BuildConcrete() { DefaultConfiguration(); + + // Ensure the default retry policy factory is set if the test factory was not provided + if (Config.RetryPolicyFactory == null) + { + Config.RetryPolicyFactory = new RetryPolicyFactory(); + } + return new ManagedIdentityApplication(BuildConfiguration()); } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs index eeb88c97e6..5169babf3b 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs @@ -4,63 +4,54 @@ using System; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.Http.Retry { class DefaultRetryPolicy : IRetryPolicy { - public enum RequestType - { - STS, - ManagedIdentity - } - - private LinearRetryStrategy linearRetryStrategy = new LinearRetryStrategy(); - // referenced in unit tests public const int DefaultStsMaxRetries = 1; public const int DefaultManagedIdentityMaxRetries = 3; - // overridden in the unit tests so that they run faster - public static int DefaultStsRetryDelayMs { get; set; } = 1000; - public static int DefaultManagedIdentityRetryDelayMs { get; set; } = 1000; - - // used for comparison, in the unit tests - // will be reset after every test - public static int NumRetries { get; set; } = 0; + private const int DefaultStsRetryDelayMs = 1000; + private const int DefaultManagedIdentityRetryDelayMs = 1000; - public static int DefaultRetryDelayMs; - private int _maxRetries; - private readonly Func RetryCondition; + public readonly int _defaultRetryDelayMs; + private readonly int _maxRetries; + private readonly Func _retryCondition; + private readonly LinearRetryStrategy _linearRetryStrategy = new LinearRetryStrategy(); public DefaultRetryPolicy(RequestType requestType) { switch (requestType) { - case RequestType.ManagedIdentity: - DefaultRetryDelayMs = DefaultManagedIdentityRetryDelayMs; + case RequestType.ManagedIdentityDefault: + _defaultRetryDelayMs = DefaultManagedIdentityRetryDelayMs; _maxRetries = DefaultManagedIdentityMaxRetries; - RetryCondition = HttpRetryConditions.DefaultManagedIdentity; + _retryCondition = HttpRetryConditions.DefaultManagedIdentity; break; case RequestType.STS: - DefaultRetryDelayMs = DefaultStsRetryDelayMs; + _defaultRetryDelayMs = DefaultStsRetryDelayMs; _maxRetries = DefaultStsMaxRetries; - RetryCondition = HttpRetryConditions.Sts; + _retryCondition = HttpRetryConditions.Sts; break; default: throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Unknown request type"); } } + internal virtual Task DelayAsync(int milliseconds) + { + return Task.Delay(milliseconds); + } + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { // Check if the status code is retriable and if the current retry count is less than max retries - if (RetryCondition(response, exception) && + if (_retryCondition(response, exception) && retryCount < _maxRetries) { - // used below in the log statement, also referenced in the unit tests - NumRetries = retryCount + 1; - // Use HeadersAsDictionary to check for "Retry-After" header string retryAfter = string.Empty; if (response?.HeadersAsDictionary != null) @@ -68,12 +59,12 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce response.HeadersAsDictionary.TryGetValue("Retry-After", out retryAfter); } - int retryAfterDelay = linearRetryStrategy.calculateDelay(retryAfter, DefaultRetryDelayMs); + int retryAfterDelay = _linearRetryStrategy.calculateDelay(retryAfter, _defaultRetryDelayMs); - logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {NumRetries})"); + logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); // Pause execution for the calculated delay - await Task.Delay(retryAfterDelay).ConfigureAwait(false); + await DelayAsync(retryAfterDelay).ConfigureAwait(false); return true; } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs new file mode 100644 index 0000000000..f39e27828c --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using static Microsoft.Identity.Client.Internal.Constants; + +namespace Microsoft.Identity.Client.Http.Retry +{ + internal interface IRetryPolicyFactory + { + IRetryPolicy GetRetryPolicy(RequestType requestType); + } +} diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs index df6eb126c7..7cf64d5ecc 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs @@ -10,30 +10,28 @@ namespace Microsoft.Identity.Client.Http.Retry { internal class ImdsRetryPolicy : IRetryPolicy { - private const int HttpStatusGoneRetryAfterMsInternal = 10 * 1000; // 10 seconds - // referenced in unit tests public const int ExponentialStrategyNumRetries = 3; public const int LinearStrategyNumRetries = 7; - // used for comparison, in the unit tests - // will be reset after every test - public static int NumRetries { get; set; } = 0; - - // overridden in the unit tests so that they run faster - public static int MinExponentialBackoffMs { get; set; } = 1000; - public static int MaxExponentialBackoffMs { get; set; } = 4000; - public static int ExponentialDeltaBackoffMs { get; set; } = 2000; - public static int HttpStatusGoneRetryAfterMs { get; set; } = HttpStatusGoneRetryAfterMsInternal; + private const int MinExponentialBackoffMs = 1000; + private const int MaxExponentialBackoffMs = 4000; + private const int ExponentialDeltaBackoffMs = 2000; + private const int HttpStatusGoneRetryAfterMs = 10000; - private int MaxRetries; + private int _maxRetries; - private ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( + private readonly ExponentialRetryStrategy _exponentialRetryStrategy = new ExponentialRetryStrategy( ImdsRetryPolicy.MinExponentialBackoffMs, ImdsRetryPolicy.MaxExponentialBackoffMs, ImdsRetryPolicy.ExponentialDeltaBackoffMs ); + internal virtual Task DelayAsync(int milliseconds) + { + return Task.Delay(milliseconds); + } + public async Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger) { int httpStatusCode = (int)response.StatusCode; @@ -41,18 +39,15 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce if (retryCount == 0) { // Calculate the maxRetries based on the status code, once per request - MaxRetries = httpStatusCode == (int)HttpStatusCode.Gone + _maxRetries = httpStatusCode == (int)HttpStatusCode.Gone ? LinearStrategyNumRetries : ExponentialStrategyNumRetries; } // Check if the status code is retriable and if the current retry count is less than max retries if (HttpRetryConditions.Imds(response, exception) && - retryCount < MaxRetries) + retryCount < _maxRetries) { - // used below in the log statement, also referenced in the unit tests - NumRetries = retryCount + 1; - int retryAfterDelay = httpStatusCode == (int)HttpStatusCode.Gone ? HttpStatusGoneRetryAfterMs : _exponentialRetryStrategy.CalculateDelay(retryCount); @@ -60,7 +55,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); // Pause execution for the calculated delay - await Task.Delay(retryAfterDelay).ConfigureAwait(false); + await DelayAsync(retryAfterDelay).ConfigureAwait(false); return true; } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs new file mode 100644 index 0000000000..829951feb8 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using static Microsoft.Identity.Client.Internal.Constants; + +namespace Microsoft.Identity.Client.Http.Retry +{ + internal class RetryPolicyFactory : IRetryPolicyFactory + { + public virtual IRetryPolicy GetRetryPolicy(RequestType requestType) + { + switch (requestType) + { + case RequestType.STS: + case RequestType.ManagedIdentityDefault: + return new DefaultRetryPolicy(requestType); + case RequestType.Imds: + return new ImdsRetryPolicy(); + default: + throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Unknown request type."); + } + } + } +} diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index b2924cb9d3..49bc49caff 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -14,7 +14,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.Region { @@ -48,8 +48,6 @@ public RegionInfo(string region, RegionAutodetectionSource regionSource, string private static bool s_failedAutoDiscovery = false; private static string s_regionDiscoveryDetails; - private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); - public RegionManager( IHttpManager httpManager, int imdsCallTimeout = 2000, @@ -80,9 +78,12 @@ public async Task GetAzureRegionAsync(RequestContext requestContext) requestContext.ApiEvent != null, "Do not call GetAzureRegionAsync outside of a request. This can happen if you perform instance discovery outside a request, for example as part of validating input params."); + IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); + // MSAL always performs region auto-discovery, even if the user configured an actual region // in order to detect inconsistencies and report via telemetry - var discoveredRegion = await DiscoverAndCacheAsync(logger, requestContext.UserCancellationToken).ConfigureAwait(false); + var discoveredRegion = await DiscoverAndCacheAsync(logger, requestContext.UserCancellationToken, retryPolicy).ConfigureAwait(false); RecordTelemetry(requestContext.ApiEvent, azureRegionConfig, discoveredRegion); @@ -157,7 +158,7 @@ private static bool IsTelemetryRecorded(ApiEvent apiEvent) apiEvent.RegionOutcome == default); } - private async Task DiscoverAndCacheAsync(ILoggerAdapter logger, CancellationToken requestCancellationToken) + private async Task DiscoverAndCacheAsync(ILoggerAdapter logger, CancellationToken requestCancellationToken, IRetryPolicy retryPolicy) { var regionInfo = GetCachedRegion(logger); if (regionInfo != null) @@ -165,12 +166,12 @@ private async Task DiscoverAndCacheAsync(ILoggerAdapter logger, Canc return regionInfo; } - var result = await DiscoverAsync(logger, requestCancellationToken).ConfigureAwait(false); + var result = await DiscoverAsync(logger, requestCancellationToken, retryPolicy).ConfigureAwait(false); return result; } - private async Task DiscoverAsync(ILoggerAdapter logger, CancellationToken requestCancellationToken) + private async Task DiscoverAsync(ILoggerAdapter logger, CancellationToken requestCancellationToken, IRetryPolicy retryPolicy) { RegionInfo result = null; @@ -212,14 +213,15 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); // A bad request occurs when the version in the IMDS call is no longer supported. if (response.StatusCode == HttpStatusCode.BadRequest) { - string apiVersion = await GetImdsUriApiVersionAsync(logger, headers, requestCancellationToken).ConfigureAwait(false); // Get the latest version + string apiVersion = await GetImdsUriApiVersionAsync(logger, headers, requestCancellationToken, retryPolicy).ConfigureAwait(false); // Get the latest version imdsUri = BuildImdsUri(apiVersion); + response = await _httpManager.SendRequestAsync( imdsUri, headers, @@ -230,7 +232,7 @@ private async Task DiscoverAsync(ILoggerAdapter logger, Cancellation mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(requestCancellationToken), - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); // Call again with updated version } @@ -319,7 +321,7 @@ private static bool ValidateRegion(string region, string source, ILoggerAdapter return true; } - private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dictionary headers, CancellationToken userCancellationToken) + private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dictionary headers, CancellationToken userCancellationToken, IRetryPolicy retryPolicy) { Uri imdsErrorUri = new(ImdsEndpoint); @@ -333,7 +335,7 @@ private async Task GetImdsUriApiVersionAsync(ILoggerAdapter logger, Dict mtlsCertificate: null, validateServerCertificate: null, cancellationToken: GetCancellationToken(userCancellationToken), - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); // When IMDS endpoint is called without the api version query param, bad request response comes back with latest version. diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 57633d4174..5f9ec09efb 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -10,6 +10,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.Instance.Validation { @@ -30,7 +31,8 @@ public async Task ValidateAuthorityAsync( var resource = $"https://{authorityInfo.Host}"; string webFingerUrl = Constants.FormatAdfsWebFingerUrl(authorityInfo.Host, resource); - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); + IRetryPolicyFactory retryPolicyFactory = _requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); Http.HttpResponse httpResponse = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( new Uri(webFingerUrl), @@ -42,7 +44,7 @@ public async Task ValidateAuthorityAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: _requestContext.UserCancellationToken, - retryPolicy: defaultRetryPolicy + retryPolicy: retryPolicy ) .ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs index d5cefd6f78..4580217e03 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs @@ -73,5 +73,11 @@ public static string FormatAdfsWebFingerUrl(string host, string resource) { return $"https://{host}/.well-known/webfinger?rel={DefaultRealm}&resource={resource}"; } + public enum RequestType + { + STS, + ManagedIdentityDefault, + Imds + } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs index 17b3c56f3a..3748745b4d 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AbstractManagedIdentity.cs @@ -14,6 +14,8 @@ using System.Text; using System.Security.Cryptography.X509Certificates; using System.Net.Security; +using Microsoft.Identity.Client.Http.Retry; + #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; @@ -57,6 +59,8 @@ public virtual async Task AuthenticateAsync( _requestContext.Logger.Info("[Managed Identity] Sending request to managed identity endpoints."); + IRetryPolicy retryPolicy = _requestContext.ServiceBundle.Config.RetryPolicyFactory.GetRetryPolicy(request.RequestType); + try { if (request.Method == HttpMethod.Get) @@ -72,7 +76,7 @@ public virtual async Task AuthenticateAsync( mtlsCertificate: null, validateServerCertificate: GetValidationCallback(), cancellationToken: cancellationToken, - retryPolicy: request.RetryPolicy).ConfigureAwait(false); + retryPolicy: retryPolicy).ConfigureAwait(false); } else { @@ -87,7 +91,7 @@ public virtual async Task AuthenticateAsync( mtlsCertificate: null, validateServerCertificate: GetValidationCallback(), cancellationToken: cancellationToken, - retryPolicy: request.RetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index 6eae5abd4c..4fe14dff65 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -13,6 +13,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Shared; using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -125,7 +126,8 @@ protected override async Task HandleResponseAsync( _requestContext.Logger.Verbose(() => "[Managed Identity] Adding authorization header to the request."); request.Headers.Add("Authorization", authHeaderValue); - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.ManagedIdentity); + IRetryPolicyFactory retryPolicyFactory = _requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.ManagedIdentityDefault); response = await _requestContext.ServiceBundle.HttpManager.SendRequestAsync( request.ComputeUri(), @@ -137,7 +139,7 @@ protected override async Task HandleResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: cancellationToken, - retryPolicy: defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); return await base.HandleResponseAsync(parameters, response, cancellationToken).ConfigureAwait(false); diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 57b35a6135..8f3409a01a 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -10,8 +10,8 @@ using Microsoft.Identity.Client.ApiConfig.Parameters; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -82,7 +82,7 @@ protected override ManagedIdentityRequest CreateRequest(string resource) break; } - request.RetryPolicy = new ImdsRetryPolicy(); + request.RequestType = RequestType.Imds; return request; } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index edd2590104..445c1e7ed0 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -7,6 +7,7 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Utils; using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.ManagedIdentity { @@ -22,18 +23,16 @@ internal class ManagedIdentityRequest public IDictionary QueryParameters { get; } - public IRetryPolicy RetryPolicy { get; set; } + public RequestType RequestType { get; set; } - public ManagedIdentityRequest(HttpMethod method, Uri endpoint, IRetryPolicy retryPolicy = null) + public ManagedIdentityRequest(HttpMethod method, Uri endpoint, RequestType requestType = RequestType.ManagedIdentityDefault) { Method = method; _baseEndpoint = endpoint; Headers = new Dictionary(); BodyParameters = new Dictionary(); QueryParameters = new Dictionary(); - - IRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.ManagedIdentity); - RetryPolicy = retryPolicy ?? defaultRetryPolicy; + RequestType = requestType; } public Uri ComputeUri() diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 5c258c5623..d2795c1d8e 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -80,6 +80,8 @@ + + @@ -163,4 +165,9 @@ + + + + + diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index ab8c063617..756d2b688e 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -15,11 +15,9 @@ using Microsoft.Identity.Client.Instance.Oidc; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; -using Microsoft.Identity.Client.Internal.Broker; using System.Security.Cryptography.X509Certificates; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; using Microsoft.Identity.Client.Http.Retry; - +using static Microsoft.Identity.Client.Internal.Constants; #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; @@ -122,7 +120,8 @@ internal async Task ExecuteRequestAsync( using (requestContext.Logger.LogBlockDuration($"[Oauth2Client] Sending {method} request ")) { - DefaultRetryPolicy defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); + IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); try { @@ -146,7 +145,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: _mtlsCertificate, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); } else @@ -161,7 +160,7 @@ internal async Task ExecuteRequestAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); } } diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index a79fe21c05..7f758c0795 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -13,14 +13,13 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Client.WsTrust { internal class WsTrustWebRequestManager : IWsTrustWebRequestManager { private readonly IHttpManager _httpManager; - private readonly DefaultRetryPolicy _defaultRetryPolicy = new DefaultRetryPolicy(RequestType.STS); public WsTrustWebRequestManager(IHttpManager httpManager) { @@ -43,6 +42,9 @@ public async Task GetMexDocumentAsync(string federationMetadataUrl, var uri = new UriBuilder(federationMetadataUrl); + IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); + HttpResponse httpResponse = await _httpManager.SendRequestAsync( uri.Uri, msalIdParams, @@ -53,7 +55,7 @@ public async Task GetMexDocumentAsync(string federationMetadataUrl, mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode != System.Net.HttpStatusCode.OK) @@ -101,6 +103,9 @@ public async Task GetWsTrustResponseAsync( wsTrustRequest, Encoding.UTF8, "application/soap+xml"); + IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); + HttpResponse resp = await _httpManager.SendRequestAsync( wsTrustEndpoint.Uri, headers, @@ -111,7 +116,7 @@ public async Task GetWsTrustResponseAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); if (resp.StatusCode != System.Net.HttpStatusCode.OK) @@ -178,6 +183,9 @@ public async Task GetUserRealmAsync( var uri = new UriBuilder(userRealmUriPrefix + userName + "?api-version=1.0").Uri; + IRetryPolicyFactory retryPolicyFactory = requestContext.ServiceBundle.Config.RetryPolicyFactory; + IRetryPolicy retryPolicy = retryPolicyFactory.GetRetryPolicy(RequestType.STS); + var httpResponse = await _httpManager.SendRequestAsync( uri, msalIdParams, @@ -188,7 +196,7 @@ public async Task GetUserRealmAsync( mtlsCertificate: null, validateServerCertificate: null, cancellationToken: requestContext.UserCancellationToken, - retryPolicy: _defaultRetryPolicy) + retryPolicy: retryPolicy) .ConfigureAwait(false); if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK) diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index 960b026375..dc92c3343a 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -11,47 +11,25 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; +using static Microsoft.Identity.Client.Internal.Constants; namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests { [TestClass] public class HttpManagerTests { - private static int _originalStsRetryDelay; - - [ClassInitialize] - public static void ClassInitialize(TestContext _) - { - // Backup original retry delay values - _originalStsRetryDelay = DefaultRetryPolicy.DefaultStsRetryDelayMs; - - // Speed up retry delays by 100x - DefaultRetryPolicy.DefaultStsRetryDelayMs = (int)(_originalStsRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); - } - - [ClassCleanup] - public static void ClassCleanup() - { - // Restore retry policy values after each test - DefaultRetryPolicy.DefaultStsRetryDelayMs = _originalStsRetryDelay; - } - - private DefaultRetryPolicy StsRetryPolicy; + private readonly TestDefaultRetryPolicy _stsRetryPolicy = new TestDefaultRetryPolicy(RequestType.STS); [TestInitialize] public void TestInitialize() { TestCommon.ResetInternalStaticCaches(); - - DefaultRetryPolicy.NumRetries = 0; - StsRetryPolicy = new DefaultRetryPolicy(RequestType.STS); } [TestMethod] @@ -86,7 +64,7 @@ public async Task MtlsCertAsync() mtlsCertificate: cert, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -126,7 +104,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: cert, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); } } @@ -163,7 +141,7 @@ public async Task TestHttpManagerWithValidationCallbackAsync() mtlsCertificate: null, validateServerCert: customCallback, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -191,7 +169,7 @@ public async Task TestSendPostNullHeaderNullBodyAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -233,7 +211,7 @@ public async Task TestSendPostNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -265,7 +243,7 @@ public async Task TestSendGetNoFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.IsNotNull(response); @@ -301,7 +279,7 @@ await Assert.ThrowsExceptionAsync(() => mtlsCertificate: null, validateServerCert: null, cancellationToken: cts.Token, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); } } @@ -324,11 +302,13 @@ public async Task TestSendGetWithHttp500TypeFailureWithInternalRetriesDisabledAs mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); - Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -338,8 +318,8 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) - for (int i = 0; i < NumErrors; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultStsMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.GatewayTimeout); } @@ -355,12 +335,12 @@ public async Task TestSendGetWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); - Assert.AreEqual(MsalError.ServiceNotAvailable, ex.ErrorCode); - Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); + + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } @@ -388,12 +368,13 @@ public async Task NoResiliencyIfRetryAfterHeaderPresentAsync(bool useTimeSpanRet mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); - - Assert.AreEqual(0, httpManager.QueueSize, "HttpManager must not retry because a RetryAfter header is present"); Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -414,12 +395,13 @@ public async Task NoResiliencyIfHttpErrorNotRetriableAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); - Assert.AreEqual(HttpStatusCode.BadRequest, msalHttpResponse.StatusCode); - Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); + + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -429,8 +411,8 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) - for (int i = 0; i < NumErrors; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultStsMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Get, HttpStatusCode.BadGateway); } @@ -445,12 +427,13 @@ public async Task TestSendGetWithHttp500TypeFailure2Async() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy) + retryPolicy: _stsRetryPolicy) .ConfigureAwait(false); Assert.AreEqual(HttpStatusCode.BadGateway, msalHttpResponse.StatusCode); - Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); + + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } @@ -460,8 +443,8 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) - for (int i = 0; i < NumErrors; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultStsMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddResiliencyMessageMockHandler(HttpMethod.Post, HttpStatusCode.ServiceUnavailable); } @@ -477,12 +460,12 @@ public async Task TestSendPostWithHttp500TypeFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); - Assert.AreEqual(MsalError.ServiceNotAvailable, exc.ErrorCode); - Assert.AreEqual(httpManager.QueueSize, 0); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultStsMaxRetries); + + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } @@ -492,8 +475,8 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) - for (int i = 0; i < NumErrors; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultStsMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Get); } @@ -509,12 +492,13 @@ public async Task TestSendGetWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); - Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } @@ -524,8 +508,8 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() using (var httpManager = new MockHttpManager()) { // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultStsMaxRetries + 1; // initial request + maximum number of retries (1) - for (int i = 0; i < NumErrors; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultStsMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddRequestTimeoutResponseMessageMockHandler(HttpMethod.Post); } @@ -541,11 +525,13 @@ public async Task TestSendPostWithRetryOnTimeoutFailureAsync() mtlsCertificate: null, validateServerCert: null, cancellationToken: default, - retryPolicy: StsRetryPolicy)) + retryPolicy: _stsRetryPolicy)) .ConfigureAwait(false); Assert.AreEqual(MsalError.RequestTimeout, exc.ErrorCode); Assert.IsTrue(exc.InnerException is TaskCanceledException); - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); + + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs new file mode 100644 index 0000000000..f180a209d5 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Identity.Client.Http.Retry; +using static Microsoft.Identity.Client.Internal.Constants; + +namespace Microsoft.Identity.Test.Unit.Helpers +{ + internal class TestDefaultRetryPolicy : DefaultRetryPolicy + { + public TestDefaultRetryPolicy(RequestType requestType) : base(requestType) { } + + internal override Task DelayAsync(int milliseconds) + { + // No delay for tests + return Task.CompletedTask; + } + } + + internal class TestImdsRetryPolicy : ImdsRetryPolicy + { + public TestImdsRetryPolicy() : base() { } + + internal override Task DelayAsync(int milliseconds) + { + // No delay for tests + return Task.CompletedTask; + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs new file mode 100644 index 0000000000..2f5f67862b --- /dev/null +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.Identity.Client.Http.Retry; +using static Microsoft.Identity.Client.Internal.Constants; + +namespace Microsoft.Identity.Test.Unit.Helpers +{ + internal class TestRetryPolicyFactory : IRetryPolicyFactory + { + public virtual IRetryPolicy GetRetryPolicy(RequestType requestType) + { + switch (requestType) + { + case RequestType.STS: + case RequestType.ManagedIdentityDefault: + return new TestDefaultRetryPolicy(requestType); + case RequestType.Imds: + return new TestImdsRetryPolicy(); + default: + throw new ArgumentOutOfRangeException(nameof(requestType), requestType, "Unknown request type."); + } + } + } +} diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs index b30de4792f..f563a34fe9 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/DefaultRetryPolicyTests.cs @@ -2,15 +2,14 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; @@ -24,32 +23,7 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class DefaultRetryPolicyTests : TestBase { - private static int s_originalManagedIdentityRetryDelay; - - [ClassInitialize] - public static void ClassInitialize(TestContext _) - { - // Backup original retry delay values - s_originalManagedIdentityRetryDelay = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; - - // Speed up retry delays by 100x - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(s_originalManagedIdentityRetryDelay * TestConstants.ONE_HUNDRED_TIMES_FASTER); - } - - [ClassCleanup] - public static void ClassCleanup() - { - // Restore retry policy values after each test - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = s_originalManagedIdentityRetryDelay; - } - - [TestInitialize] - public override void TestInitialize() - { - base.TestInitialize(); - - DefaultRetryPolicy.NumRetries = 0; - } + private readonly TestRetryPolicyFactory _testRetryPolicyFactory = new TestRetryPolicyFactory(); [DataTestMethod] // see test class header: all sources that allow UAMI [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] @@ -69,7 +43,8 @@ public async Task UAMIFails500OnceThenSucceeds200Async( ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -95,23 +70,15 @@ public async Task UAMIFails500OnceThenSucceeds200Async( userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // linear backoff (1 second * 1 retry) - Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); - + AuthenticationResult result = + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + + const int NumRequests = 2; // initial request + 1 retry + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -133,7 +100,8 @@ public async Task UAMIFails500PermanentlyAsync( ManagedIdentityId managedIdentityId = ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -141,8 +109,8 @@ public async Task UAMIFails500PermanentlyAsync( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 500s (to trigger the maximum number of retries) - const int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) + const int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultManagedIdentityMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -155,38 +123,43 @@ public async Task UAMIFails500PermanentlyAsync( } MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // linear backoff (1 second * 3 retries) - Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs} * {DefaultRetryPolicy.DefaultManagedIdentityMaxRetries}"); - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs * DefaultRetryPolicy.DefaultManagedIdentityMaxRetries)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } - [DataTestMethod] // see test class header: all sources allow SAMI - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( + [DataTestMethod] + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint, null)] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint, null)] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint, null)] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint, null)] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint, null)] + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint, "3")] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint, "3")] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint, "3")] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint, "3")] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint, "3")] + [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint, "date")] + [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint, "date")] + [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint, "date")] + [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint, "date")] + [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint, "date")] + public async Task SAMIFails500OnceWithVariousRetryAfterHeaderValuesThenSucceeds200Async( ManagedIdentitySource managedIdentitySource, - string endpoint) + string endpoint, + string retryAfterHeader) { using (new EnvVariableContext()) using (var httpManager = new MockHttpManager()) @@ -194,74 +167,14 @@ public async Task SAMIFails500OnceWithNoRetryAfterHeaderThenSucceeds200Async( SetEnvironmentVariables(managedIdentitySource, endpoint); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; IManagedIdentityApplication mi = miBuilder.Build(); - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); - - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); - - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // linear backoff (1 second * 1 retry) - Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs}"); - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs); - - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); - - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } - } - - [DataTestMethod] // see test class header: all sources allow SAMI - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Async( - ManagedIdentitySource managedIdentitySource, - string endpoint) - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager()) - { - SetEnvironmentVariables(managedIdentitySource, endpoint); - - ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - IManagedIdentityApplication mi = miBuilder.Build(); - - // make it one hundred times faster so the test completes quickly - double retryAfterSeconds = 3 * TestConstants.ONE_HUNDRED_TIMES_FASTER; - // Initial request fails with 500 httpManager.AddManagedIdentityMockHandler( endpoint, @@ -269,7 +182,7 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy "", managedIdentitySource, statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterSeconds.ToString()); + retryAfterHeader: retryAfterHeader == "date" ? DateTime.UtcNow.AddSeconds(3).ToString("R") : retryAfterHeader); // Final success httpManager.AddManagedIdentityMockHandler( @@ -278,87 +191,15 @@ public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsThenSucceeds200Asy MockHelpers.GetMsiSuccessfulResponse(), managedIdentitySource); - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {retryAfterSeconds} * 1000"); - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (retryAfterSeconds * 1000)); // convert to milliseconds - - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); - + AuthenticationResult result = + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); - } - } - [DataTestMethod] // see test class header: all sources allow SAMI - [DataRow(ManagedIdentitySource.AppService, TestConstants.AppServiceEndpoint)] - [DataRow(ManagedIdentitySource.AzureArc, TestConstants.AzureArcEndpoint)] - [DataRow(ManagedIdentitySource.CloudShell, TestConstants.CloudShellEndpoint)] - [DataRow(ManagedIdentitySource.MachineLearning, TestConstants.MachineLearningEndpoint)] - [DataRow(ManagedIdentitySource.ServiceFabric, TestConstants.ServiceFabricEndpoint)] - public async Task SAMIFails500OnceWithRetryAfterHeader3SecondsAsHttpDateThenSucceeds200Async( - ManagedIdentitySource managedIdentitySource, - string endpoint) - { - using (new EnvVariableContext()) - using (var httpManager = new MockHttpManager()) - { - SetEnvironmentVariables(managedIdentitySource, endpoint); - - ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); - - // Disable cache to avoid pollution - miBuilder.Config.AccessorOptions = null; - - IManagedIdentityApplication mi = miBuilder.Build(); - - // this test can not be made one hundred times faster because it is based on a date - const int retryAfterMilliseconds = 3000; - // an extra second has been added to account for this date operation - var retryAfterHttpDate = DateTime.UtcNow.AddMilliseconds(retryAfterMilliseconds + 1000).ToString("R"); - - // Initial request fails with 500 - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - "", - managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError, - retryAfterHeader: retryAfterHttpDate); - - // Final success - httpManager.AddManagedIdentityMockHandler( - endpoint, - ManagedIdentityTests.Resource, - MockHelpers.GetMsiSuccessfulResponse(), - managedIdentitySource); - - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made - Console.WriteLine($"{stopwatch.ElapsedMilliseconds} >= {retryAfterMilliseconds}"); - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= retryAfterMilliseconds); - - // ensure that exactly 2 requests were made: initial request + 1 retry - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 1); - Assert.AreEqual(httpManager.QueueSize, 0); - - Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + const int NumRequests = 2; // initial request + 1 retry + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -378,7 +219,8 @@ public async Task SAMIFails500Permanently( SetEnvironmentVariables(managedIdentitySource, endpoint); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -386,8 +228,8 @@ public async Task SAMIFails500Permanently( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 500s (to trigger the maximum number of retries) - int NUM_500 = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_500; i++) + int Num500Errors = 1 + TestDefaultRetryPolicy.DefaultManagedIdentityMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num500Errors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -401,7 +243,8 @@ public async Task SAMIFails500Permanently( try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -409,9 +252,8 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that the first request was made and retried 3 times - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + int requestsMade = Num500Errors - httpManager.QueueSize; + Assert.AreEqual(Num500Errors, requestsMade); } } @@ -431,7 +273,8 @@ public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy SetEnvironmentVariables(managedIdentitySource, endpoint); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -449,7 +292,8 @@ public async Task SAMIFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -457,9 +301,9 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -479,7 +323,8 @@ public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( SetEnvironmentVariables(managedIdentitySource, endpoint); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -497,7 +342,8 @@ public async Task SAMIFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -505,9 +351,9 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that only the initial request was made - Assert.AreEqual(DefaultRetryPolicy.NumRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs index 5c069ba023..13e22314af 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ImdsTests.cs @@ -2,15 +2,14 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; using System.Net; using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; @@ -19,44 +18,7 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests [TestClass] public class ImdsTests : TestBase { - private static int s_originalMinBackoff; - private static int s_originalMaxBackoff; - private static int s_originalDeltaBackoff; - private static int s_originalGoneRetryAfter; - - [ClassInitialize] - public static void ClassInitialize(TestContext _) - { - // Backup original retry delay values - s_originalMinBackoff = ImdsRetryPolicy.MinExponentialBackoffMs; - s_originalMaxBackoff = ImdsRetryPolicy.MaxExponentialBackoffMs; - s_originalDeltaBackoff = ImdsRetryPolicy.ExponentialDeltaBackoffMs; - s_originalGoneRetryAfter = ImdsRetryPolicy.HttpStatusGoneRetryAfterMs; - - // Speed up retry delays by 100x - ImdsRetryPolicy.MinExponentialBackoffMs = (int)(s_originalMinBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.MaxExponentialBackoffMs = (int)(s_originalMaxBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.ExponentialDeltaBackoffMs = (int)(s_originalDeltaBackoff * TestConstants.ONE_HUNDRED_TIMES_FASTER); - ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = (int)(s_originalGoneRetryAfter * TestConstants.ONE_HUNDRED_TIMES_FASTER); - } - - [ClassCleanup] - public static void ClassCleanup() - { - // Restore retry policy values after each test - ImdsRetryPolicy.MinExponentialBackoffMs = s_originalMinBackoff; - ImdsRetryPolicy.MaxExponentialBackoffMs = s_originalMaxBackoff; - ImdsRetryPolicy.ExponentialDeltaBackoffMs = s_originalDeltaBackoff; - ImdsRetryPolicy.HttpStatusGoneRetryAfterMs = s_originalGoneRetryAfter; - } - - [TestInitialize] - public override void TestInitialize() - { - base.TestInitialize(); - - ImdsRetryPolicy.NumRetries = 0; - } + private readonly TestRetryPolicyFactory _testRetryPolicyFactory = new TestRetryPolicyFactory(); [DataTestMethod] [DataRow(null, null)] // SAMI @@ -74,7 +36,8 @@ public async Task ImdsFails404TwiceThenSucceeds200Async( ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -82,8 +45,8 @@ public async Task ImdsFails404TwiceThenSucceeds200Async( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate two 404s (to trigger retries), then a successful response - const int NUM_404 = 2; - for (int i = 0; i < NUM_404; i++) + const int Num404Errors = 2; + for (int i = 0; i < Num404Errors; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -104,23 +67,15 @@ public async Task ImdsFails404TwiceThenSucceeds200Async( userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // exponential backoff (1 second -> 2 seconds) - const int ImdsExponentialStrategyTwoRetriesInMs = 3000; - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyTwoRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 3 requests were made: initial request + 2 retries - Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_404); - Assert.AreEqual(httpManager.QueueSize, 0); - + AuthenticationResult result = + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + + const int NumRequests = 1 + Num404Errors; // initial request + 2 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -140,7 +95,8 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async( ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -148,8 +104,8 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate four 410s (to trigger retries), then a successful response - const int NUM_410 = 4; - for (int i = 0; i < NUM_410; i++) + const int Num410Errors = 4; + for (int i = 0; i < Num410Errors; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -170,22 +126,15 @@ public async Task ImdsFails410FourTimesThenSucceeds200Async( userAssignedId: userAssignedId, userAssignedIdentityId: userAssignedIdentityId); - var stopwatch = Stopwatch.StartNew(); - - AuthenticationResult result = await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync() - .ConfigureAwait(false); - - stopwatch.Stop(); - - // linear backoff (10 seconds * 4 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * NUM_410 * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 5 requests were made: initial request + 4 retries - Assert.AreEqual(ImdsRetryPolicy.NumRetries, NUM_410); - Assert.AreEqual(httpManager.QueueSize, 0); - + AuthenticationResult result = + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false); Assert.AreEqual(result.AccessToken, TestConstants.ATSecret); + + const int NumRequests = 1 + Num410Errors; // initial request + 4 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -205,7 +154,8 @@ public async Task ImdsFails410PermanentlyAsync( ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -213,8 +163,8 @@ public async Task ImdsFails410PermanentlyAsync( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 410s (to trigger the maximum number of retries) - const int NUM_410 = ImdsRetryPolicy.LinearStrategyNumRetries + 1; // initial request + maximum number of retries (7) - for (int i = 0; i < NUM_410; i++) + const int Num410Errors = 1 + TestImdsRetryPolicy.LinearStrategyNumRetries; // initial request + maximum number of retries + for (int i = 0; i < Num410Errors; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -227,25 +177,20 @@ public async Task ImdsFails410PermanentlyAsync( } MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // linear backoff (10 seconds * 7 retries) - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsRetryPolicy.HttpStatusGoneRetryAfterMs * ImdsRetryPolicy.LinearStrategyNumRetries * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 8 requests were made: initial request + 7 retries - Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.LinearStrategyNumRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + int requestsMade = Num410Errors - httpManager.QueueSize; + Assert.AreEqual(Num410Errors, requestsMade); } } @@ -265,7 +210,8 @@ public async Task ImdsFails504PermanentlyAsync( ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -273,8 +219,8 @@ public async Task ImdsFails504PermanentlyAsync( IManagedIdentityApplication mi = miBuilder.Build(); // Simulate permanent 504s (to trigger the maximum number of retries) - const int NUM_504 = ImdsRetryPolicy.ExponentialStrategyNumRetries + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NUM_504; i++) + const int Num504Errors = 1 + TestImdsRetryPolicy.ExponentialStrategyNumRetries; // initial request + maximum number of retries + for (int i = 0; i < Num504Errors; i++) { httpManager.AddManagedIdentityMockHandler( ManagedIdentityTests.ImdsEndpoint, @@ -287,26 +233,20 @@ public async Task ImdsFails504PermanentlyAsync( } MsalServiceException msalException = null; - var stopwatch = Stopwatch.StartNew(); try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { msalException = ex as MsalServiceException; } - stopwatch.Stop(); Assert.IsNotNull(msalException); - // exponential backoff (1 second -> 2 seconds -> 4 seconds) - const int ImdsExponentialStrategyMaxRetriesInMs = 7000; - Assert.IsTrue(stopwatch.ElapsedMilliseconds >= (ImdsExponentialStrategyMaxRetriesInMs * TestConstants.ONE_HUNDRED_TIMES_FASTER)); - - // ensure that exactly 4 requests were made: initial request + 3 retries - Assert.AreEqual(ImdsRetryPolicy.NumRetries, ImdsRetryPolicy.ExponentialStrategyNumRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + int requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); } } @@ -326,7 +266,8 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -346,7 +287,8 @@ public async Task ImdsFails400WhichIsNonRetriableAndRetryPolicyIsNotTriggeredAsy try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -354,9 +296,9 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); } } @@ -376,7 +318,8 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( ? ManagedIdentityId.SystemAssigned : ManagedIdentityId.WithUserAssignedClientId(userAssignedId); ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(managedIdentityId) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -396,7 +339,8 @@ public async Task ImdsFails500AndRetryPolicyIsDisabledAndNotTriggeredAsync( try { await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) - .ExecuteAsync().ConfigureAwait(false); + .ExecuteAsync() + .ConfigureAwait(false); } catch (Exception ex) { @@ -404,9 +348,94 @@ await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) } Assert.IsNotNull(msalException); - // ensure that only the initial request was made - Assert.AreEqual(ImdsRetryPolicy.NumRetries, 0); - Assert.AreEqual(httpManager.QueueSize, 0); + const int NumRequests = 1; // initial request + 0 retries + int requestsMade = NumRequests - httpManager.QueueSize; + Assert.AreEqual(NumRequests, requestsMade); + } + } + + [TestMethod] + + public async Task ImdsRetryPolicyLifeTimeIsPerRequestAsync() + { + using (new EnvVariableContext()) + using (var httpManager = new MockHttpManager()) + { + SetEnvironmentVariables(ManagedIdentitySource.Imds, TestConstants.ImdsEndpoint); + + ManagedIdentityApplicationBuilder miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); + + // Disable cache to avoid pollution + miBuilder.Config.AccessorOptions = null; + + IManagedIdentityApplication mi = miBuilder.Build(); + + // Simulate permanent errors (to trigger the maximum number of retries) + const int Num504Errors = 1 + TestImdsRetryPolicy.ExponentialStrategyNumRetries; // initial request + maximum number of retries + for (int i = 0; i < Num504Errors; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout); + } + + MsalServiceException ex = + await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); + Assert.IsNotNull(ex); + + int requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); + + for (int i = 0; i < Num504Errors; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout); + } + + ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); + Assert.IsNotNull(ex); + + // 3 retries (requestsMade would be 6 if retry policy was NOT per request) + requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); + + for (int i = 0; i < Num504Errors; i++) + { + httpManager.AddManagedIdentityMockHandler( + ManagedIdentityTests.ImdsEndpoint, + ManagedIdentityTests.Resource, + MockHelpers.GetMsiImdsErrorResponse(), + ManagedIdentitySource.Imds, + statusCode: HttpStatusCode.GatewayTimeout); + } + + ex = await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(ManagedIdentityTests.Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); + Assert.IsNotNull(ex); + + // 3 retries (requestsMade would be 9 if retry policy was NOT per request) + requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs index f4eba0630d..d8d385e848 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/ManagedIdentityTests.cs @@ -11,13 +11,13 @@ using System.Threading.Tasks; using Microsoft.Identity.Client; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Test.Common; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; +using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; @@ -39,32 +39,7 @@ public class ManagedIdentityTests : TestBase internal const string ExpectedErrorCode = "ErrorCode"; internal const string ExpectedCorrelationId = "Some GUID"; - private static int _originalDefaultManagedIdentityRetryDelayMs; - - [ClassInitialize] - public static void ClassInitialize(TestContext _) - { - // Backup original retry delay values - _originalDefaultManagedIdentityRetryDelayMs = DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs; - - // Speed up retry delays by 100x - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = (int)(_originalDefaultManagedIdentityRetryDelayMs * TestConstants.ONE_HUNDRED_TIMES_FASTER); - } - - [ClassCleanup] - public static void ClassCleanup() - { - // Restore retry delay values after all tests - DefaultRetryPolicy.DefaultManagedIdentityRetryDelayMs = _originalDefaultManagedIdentityRetryDelayMs; - } - - [TestInitialize] - public override void TestInitialize() - { - base.TestInitialize(); - - ImdsRetryPolicy.NumRetries = 0; - } + private readonly TestRetryPolicyFactory _testRetryPolicyFactory = new TestRetryPolicyFactory(); [DataTestMethod] [DataRow("http://127.0.0.1:41564/msi/token/", ManagedIdentitySource.AppService, ManagedIdentitySource.AppService)] @@ -1275,7 +1250,6 @@ public async Task MixedUserAndSystemAssignedManagedIdentityTestAsync() [DataRow(ManagedIdentitySource.AppService, AppServiceEndpoint, HttpStatusCode.GatewayTimeout)] [DataRow(ManagedIdentitySource.AzureArc, AzureArcEndpoint, HttpStatusCode.GatewayTimeout)] [DataRow(ManagedIdentitySource.CloudShell, CloudShellEndpoint, HttpStatusCode.GatewayTimeout)] - [DataRow(ManagedIdentitySource.Imds, ImdsEndpoint, HttpStatusCode.GatewayTimeout)] [DataRow(ManagedIdentitySource.MachineLearning, MachineLearningEndpoint, HttpStatusCode.GatewayTimeout)] [DataRow(ManagedIdentitySource.ServiceFabric, ServiceFabricEndpoint, HttpStatusCode.GatewayTimeout)] public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( @@ -1289,7 +1263,8 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( SetEnvironmentVariables(managedIdentitySource, endpoint); var miBuilder = ManagedIdentityApplicationBuilder.Create(ManagedIdentityId.SystemAssigned) - .WithHttpManager(httpManager); + .WithHttpManager(httpManager) + .WithRetryPolicyFactory(_testRetryPolicyFactory); // Disable cache to avoid pollution miBuilder.Config.AccessorOptions = null; @@ -1297,8 +1272,8 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( var mi = miBuilder.Build(); // Simulate permanent errors (to trigger the maximum number of retries) - const int NumErrors = DefaultRetryPolicy.DefaultManagedIdentityMaxRetries + 1; // initial request + maximum number of retries (3) - for (int i = 0; i < NumErrors; i++) + const int Num504Errors = 1 + TestDefaultRetryPolicy.DefaultManagedIdentityMaxRetries; // initial request + maximum number of retries + for (int i = 0; i < Num504Errors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, @@ -1308,52 +1283,58 @@ public async Task ManagedIdentityRetryPolicyLifeTimeIsPerRequestAsync( statusCode: statusCode); } - MsalServiceException ex = await Assert.ThrowsExceptionAsync(async () => - await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + MsalServiceException ex = + await Assert.ThrowsExceptionAsync(async () => + await mi.AcquireTokenForManagedIdentity(Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); Assert.IsNotNull(ex); - // 3 retries - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + int requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); - for (int i = 0; i < NumErrors; i++) + for (int i = 0; i < Num504Errors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, Resource, "", managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + statusCode: statusCode); } ex = await Assert.ThrowsExceptionAsync(async () => - await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + await mi.AcquireTokenForManagedIdentity(Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); Assert.IsNotNull(ex); - // 3 retries (DefaultRetryPolicy.numRetries would be 6 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + // 3 retries (requestsMade would be 6 if retry policy was NOT per request) + requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); - for (int i = 0; i < NumErrors; i++) + for (int i = 0; i < Num504Errors; i++) { httpManager.AddManagedIdentityMockHandler( endpoint, Resource, "", managedIdentitySource, - statusCode: HttpStatusCode.InternalServerError); + statusCode: statusCode); } ex = await Assert.ThrowsExceptionAsync(async () => - await mi.AcquireTokenForManagedIdentity(Resource) - .ExecuteAsync().ConfigureAwait(false)).ConfigureAwait(false); + await mi.AcquireTokenForManagedIdentity(Resource) + .ExecuteAsync() + .ConfigureAwait(false)) + .ConfigureAwait(false); Assert.IsNotNull(ex); - // 3 retries (DefaultRetryPolicy.numRetries would be 9 if retry policy was NOT per request) - Assert.AreEqual(DefaultRetryPolicy.NumRetries, DefaultRetryPolicy.DefaultManagedIdentityMaxRetries); - Assert.AreEqual(httpManager.QueueSize, 0); + // 3 retries (requestsMade would be 9 if retry policy was NOT per request) + requestsMade = Num504Errors - httpManager.QueueSize; + Assert.AreEqual(Num504Errors, requestsMade); } } From 5e3ad096b46cd58a537cca0490ac449afaad66a2 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:23:33 -0400 Subject: [PATCH 59/68] Moved RequestType enum from Constants to IRetryPolicyFactory (#5316) --- .../Http/Retry/DefaultRetryPolicy.cs | 2 +- .../Http/Retry/IRetryPolicyFactory.cs | 9 +++++++-- .../Http/Retry/RetryPolicyFactory.cs | 2 +- .../Instance/Region/RegionManager.cs | 2 +- .../Instance/Validation/AdfsAuthorityValidator.cs | 3 +-- .../Microsoft.Identity.Client/Internal/Constants.cs | 6 ------ .../ManagedIdentity/AzureArcManagedIdentitySource.cs | 3 +-- .../ManagedIdentity/ImdsManagedIdentitySource.cs | 2 +- .../ManagedIdentity/ManagedIdentityRequest.cs | 4 +--- .../Microsoft.Identity.Client/OAuth2/OAuth2Client.cs | 2 +- .../WsTrust/WsTrustWebRequestManager.cs | 2 +- .../CoreTests/HttpTests/HttpManagerTests.cs | 2 +- .../Helpers/TestRetryPolicies.cs | 2 +- .../Helpers/TestRetryPolicyFactory.cs | 2 +- 14 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs index 5169babf3b..09b291a6d8 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Http.Retry { diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs index f39e27828c..02d9a20ef4 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs @@ -1,12 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using static Microsoft.Identity.Client.Internal.Constants; - namespace Microsoft.Identity.Client.Http.Retry { internal interface IRetryPolicyFactory { + public enum RequestType + { + STS, + ManagedIdentityDefault, + Imds + } + IRetryPolicy GetRetryPolicy(RequestType requestType); } } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs index 829951feb8..700b29a9e9 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Http.Retry { diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index 49bc49caff..2b60a59802 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -14,7 +14,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Region { diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index 5f9ec09efb..f121e87aa4 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -9,8 +9,7 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Instance.Validation { diff --git a/src/client/Microsoft.Identity.Client/Internal/Constants.cs b/src/client/Microsoft.Identity.Client/Internal/Constants.cs index 4580217e03..d5cefd6f78 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Constants.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Constants.cs @@ -73,11 +73,5 @@ public static string FormatAdfsWebFingerUrl(string host, string resource) { return $"https://{host}/.well-known/webfinger?rel={DefaultRealm}&resource={resource}"; } - public enum RequestType - { - STS, - ManagedIdentityDefault, - Imds - } } } diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index 4fe14dff65..13ded7e335 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -12,8 +12,7 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Shared; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 8f3409a01a..694e1af9e6 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -11,7 +11,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index 445c1e7ed0..a4a72bd0f6 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Net.Http; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.DefaultRetryPolicy; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 756d2b688e..4c84386c3a 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -17,7 +17,7 @@ using Microsoft.Identity.Client.Utils; using System.Security.Cryptography.X509Certificates; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index 7f758c0795..b111df5ce6 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -13,7 +13,7 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.WsTrust { diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index dc92c3343a..c5665ef6b5 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -17,7 +17,7 @@ using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests { diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs index f180a209d5..ae1a7e6c09 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.Helpers { diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs index 2f5f67862b..ed8b46ab64 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs @@ -3,7 +3,7 @@ using System; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Internal.Constants; +using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.Helpers { From 960c9d97be2ff8624bf30672617e41db07511204 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 2 Jun 2025 16:16:23 -0400 Subject: [PATCH 60/68] Moved RetryPolicyFactory configuration from Managed Identity Builder to Base Abstract Builder --- .../BaseAbstractApplicationBuilder.cs | 19 +++++++++++++++++++ .../ManagedIdentityApplicationBuilder.cs | 17 ----------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index 2fc2fa6f95..fec690bb97 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -14,6 +14,8 @@ using Microsoft.Identity.Client.Utils; using Microsoft.IdentityModel.Abstractions; using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.Http.Retry; + #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; #else @@ -31,6 +33,12 @@ public abstract class BaseAbstractApplicationBuilder internal BaseAbstractApplicationBuilder(ApplicationConfiguration configuration) { Config = configuration; + + // Ensure the default retry policy factory is set if the test factory was not provided + if (Config.RetryPolicyFactory == null) + { + Config.RetryPolicyFactory = new RetryPolicyFactory(); + } } internal ApplicationConfiguration Config { get; } @@ -323,5 +331,16 @@ internal static string GetValueIfNotEmpty(string original, string value) { return string.IsNullOrWhiteSpace(value) ? original : value; } + + /// + /// Internal only: Allows tests to inject a custom retry policy factory. + /// + /// The retry policy factory to use. + /// The builder for chaining. + internal T WithRetryPolicyFactory(IRetryPolicyFactory factory) + { + Config.RetryPolicyFactory = factory; + return (T)this; + } } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs index 0308b1c266..69ed5a8a1a 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs @@ -57,12 +57,6 @@ private static ApplicationConfiguration BuildConfiguration(ManagedIdentityId man config.CacheSynchronizationEnabled = false; config.AccessorOptions = CacheOptions.EnableSharedCacheOptions; - // Ensure the default retry policy factory is set if the test factory was not provided - if (config.RetryPolicyFactory == null) - { - config.RetryPolicyFactory = new RetryPolicyFactory(); - } - return config; } @@ -82,17 +76,6 @@ internal ManagedIdentityApplicationBuilder WithAppTokenCacheInternalForTest(ITok return this; } - /// - /// Internal only: Allows tests to inject a custom retry policy factory. - /// - /// The retry policy factory to use. - /// The builder for chaining. - internal ManagedIdentityApplicationBuilder WithRetryPolicyFactory(IRetryPolicyFactory factory) - { - Config.RetryPolicyFactory = factory; - return this; - } - /// /// Microsoft Identity specific OIDC extension that allows resource challenges to be resolved without interaction. /// Allows configuration of one or more client capabilities, e.g. "llt" From 94dfbdd99a108c8ade6587c82e0ee5b5bd654e37 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 2 Jun 2025 16:17:42 -0400 Subject: [PATCH 61/68] Removed another piece that was left out in previous committ --- .../AppConfig/ManagedIdentityApplicationBuilder.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs index 69ed5a8a1a..c4f8762862 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Linq; using Microsoft.Identity.Client.AppConfig; -using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.IdentityModel.Abstractions; @@ -110,12 +109,6 @@ internal ManagedIdentityApplication BuildConcrete() { DefaultConfiguration(); - // Ensure the default retry policy factory is set if the test factory was not provided - if (Config.RetryPolicyFactory == null) - { - Config.RetryPolicyFactory = new RetryPolicyFactory(); - } - return new ManagedIdentityApplication(BuildConfiguration()); } From 7b69caac1169150e546c2cb90c1fc89bf11a4f5c Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 2 Jun 2025 16:20:53 -0400 Subject: [PATCH 62/68] undid changes in ManagedIdentityBuilder --- .../AppConfig/ManagedIdentityApplicationBuilder.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs index c4f8762862..434d2764ce 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ManagedIdentityApplicationBuilder.cs @@ -5,8 +5,15 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.TelemetryCore; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; +using Microsoft.Identity.Client.Utils; using Microsoft.IdentityModel.Abstractions; namespace Microsoft.Identity.Client @@ -53,6 +60,7 @@ private static ApplicationConfiguration BuildConfiguration(ManagedIdentityId man var config = new ApplicationConfiguration(MsalClientType.ManagedIdentityClient); config.ManagedIdentityId = managedIdentityId; + config.CacheSynchronizationEnabled = false; config.AccessorOptions = CacheOptions.EnableSharedCacheOptions; @@ -108,7 +116,6 @@ public IManagedIdentityApplication Build() internal ManagedIdentityApplication BuildConcrete() { DefaultConfiguration(); - return new ManagedIdentityApplication(BuildConfiguration()); } From 126ccc4de56e7d99cf653aeb700ca2576d11b2a7 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Mon, 2 Jun 2025 17:16:07 -0400 Subject: [PATCH 63/68] Added RetryPolicyFactory to test mock --- tests/Microsoft.Identity.Test.Common/TestCommon.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Microsoft.Identity.Test.Common/TestCommon.cs b/tests/Microsoft.Identity.Test.Common/TestCommon.cs index 95772c3da7..4411a37492 100644 --- a/tests/Microsoft.Identity.Test.Common/TestCommon.cs +++ b/tests/Microsoft.Identity.Test.Common/TestCommon.cs @@ -31,6 +31,7 @@ using Microsoft.Identity.Test.Common.Core.Mocks; using NSubstitute; using static Microsoft.Identity.Client.TelemetryCore.Internal.Events.ApiEvent; +using Microsoft.Identity.Client.Http.Retry; namespace Microsoft.Identity.Test.Common { @@ -90,7 +91,8 @@ public static IServiceBundle CreateServiceBundleWithCustomHttpManager( LegacyCacheCompatibilityEnabled = isLegacyCacheEnabled, MultiCloudSupportEnabled = isMultiCloudSupportEnabled, IsInstanceDiscoveryEnabled = isInstanceDiscoveryEnabled, - PlatformProxy = platformProxy + PlatformProxy = platformProxy, + RetryPolicyFactory = new RetryPolicyFactory() }; return new ServiceBundle(appConfig, clearCaches); } From d92059a8b08097cffccb9e27b7fee4af041929fa Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 4 Jun 2025 14:04:46 -0400 Subject: [PATCH 64/68] Implemented Neha's feedback --- src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs | 2 ++ .../Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs index 194a42eed4..6320e9bfcc 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs @@ -7,6 +7,8 @@ namespace Microsoft.Identity.Client.Http.Retry { + // Interface for implementing retry logic for HTTP requests. + // Determines if a retry should occur and handles pause logic between retries. internal interface IRetryPolicy { Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger); diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs index 7cf64d5ecc..6ed44e0745 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/ImdsRetryPolicy.cs @@ -8,6 +8,7 @@ namespace Microsoft.Identity.Client.Http.Retry { + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/docs/imds_retry_based_on_errors.md internal class ImdsRetryPolicy : IRetryPolicy { // referenced in unit tests From 05d3d860955de1d01faf84c046c046be62a12fef Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 4 Jun 2025 14:14:03 -0400 Subject: [PATCH 65/68] Implemented Gladwin's feedback --- .../Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs | 2 +- .../Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs | 2 +- .../Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs | 2 +- tests/Microsoft.Identity.Test.Common/TestConstants.cs | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs index 09b291a6d8..58a4b657af 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs @@ -59,7 +59,7 @@ public async Task PauseForRetryAsync(HttpResponse response, Exception exce response.HeadersAsDictionary.TryGetValue("Retry-After", out retryAfter); } - int retryAfterDelay = _linearRetryStrategy.calculateDelay(retryAfter, _defaultRetryDelayMs); + int retryAfterDelay = _linearRetryStrategy.CalculateDelay(retryAfter, _defaultRetryDelayMs); logger.Warning($"Retrying request in {retryAfterDelay}ms (retry attempt: {retryCount + 1})"); diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs b/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs index 7d43515842..800a5280df 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/HttpRetryCondition.cs @@ -10,7 +10,7 @@ internal static class HttpRetryConditions { /// /// Retry policy specific to managed identity flow. - /// Avoid changing this, as it's breaking change. + /// Avoid changing this, as it's a breaking change. /// public static bool DefaultManagedIdentity(HttpResponse response, Exception exception) { diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs index 847f4f5266..e6fc10c797 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/LinearRetryStrategy.cs @@ -13,7 +13,7 @@ internal class LinearRetryStrategy /// The value of the `Retry-After` HTTP header. This can be either a number of seconds or an HTTP date string. /// The minimum delay in milliseconds to return if the header is not present or invalid. /// The number of milliseconds to sleep before retrying the request. - public int calculateDelay(string retryHeader, int minimumDelay) + public int CalculateDelay(string retryHeader, int minimumDelay) { if (string.IsNullOrEmpty(retryHeader)) { diff --git a/tests/Microsoft.Identity.Test.Common/TestConstants.cs b/tests/Microsoft.Identity.Test.Common/TestConstants.cs index f72bf902d1..eef1747403 100644 --- a/tests/Microsoft.Identity.Test.Common/TestConstants.cs +++ b/tests/Microsoft.Identity.Test.Common/TestConstants.cs @@ -205,8 +205,6 @@ public static HashSet s_scope public const string ImdsHost = "169.254.169.254"; public const string ImdsUrl = $"http://{ImdsHost}/metadata/instance/compute/location"; - public const double ONE_HUNDRED_TIMES_FASTER = 0.01; - public const string AppServiceEndpoint = "http://127.0.0.1:41564/msi/token"; public const string AzureArcEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; public const string CloudShellEndpoint = "http://localhost:40342/metadata/identity/oauth2/token"; From ce6349bed8f4aaa317cf850c2be583fd2c8b32fa Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 4 Jun 2025 14:23:24 -0400 Subject: [PATCH 66/68] undid changes to .csproj --- .../Microsoft.Identity.Client.csproj | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj index 0c3596869e..578bb27e45 100644 --- a/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj +++ b/src/client/Microsoft.Identity.Client/Microsoft.Identity.Client.csproj @@ -80,8 +80,6 @@ - - @@ -165,9 +163,4 @@ - - - - - From dbb531583660f3a0afc0b7eea5bc7f43b8aec582 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 4 Jun 2025 14:55:03 -0400 Subject: [PATCH 67/68] Implemented Travis's feedback --- .../AppConfig/ApplicationConfiguration.cs | 4 ++-- .../BaseAbstractApplicationBuilder.cs | 22 +++++++++---------- .../Http/Retry/IRetryPolicy.cs | 9 ++++++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 8f3f6d316f..ab19425b9e 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -125,6 +125,8 @@ public string ClientVersion public Func> AppTokenProvider; + internal IRetryPolicyFactory RetryPolicyFactory { get; set; } + #region ClientCredentials // Indicates if claims or assertions are used within the configuration @@ -208,7 +210,5 @@ public X509Certificate2 ClientCredentialCertificate public IDeviceAuthManager DeviceAuthManagerForTest { get; set; } public bool IsInstanceDiscoveryEnabled { get; internal set; } = true; #endregion - - internal IRetryPolicyFactory RetryPolicyFactory { get; set; } } } diff --git a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs index fec690bb97..b60ae2dbe0 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/BaseAbstractApplicationBuilder.cs @@ -235,6 +235,17 @@ public T WithClientVersion(string clientVersion) return this as T; } + /// + /// Internal only: Allows tests to inject a custom retry policy factory. + /// + /// The retry policy factory to use. + /// The builder for chaining. + internal T WithRetryPolicyFactory(IRetryPolicyFactory factory) + { + Config.RetryPolicyFactory = factory; + return (T)this; + } + internal virtual ApplicationConfiguration BuildConfiguration() { ResolveAuthority(); @@ -331,16 +342,5 @@ internal static string GetValueIfNotEmpty(string original, string value) { return string.IsNullOrWhiteSpace(value) ? original : value; } - - /// - /// Internal only: Allows tests to inject a custom retry policy factory. - /// - /// The retry policy factory to use. - /// The builder for chaining. - internal T WithRetryPolicyFactory(IRetryPolicyFactory factory) - { - Config.RetryPolicyFactory = factory; - return (T)this; - } } } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs index 6320e9bfcc..ea8c80b809 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicy.cs @@ -11,6 +11,15 @@ namespace Microsoft.Identity.Client.Http.Retry // Determines if a retry should occur and handles pause logic between retries. internal interface IRetryPolicy { + /// + /// Determines whether a retry should be attempted for a given HTTP response or exception, + /// and performs any necessary pause or delay logic before the next retry attempt. + /// + /// The HTTP response received from the request. + /// The exception encountered during the request. + /// The current retry attempt count. + /// The logger used for diagnostic and informational messages. + /// A task that returns true if a retry should be performed; otherwise, false. Task PauseForRetryAsync(HttpResponse response, Exception exception, int retryCount, ILoggerAdapter logger); } } From fdfdfb7f5b41c2a8200c7c1ef3bff591741c8568 Mon Sep 17 00:00:00 2001 From: Robbie Ginsburg Date: Wed, 4 Jun 2025 15:28:11 -0400 Subject: [PATCH 68/68] Moved RequestType enum to it's own file and updated imports --- .../Http/Retry/DefaultRetryPolicy.cs | 1 - .../Http/Retry/IRetryPolicyFactory.cs | 7 ----- .../Http/Retry/RetryPolicyFactory.cs | 1 - .../Instance/Region/RegionManager.cs | 1 - .../Validation/AdfsAuthorityValidator.cs | 1 - .../AzureArcManagedIdentitySource.cs | 1 - .../ImdsManagedIdentitySource.cs | 1 - .../ManagedIdentity/ManagedIdentityRequest.cs | 1 - .../OAuth2/OAuth2Client.cs | 1 - .../Microsoft.Identity.Client/RequestType.cs | 26 +++++++++++++++++++ .../WsTrust/WsTrustWebRequestManager.cs | 1 - .../CoreTests/HttpTests/HttpManagerTests.cs | 1 - .../Helpers/TestRetryPolicies.cs | 2 +- .../Helpers/TestRetryPolicyFactory.cs | 2 +- 14 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 src/client/Microsoft.Identity.Client/RequestType.cs diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs index 58a4b657af..63b06c0e77 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/DefaultRetryPolicy.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.Identity.Client.Core; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Http.Retry { diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs index 02d9a20ef4..c40d9c49b2 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/IRetryPolicyFactory.cs @@ -5,13 +5,6 @@ namespace Microsoft.Identity.Client.Http.Retry { internal interface IRetryPolicyFactory { - public enum RequestType - { - STS, - ManagedIdentityDefault, - Imds - } - IRetryPolicy GetRetryPolicy(RequestType requestType); } } diff --git a/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs index 700b29a9e9..dd62d4c886 100644 --- a/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs +++ b/src/client/Microsoft.Identity.Client/Http/Retry/RetryPolicyFactory.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Http.Retry { diff --git a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs index 2b60a59802..bcc3ad76e8 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Region/RegionManager.cs @@ -14,7 +14,6 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Region { diff --git a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs index f121e87aa4..585a172a38 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Validation/AdfsAuthorityValidator.cs @@ -9,7 +9,6 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.OAuth2; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.Instance.Validation { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs index 13ded7e335..8071a13944 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/AzureArcManagedIdentitySource.cs @@ -12,7 +12,6 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.PlatformsCommon.Shared; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs index 694e1af9e6..e4c6384103 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ImdsManagedIdentitySource.cs @@ -11,7 +11,6 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Http; using Microsoft.Identity.Client.Internal; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs index a4a72bd0f6..fb08b37822 100644 --- a/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs +++ b/src/client/Microsoft.Identity.Client/ManagedIdentity/ManagedIdentityRequest.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Net.Http; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.ManagedIdentity { diff --git a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs index 4c84386c3a..ae6384fe5f 100644 --- a/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs +++ b/src/client/Microsoft.Identity.Client/OAuth2/OAuth2Client.cs @@ -17,7 +17,6 @@ using Microsoft.Identity.Client.Utils; using System.Security.Cryptography.X509Certificates; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; #if SUPPORTS_SYSTEM_TEXT_JSON using System.Text.Json; diff --git a/src/client/Microsoft.Identity.Client/RequestType.cs b/src/client/Microsoft.Identity.Client/RequestType.cs new file mode 100644 index 0000000000..fe92431689 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/RequestType.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Client +{ + /// + /// Specifies the type of request being made to the identity provider. + /// + internal enum RequestType + { + /// + /// Security Token Service (STS) request, used for standard authentication flows. + /// + STS, + + /// + /// Managed Identity Default request, used when acquiring tokens for managed identities in Azure. + /// + ManagedIdentityDefault, + + /// + /// Instance Metadata Service (IMDS) request, used for obtaining tokens from the Azure VM metadata endpoint. + /// + Imds + } +} diff --git a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs index b111df5ce6..c7302c4c77 100644 --- a/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs +++ b/src/client/Microsoft.Identity.Client/WsTrust/WsTrustWebRequestManager.cs @@ -13,7 +13,6 @@ using Microsoft.Identity.Client.Http.Retry; using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Utils; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Client.WsTrust { diff --git a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs index c5665ef6b5..66cb40d66f 100644 --- a/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/CoreTests/HttpTests/HttpManagerTests.cs @@ -17,7 +17,6 @@ using Microsoft.Identity.Test.Unit.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.CoreTests.HttpTests { diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs index ae1a7e6c09..75e95f7e14 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicies.cs @@ -2,8 +2,8 @@ // Licensed under the MIT License. using System.Threading.Tasks; +using Microsoft.Identity.Client; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.Helpers { diff --git a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs index ed8b46ab64..9f5a656794 100644 --- a/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs +++ b/tests/Microsoft.Identity.Test.Unit/Helpers/TestRetryPolicyFactory.cs @@ -2,8 +2,8 @@ // Licensed under the MIT License. using System; +using Microsoft.Identity.Client; using Microsoft.Identity.Client.Http.Retry; -using static Microsoft.Identity.Client.Http.Retry.IRetryPolicyFactory; namespace Microsoft.Identity.Test.Unit.Helpers {