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, ISftpSessionFactory { /// 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; } = []; /// Requests for a file-transfer session, in order. /// /// Kept apart from deliberately: file transfer is a separate connection, and a /// test asserting that opening a terminal did not also open one would have nothing to look at if the two /// shared a list. /// internal List SftpRequests { get; } = []; /// public Task ConnectAsync( SshConnectionRequest request, CancellationToken cancellationToken) { Requests.Add(request); return Failure is { } failure ? Task.FromException(failure) : Task.FromResult(new FakeSshConnection(request)); } /// public Task OpenSftpAsync( SshConnectionRequest request, CancellationToken cancellationToken) { SftpRequests.Add(request); return Failure is { } failure ? Task.FromException(failure) : Task.FromResult(new FakeSftpSession(request)); } } /// A remote filesystem with one directory in it. /// /// Enough for the shell suite, which is about what the screen does around a session rather than about /// moving bytes. The queue's own behaviour is covered against a fuller fake in /// DodoSSH.Client.Transfer.Tests, and the real subsystem against a container in /// DodoSSH.Client.Ssh.Tests. /// internal sealed class FakeSftpSession(SshConnectionRequest request) : ISftpSession { /// public bool IsConnected { get; private set; } = true; /// public HostKeyPresentation HostKey { get; } = new(request.Host, request.Port, "ssh-ed25519", "SHA256:fake"); /// public string HomeDirectory => $"/home/{request.Username}"; /// public Task> ListAsync(string path, CancellationToken cancellationToken) => Task.FromResult>( [ new SftpEntry( "notes.txt", SftpPath.Combine(path, "notes.txt"), SftpEntryKind.File, 12, DateTimeOffset.UnixEpoch, "-rw-r--r--"), ]); /// public Task StatAsync(string path, CancellationToken cancellationToken) => Task.FromResult(null); /// public Task OpenReadAsync(string path, long offset, CancellationToken cancellationToken) => Task.FromResult(new MemoryStream("hello there\n"u8.ToArray(), writable: false)); /// public Task OpenWriteAsync(string path, long offset, CancellationToken cancellationToken) => Task.FromResult(new MemoryStream()); /// public Task CreateDirectoryAsync(string path, CancellationToken cancellationToken) => Task.CompletedTask; /// public Task DeleteAsync(string path, CancellationToken cancellationToken) => Task.CompletedTask; /// public Task RenameAsync(string fromPath, string toPath, CancellationToken cancellationToken) => Task.CompletedTask; /// public ValueTask DisposeAsync() { IsConnected = false; return ValueTask.CompletedTask; } } 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(); } }