using DodoSSH.Client.Session;
namespace DodoSSH.Client.App.Tests;
///
/// A release channel that answers whatever the test tells it to.
///
///
///
/// records the call instead of making it, and that is the whole reason
/// IUpdateChannel exists as an interface. The real one replaces the running process and never
/// returns, so a test could not observe it at all — and the single most important thing to be able to
/// assert about this feature is a negative: that a downloaded update is never applied unless
/// somebody pressed the button. A counter is what makes that assertion writable.
///
///
/// exists for the same reason in the other direction: without a way to stop a
/// download halfway, the Downloading state would be a state no test could ever catch the view model in.
///
///
internal sealed class FakeUpdateChannel : IUpdateChannel
{
/// What the next check finds. Null means this build is current.
internal AvailableUpdate? Available { get; set; }
/// When set, checking throws it.
internal Exception? CheckFailure { get; set; }
/// When set, downloading throws it.
internal Exception? DownloadFailure { get; set; }
/// When set, applying throws it — after counting, since the call was still made.
///
/// The phone's install can fail on the spot rather than by ending the process: a session the platform
/// refuses, or an unknown-sources answer that changed under it. What that must not do is escape a
/// command handler, so this is what lets a test hold the view model to reporting it.
///
internal Exception? RestartFailure { get; set; }
/// When set, a download waits on it before completing.
internal TaskCompletionSource? HoldDownload { get; set; }
/// The percentages a download reports on its way through.
internal IReadOnlyList ProgressSteps { get; set; } = [25, 50, 100];
internal int Checks { get; private set; }
internal int Downloads { get; private set; }
internal int Restarts { get; private set; }
internal AvailableUpdate? RestartedWith { get; private set; }
///
public bool IsSupported { get; set; } = true;
/// Settable, because the two heads answer it differently and both paths need driving.
///
/// True is the desktop's: the shell disposes itself before applying. False is the phone's, where the
/// install can be declined — and the test that matters is that declining leaves a working session
/// rather than a torn-down one.
///
public bool ApplyingEndsTheProcess { get; set; } = true;
///
public string CurrentVersion { get; set; } = "1.0.0";
///
public Task CheckAsync(CancellationToken cancellationToken)
{
Checks++;
return CheckFailure is { } failure
? Task.FromException(failure)
: Task.FromResult(Available);
}
///
public async Task DownloadAsync(
AvailableUpdate update,
IProgress progress,
CancellationToken cancellationToken)
{
Downloads++;
if (DownloadFailure is { } failure)
{
throw failure;
}
if (HoldDownload is { } gate)
{
await gate.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}
foreach (var percent in ProgressSteps)
{
cancellationToken.ThrowIfCancellationRequested();
progress.Report(percent);
}
}
///
public void ApplyAndRestart(AvailableUpdate update)
{
Restarts++;
RestartedWith = update;
if (RestartFailure is { } failure)
{
throw failure;
}
}
}