using DodoSSH.Client.Ssh; namespace DodoSSH.Client.App.Tests; /// /// An SSH stack that connects to nothing. /// /// /// The shell suite is about what the view models do, and the real factory would need a reachable /// sshd — which DodoSSH.Client.Ssh.Tests already covers against a container. What this makes /// testable is everything the connect path does around the connection. /// internal sealed class FakeSshConnectionFactory : ISshConnectionFactory { /// Thrown instead of connecting, when set. Used for the host-key paths. internal Exception? Failure { get; set; } /// Requests this factory was asked for, in order. internal List Requests { get; } = []; /// public Task ConnectAsync( SshConnectionRequest request, CancellationToken cancellationToken) { Requests.Add(request); return Failure is { } failure ? Task.FromException(failure) : Task.FromResult(new FakeSshConnection(request)); } } internal sealed class FakeSshConnection(SshConnectionRequest request) : ISshConnection { /// public bool IsConnected { get; private set; } = true; /// public HostKeyPresentation HostKey { get; } = new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake"); /// public Task OpenShellAsync( TerminalSize size, CancellationToken cancellationToken) => Task.FromResult(new FakeSshShellSession()); /// public ValueTask DisposeAsync() { IsConnected = false; return ValueTask.CompletedTask; } } /// A shell that is open, silent and never closes on its own. /// /// blocks rather than returning 0. Returning 0 means the remote closed the /// channel, which would end the session the moment it was opened and make the test assert against a /// connection that had already gone. /// internal sealed class FakeSshShellSession : ISshShellSession { private readonly CancellationTokenSource closed = new(); /// public bool IsOpen { get; private set; } = true; /// public async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { using var linked = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, closed.Token); await Task.Delay(Timeout.InfiniteTimeSpan, linked.Token).ConfigureAwait(false); return 0; } /// public ValueTask WriteAsync(ReadOnlyMemory data, CancellationToken cancellationToken) => ValueTask.CompletedTask; /// public void Resize(TerminalSize size) { } /// public async ValueTask DisposeAsync() { IsOpen = false; await closed.CancelAsync().ConfigureAwait(false); closed.Dispose(); } }