Skip to content

Commit f0dfcf2

Browse files
authored
Fix flaky certificate tests failing with InvalidOperationException (#1697)
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.
1 parent 044c1f1 commit f0dfcf2

1 file changed

Lines changed: 23 additions & 15 deletions

File tree

src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,11 +163,11 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica
163163
var optionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<CertificateOptions>>();
164164
optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509);
165165

166-
await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken);
167-
await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken);
168-
169-
using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken);
170-
await pollTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
166+
await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () =>
167+
{
168+
await File.WriteAllTextAsync(certificateFilePath, secondCertificateContent, TestContext.Current.CancellationToken);
169+
await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken);
170+
});
171171

172172
optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509);
173173
}
@@ -200,11 +200,11 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa
200200
var optionsMonitor = serviceProvider.GetRequiredService<IOptionsMonitor<CertificateOptions>>();
201201
optionsMonitor.Get(certificateName).Certificate.Should().BeEquivalentTo(firstX509);
202202

203-
appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key");
204-
await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken);
205-
206-
using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken);
207-
await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken);
203+
await WaitUntilCertificateChangedToAsync(certificateName, secondX509, optionsMonitor, async () =>
204+
{
205+
appSettings = BuildAppSettingsJson(certificateName, "secondInstance.crt", "secondInstance.key");
206+
await File.WriteAllTextAsync(appSettingsPath, appSettings, TestContext.Current.CancellationToken);
207+
});
208208

209209
optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509);
210210
}
@@ -235,13 +235,21 @@ private static string BuildAppSettingsJson(string certificateName, string certif
235235
""";
236236
}
237237

238-
private static async Task WaitUntilCertificateChangedToAsync(X509Certificate2 expectedCertificate, IOptionsMonitor<CertificateOptions> optionsMonitor,
239-
string certificateName, CancellationToken cancellationToken)
238+
private static async Task WaitUntilCertificateChangedToAsync(string certificateName, X509Certificate2 expectedCertificate,
239+
IOptionsMonitor<CertificateOptions> optionsMonitor, Func<Task> triggerAction)
240240
{
241-
while (!Equals(optionsMonitor.Get(certificateName).Certificate, expectedCertificate))
241+
var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
242+
243+
using IDisposable? changeListener = optionsMonitor.OnChange((options, name) =>
242244
{
243-
await Task.Delay(50, cancellationToken);
244-
}
245+
if (name == certificateName && Equals(options.Certificate, expectedCertificate))
246+
{
247+
completionSource.TrySetResult();
248+
}
249+
});
250+
251+
await triggerAction();
252+
await completionSource.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
245253
}
246254

247255
private static string GetConfigurationKey(string? optionName, string propertyName)

0 commit comments

Comments
 (0)