Merge branch 'claude/sftp-s3-connection-ui-b0730f'
ci / android head (push) Failing after 5s
ci / build and test (push) Successful in 1m23s
ci / api image (push) Successful in 24s

This commit is contained in:
2026-08-03 15:51:19 +02:00
5 changed files with 427 additions and 122 deletions
@@ -5314,6 +5314,67 @@ public sealed class ShellFlowTests : IAsyncLifetime
shell.Transfers.RemoteEntries.Select(entry => entry.Name).ShouldBe(["notes.txt"]);
}
/// <remarks>
/// <para>
/// The picker that replaced the desktop's connect bar, and the four things that put it away again. It
/// is a flag rather than a screen, so what is worth asserting is that it is never left open over a pane
/// it no longer belongs to: connecting closes it, disconnecting closes it, moving between SFTP and S3
/// closes it, and losing the vault closes it.
/// </para>
/// <para>
/// The last two are the ones that would rot quietly. A picker surviving a hop to the other tab offers
/// hosts on a screen showing buckets, and one surviving a lock offers a list that has just been emptied
/// because its rows carried decrypted secrets.
/// </para>
/// </remarks>
[Fact]
public async Task TheFilePickerIsPutAwayByEverythingThatChangesWhatItWouldBePicking()
{
var vault = await ReadyToConnectAsync();
shell.Transfers.Attach(vault, knownHosts);
shell.Transfers.IsChoosingRemote.ShouldBeFalse("the pane opens on its invitation");
// Connecting.
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
shell.Transfers.SelectedHost = shell.Transfers.Hosts[0];
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
shell.Transfers.IsConnected.ShouldBeTrue(shell.Transfers.Status);
shell.Transfers.IsChoosingRemote.ShouldBeFalse("what is open is the listing now");
// Disconnecting, which puts the pane back to the invitation rather than to the form.
await shell.Transfers.DisconnectCommand.ExecuteAsync(null);
shell.Transfers.IsConnected.ShouldBeFalse(shell.Transfers.Status);
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
// Moving to the other destination, which has a different list behind it.
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
shell.Transfers.ShowsBucketPicker.ShouldBeTrue();
shell.Transfers.IsChoosingRemote.ShouldBeFalse("that picker was offering hosts");
// Cancelling, which also takes the typed password with it.
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
shell.Transfers.TypedPassword = "hunter2";
shell.Transfers.CancelChoosingRemoteCommand.Execute(null);
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
shell.Transfers.TypedPassword.ShouldBeEmpty("a secret nobody asked to keep");
// And losing the vault, which empties both lists.
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
await shell.LockCommand.ExecuteAsync(null);
shell.Transfers.Hosts.ShouldBeEmpty();
shell.Transfers.IsChoosingRemote.ShouldBeFalse("a form offering a choice between nothing");
}
private async Task UnlockedAsync()
{
await EnrolledAndConfirmedAsync();