using DodoSSH.Client.App.Platform; using DodoSSH.Client.Session; using NSubstitute; namespace DodoSSH.Client.App.Tests; /// /// That the slow probe choosing a device key store runs once, and not on the thread that asked. /// /// /// On Windows the probe creates and deletes a TPM key, and it used to run synchronously on the UI thread at /// startup and again after every unlock. See . /// public sealed class ProbedDeviceKeyStoreTests { private static CancellationToken Token => TestContext.Current.CancellationToken; /// /// "Off the caller's thread" is proved by blocking, not by comparing thread ids. The probe waits for a /// release the test only gives once construction has returned, so a probe run on the thread that /// constructed the store could never be released and times out instead. Thread ids proved nothing: the /// test itself runs on a pool thread, and that same thread is free to pick up the queued probe the /// moment the test awaits, which is what failed on CI with the code doing exactly the right thing. /// [Fact] public async Task TheChoiceIsMadeOnce_OffTheCallersThread() { var probes = 0; var releasedInTime = false; using var release = new ManualResetEventSlim(); var inner = Substitute.For(); var store = new ProbedDeviceKeyStore(() => { Interlocked.Increment(ref probes); releasedInTime = release.Wait(TimeSpan.FromSeconds(10)); return inner; }); var available = store.IsAvailableAsync(Token); available.IsCompleted.ShouldBeFalse("nothing has released the probe yet"); release.Set(); (await available).ShouldBeTrue(); (await store.IsAvailableAsync(Token)).ShouldBeTrue(); await store.TryLoadAsync(Token); releasedInTime.ShouldBeTrue("the probe ran on the thread that asked, so nothing could release it"); probes.ShouldBe(1); // Availability is the choice, not a second probe through the store it chose. await inner.DidNotReceive().IsAvailableAsync(Arg.Any()); await inner.Received(1).TryLoadAsync(Arg.Any()); } [Fact] public async Task AMachineWithNoStore_IsUnavailable_AndForgettingIsHarmless() { var store = new ProbedDeviceKeyStore(static () => new UnavailableDeviceKeyStore()); (await store.IsAvailableAsync(Token)).ShouldBeFalse(); (await store.TryLoadAsync(Token)).ShouldBeNull(); await store.ForgetAsync(Token); } }