diff --git a/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs b/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs index f2bb31139..9e06aa41e 100644 --- a/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CertificateRetryCounterTests.cs @@ -143,7 +143,7 @@ await Task.Run(async () => /// Regression test: Simulates the Agent Identities scenario where nested token acquisitions /// with bad config should fail quickly, not hang. /// - [Fact(Timeout = 5000)] // 5 second timeout - if it takes longer, it's likely hanging + [Fact(Timeout = 30000)] // 30 second timeout - generous to accommodate slow CI, but still validates no infinite hang public async Task GetAuthenticationResultForAppAsync_WithBadClientId_CompletesQuickly() { // Arrange diff --git a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs index d7f084287..353dc5283 100644 --- a/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs +++ b/tests/Microsoft.Identity.Web.Test/CertificatesObserverTests.cs @@ -2,14 +2,10 @@ // Licensed under the MIT License. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Runtime.ConstrainedExecution; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -53,8 +49,6 @@ static void RemoveCertificate(X509Certificate2? certificate) string certName = $"CN=TestCert-{Guid.NewGuid():N}"; cert1 = CreateAndInstallCertificate(certName); - // Verify certificate is properly installed in store with timeout - await VerifyCertificateInStoreAsync(cert1, TimeSpan.FromSeconds(5)); var description = new CredentialDescription { SourceType = CredentialSource.StoreWithDistinguishedName, @@ -134,9 +128,6 @@ static void RemoveCertificate(X509Certificate2? certificate) RemoveCertificate(cert1); cert2 = CreateAndInstallCertificate(certName); - // Verify certificate is properly installed in store with timeout - await VerifyCertificateInStoreAsync(cert2, TimeSpan.FromSeconds(5)); - // Rerun but it fails this time mockHttpFactory.ValidCertificates.Clear(); mockHttpFactory.ValidCertificates.Add(cert2); @@ -184,7 +175,8 @@ internal X509Certificate2 CreateAndInstallCertificate(string certName) var req = new CertificateRequest($"CN={certName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); #endif - var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddDays(1)); + // Backdate NotBefore to avoid time-boundary races with FindByTimeValid. + var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddMinutes(-5), DateTimeOffset.Now.AddDays(1)); byte[] bytes = cert.Export(X509ContentType.Pfx, (string?)null); #pragma warning disable SYSLIB0057 // Type or member is obsolete @@ -196,32 +188,30 @@ internal X509Certificate2 CreateAndInstallCertificate(string certName) x509Store.Add(certWithPrivateKey); x509Store.Close(); + // X509Store.Add is synchronous — verify the cert is immediately findable + // using the same DN-based lookup that production code uses. + VerifyCertificateInStore(certWithPrivateKey); + return certWithPrivateKey; } /// - /// Verifies that a certificate is properly installed in the certificate store. + /// Verifies that a certificate is installed in the store using the same + /// distinguished-name lookup that the production credential loader uses. + /// X509Store.Add is synchronous, so no polling is needed. /// - /// The certificate to verify. - /// Maximum time to wait for the certificate to appear in the store. - private static async Task VerifyCertificateInStoreAsync(X509Certificate2 certificate, TimeSpan timeout) + private static void VerifyCertificateInStore(X509Certificate2 certificate) { - var stopwatch = Stopwatch.StartNew(); - var minWaitTime = TimeSpan.FromSeconds(2); // Minimum wait to ensure store operations complete - do + using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + var found = store.Certificates + .Find(X509FindType.FindBySubjectDistinguishedName, certificate.SubjectName.Name, false); + if (found.Count == 0) { - using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); - store.Open(OpenFlags.ReadOnly); - var foundCerts = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, false); - if (foundCerts.Count > 0 && stopwatch.Elapsed >= minWaitTime) - { - return; // Certificate found and minimum wait time elapsed - } - - await Task.Delay(100); // Wait 100ms before checking again + throw new InvalidOperationException( + $"Test setup failure: certificate '{certificate.SubjectName.Name}' (thumbprint {certificate.Thumbprint}) " + + $"was not found in CurrentUser/My immediately after Add. The store may be in an unexpected state."); } - while (stopwatch.Elapsed < timeout); - throw new TimeoutException($"Certificate with thumbprint {certificate.Thumbprint} was not found in the certificate store within {timeout.TotalSeconds} seconds."); } private class TestCertificatesObserver : ICertificatesObserver