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; }
///
/// Holds a connection open until it is completed, when set.
///
///
/// A handshake takes as long as a network takes, and connecting is deliberately no longer allowed to
/// hold the window still while it does — so there is now behaviour that only exists during a
/// connection: a tab in the strip with no session behind it. This is how a test gets to look at that
/// moment rather than at the two on either side of it.
///
internal TaskCompletionSource? Gate { 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 async Task ConnectAsync(
SshConnectionRequest request,
CancellationToken cancellationToken)
{
Requests.Add(request);
if (Gate is { } gate)
{
await gate.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
}
return Failure is { } failure
? throw failure
: 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();
}
}