Public Access
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64.
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
using Avalonia.Threading;
|
||||
using DodoSSH.Client.Shell.ViewModels;
|
||||
using DodoSSH.Client.Ssh;
|
||||
using NSubstitute;
|
||||
|
||||
namespace DodoSSH.Client.App.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// What may be queued for transfer, and what is said about the rest.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is the whole of what drag and drop decides. The handlers on the screen extract paths or rows from a
|
||||
/// drop and hand them here; every rule about which of them can be moved, which are skipped and what the
|
||||
/// status line says lives in the view model, where it needs no window.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>What these cannot cover is the drag itself.</b> Headless Avalonia has no native window and cannot
|
||||
/// synthesise a platform drag, so a test that pretended to drop a file from the file manager would pass
|
||||
/// while confirming nothing. The wiring is verified by hand — see <c>docs/manual-checks.md</c> — and what
|
||||
/// is automated is the half that a person checking by eye would most easily get wrong: the counting.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class TransferQueueingTests : IDisposable
|
||||
{
|
||||
private readonly string directory =
|
||||
Path.Combine(Path.GetTempPath(), $"dodossh-drop-{Guid.CreateVersion7():N}");
|
||||
|
||||
private readonly TransfersViewModel transfers =
|
||||
new(Substitute.For<ISftpSessionFactory>(), TimeProvider.System);
|
||||
|
||||
public TransferQueueingTests() => Directory.CreateDirectory(directory);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(directory))
|
||||
{
|
||||
Directory.Delete(directory, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The refusal that has to happen before anything else: there is nowhere to put a file until a host is
|
||||
/// connected, and a queue that filled up first would start failing the moment one was.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void DroppingFilesWhileDisconnected_QueuesNothingAndSaysWhy()
|
||||
{
|
||||
transfers.IsConnected = false;
|
||||
|
||||
transfers.QueueUploads([File("one.txt")]);
|
||||
|
||||
Queued().ShouldBeEmpty();
|
||||
transfers.Status.ShouldContain("Connect to a host first");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DroppingSeveralFiles_QueuesEachOfThem()
|
||||
{
|
||||
Connected();
|
||||
|
||||
transfers.QueueUploads([File("one.txt"), File("two.txt"), File("three.txt")]);
|
||||
|
||||
Queued().Count.ShouldBe(3);
|
||||
transfers.Status.ShouldContain("3 files");
|
||||
transfers.Status.ShouldContain("/srv/app");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The queue moves files. There is no recursive upload, and a folder dragged in and silently ignored
|
||||
/// looks exactly like a transfer that failed to start — so it is counted and reported.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void DroppingAFolderAmongFiles_SkipsItAndSaysSo()
|
||||
{
|
||||
Connected();
|
||||
|
||||
var folder = Path.Combine(directory, "a-folder");
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
transfers.QueueUploads([File("one.txt"), folder]);
|
||||
|
||||
Queued().ShouldHaveSingleItem();
|
||||
transfers.Status.ShouldContain("1 file");
|
||||
transfers.Status.ShouldContain("1 folder was skipped");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The paths in an operating-system drop come from another process and are not obliged to still be
|
||||
/// right by the time the drop lands.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void DroppingAFileThatHasGone_SkipsItAndSaysSo()
|
||||
{
|
||||
Connected();
|
||||
|
||||
transfers.QueueUploads([File("one.txt"), Path.Combine(directory, "never-existed.txt")]);
|
||||
|
||||
Queued().ShouldHaveSingleItem();
|
||||
transfers.Status.ShouldContain("1 item was no longer there");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DroppingOnlyFolders_QueuesNothingAndDoesNotClaimOtherwise()
|
||||
{
|
||||
Connected();
|
||||
|
||||
var folder = Path.Combine(directory, "a-folder");
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
transfers.QueueUploads([folder]);
|
||||
|
||||
Queued().ShouldBeEmpty();
|
||||
transfers.Status.ShouldContain("Nothing was queued");
|
||||
transfers.Status.ShouldContain("1 folder was skipped");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DroppingRemoteRowsOnTheLocalPane_QueuesDownloads()
|
||||
{
|
||||
Connected();
|
||||
|
||||
transfers.QueueDownloads([RemoteFile("one.log"), RemoteFile("two.log")]);
|
||||
|
||||
Queued().Count.ShouldBe(2);
|
||||
transfers.Status.ShouldContain("2 files");
|
||||
transfers.Status.ShouldContain("download into");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DroppingARemoteDirectory_SkipsItAndSaysSo()
|
||||
{
|
||||
Connected();
|
||||
|
||||
transfers.QueueDownloads([RemoteFile("one.log"), RemoteDirectory("logs")]);
|
||||
|
||||
Queued().ShouldHaveSingleItem();
|
||||
transfers.Status.ShouldContain("1 folder was skipped");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The buttons were the only way to queue anything before drag and drop, and they now go through the
|
||||
/// same two methods — so there is one set of rules rather than two that have to agree. This is what
|
||||
/// says they still do.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void TheDownloadButton_GoesThroughTheSamePathAsADrop()
|
||||
{
|
||||
Connected();
|
||||
|
||||
transfers.SelectedRemoteEntry = RemoteFile("one.log");
|
||||
transfers.DownloadCommand.Execute(null);
|
||||
|
||||
Queued().ShouldHaveSingleItem();
|
||||
|
||||
// And refuses a directory in the same words, rather than with the button's own message.
|
||||
transfers.SelectedRemoteEntry = RemoteDirectory("logs");
|
||||
transfers.DownloadCommand.Execute(null);
|
||||
|
||||
Queued().Count.ShouldBe(1);
|
||||
transfers.Status.ShouldContain("1 folder was skipped");
|
||||
}
|
||||
|
||||
/// <summary>The queue's rows, once the posts that create them have been let run.</summary>
|
||||
/// <remarks>
|
||||
/// <c>TransfersViewModel</c> adds a row from the queue's own <c>Changed</c> event, which it marshals
|
||||
/// through <c>Dispatcher.UIThread</c> because the queue raises it from a pump thread. There is no
|
||||
/// Avalonia application here to drain that, so the posts are run by hand — the alternative is asserting
|
||||
/// on the status line alone, which is a string this code wrote about itself and proves nothing about
|
||||
/// anything having been enqueued.
|
||||
/// </remarks>
|
||||
private IReadOnlyList<TransferRowViewModel> Queued()
|
||||
{
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
|
||||
return transfers.Transfers;
|
||||
}
|
||||
|
||||
private void Connected()
|
||||
{
|
||||
transfers.IsConnected = true;
|
||||
transfers.RemotePath = "/srv/app";
|
||||
transfers.LocalPath = directory;
|
||||
}
|
||||
|
||||
private string File(string name)
|
||||
{
|
||||
var path = Path.Combine(directory, name);
|
||||
System.IO.File.WriteAllText(path, "contents");
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private static RemoteEntryRowViewModel RemoteFile(string name) => new(
|
||||
new SftpEntry(name, $"/srv/app/{name}", SftpEntryKind.File, 128, DateTimeOffset.UnixEpoch, "-rw-r--r--"));
|
||||
|
||||
private static RemoteEntryRowViewModel RemoteDirectory(string name) => new(
|
||||
new SftpEntry(name, $"/srv/app/{name}", SftpEntryKind.Directory, 0, DateTimeOffset.UnixEpoch, "drwxr-xr-x"));
|
||||
}
|
||||
Reference in New Issue
Block a user