From 13a8ace996cedc874414554adce25ace7c552ec9 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Wed, 13 May 2026 13:06:15 +0200 Subject: [PATCH] Fix flaky certificate tests failing with InvalidOperationException The tests used `using Task` on a poll task that was still running when the `WaitAsync` timeout expired, causing `Task.Dispose()` to throw `InvalidOperationException` instead of a meaningful timeout failure. Replace polling with `IOptionsMonitor.OnChange` and a `TaskCompletionSource`, registering the listener before triggering the file change to avoid race conditions. --- .../ConfigureCertificateOptionsTest.cs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs index f14de9eed7..080e005b87 100644 --- a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs +++ b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs @@ -163,11 +163,11 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica var optionsMonitor = serviceProvider.GetRequiredService>(); optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509); - await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken); - await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken); - - using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken); - await pollTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); + await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () => + { + await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken); + await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken); + }); optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509); } @@ -200,11 +200,11 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa var optionsMonitor = serviceProvider.GetRequiredService>(); optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509); - appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key"); - await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken); - - using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken); - await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () => + { + appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key"); + await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken); + }); optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509); } @@ -235,13 +235,21 @@ private static string BuildAppSettingsJson(string certificateName, string certif """; } - private static async Task WaitUntilCertificateChangedToAsync(X509Certificate2 expectedCertificate, IOptionsMonitor optionsMonitor, - string certificateName, CancellationToken cancellationToken) + private static async Task WaitUntilCertificateChangedToAsync(string certificateName, X509Certificate2 expectedCertificate, + IOptionsMonitor optionsMonitor, Func triggerAction) { - while (!Equals(optionsMonitor.Get(certificateName).Certificate, expectedCertificate)) + var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + using IDisposable? changeListener = optionsMonitor.OnChange((options, name) => { - await Task.Delay(50, cancellationToken); - } + if (name == certificateName && Equals(options.Certificate, expectedCertificate)) + { + completionSource.TrySetResult(); + } + }); + + await triggerAction(); + await completionSource.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); } private static string GetConfigurationKey(string? optionName, string propertyName)