Public Access
Give the phone its pins: an editor section and chips on Files
The data was never the gap — HostSecret.PinnedPaths syncs and merges on both heads, and the desktop's drawer has staged it since v5 — the phone just had nowhere to add, remove or use a pin. Now it has both halves. The host editor page gains a QUICK ACCESS section over the same shared staging the drawer binds (EditorPinnedPaths, AddEditorPin, RemoveEditorPin), with the remove target at this head's 44dp touch floor rather than the desktop's 22-pixel close box, and no folder glyph because this head embeds no icon font for one. The page also gains a Status line of its own: the add command's five refusals speak through Status, and this page covers the screen that normally draws it — a refusal nothing shows is no refusal at all. The Files screen draws the connected host's pins as chips between the breadcrumb and the listing, each running GoRemoteCommand exactly as a crumb does. They are captured at connect, like ConnectedTo and the session facts before them; a bucket gets none, having no HostSecret to pin anything on. Covered headlessly in ShellFlowTests — connect populates, disconnect clears, a bucket stays empty — and by manual checks 8.18 and 8.19, whose phase preamble also stops claiming thirteen checks when it lists twenty-one.
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Globalization;
|
||||
using DodoSSH.Client.Auth;
|
||||
using DodoSSH.Client.Domain;
|
||||
using DodoSSH.Client.Import;
|
||||
using DodoSSH.Client.ObjectStore;
|
||||
using DodoSSH.Client.Session;
|
||||
// FakeDeviceKeyStore is compiled into this assembly from a source link and keeps its original namespace;
|
||||
// see the csproj for why it is shared rather than reimplemented.
|
||||
@@ -8059,6 +8060,129 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
shell.Transfers.RemoteEntries.Select(entry => entry.Name).ShouldBe(["notes.txt"]);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The phone's Files-screen chip row, proven at the view model rather than through Avalonia: a pin
|
||||
/// saved on the host before this screen ever connects to it is read straight off the row's own
|
||||
/// <c>HostSecret.PinnedPaths</c> at the moment <c>MarkHostConnected</c> runs, which is what
|
||||
/// <see cref="TransfersViewModel.ConnectedPinnedPaths"/>'s own remark promises rather than a live follow
|
||||
/// of the vault.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ConnectingATransfersHostWithPins_PopulatesConnectedPinnedPaths()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
vault.EditSelectedHostCommand.Execute(null);
|
||||
vault.EditorNewPin = "/var/www/app";
|
||||
vault.AddEditorPinCommand.Execute(null);
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.Attach(vault, knownHosts);
|
||||
shell.Transfers.SelectedHost = shell.Transfers.Hosts[0];
|
||||
|
||||
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.IsConnected.ShouldBeTrue(shell.Transfers.Status);
|
||||
shell.Transfers.ConnectedPinnedPaths.ShouldBe(["/var/www/app"]);
|
||||
shell.Transfers.HasConnectedPins.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The other half of <see cref="ConnectingATransfersHostWithPins_PopulatesConnectedPinnedPaths"/>: the
|
||||
/// chip row has to go with the connection it belongs to, or a later connect to a host with no pins would
|
||||
/// show the previous host's.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task DisconnectingTheTransfersScreen_ClearsConnectedPinnedPaths()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
vault.EditSelectedHostCommand.Execute(null);
|
||||
vault.EditorNewPin = "/var/www/app";
|
||||
vault.AddEditorPinCommand.Execute(null);
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.Attach(vault, knownHosts);
|
||||
shell.Transfers.SelectedHost = shell.Transfers.Hosts[0];
|
||||
|
||||
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
|
||||
shell.Transfers.HasConnectedPins.ShouldBeTrue();
|
||||
|
||||
await shell.Transfers.DisconnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.ConnectedPinnedPaths.ShouldBeEmpty();
|
||||
shell.Transfers.HasConnectedPins.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A bucket is an <c>IRemoteFileStore</c> with no <c>HostSecret</c> underneath it, so there is no
|
||||
/// <c>PinnedPaths</c> to read at all — see <see cref="TransfersViewModel.OpenBucketAsync"/>'s own remark.
|
||||
/// The bucket here is created through the same keychain route
|
||||
/// <see cref="TheS3ScreenWithNoBuckets_SaysWhereOneIsMadeAndGoesThere"/> exercises, and
|
||||
/// <see cref="FakeObjectStoreFactory"/> stands in for the network the way <see cref="FakeSshConnectionFactory"/>
|
||||
/// already does for SFTP.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ConnectingABucket_LeavesConnectedPinnedPathsEmpty()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
shell.Transfers.Attach(vault, knownHosts, buckets: new FakeObjectStoreFactory());
|
||||
|
||||
vault.NewObjectStoreCommand.Execute(null);
|
||||
vault.BucketEditorLabel = "Backups";
|
||||
vault.BucketEditorBucket = "backups";
|
||||
vault.BucketEditorAccessKeyId = "AKIAEXAMPLE";
|
||||
vault.BucketEditorSecretAccessKey = "a-secret-access-key";
|
||||
vault.BucketEditorRegion = "eu-west-1";
|
||||
await vault.SaveObjectStoreCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.SelectedBucket = shell.Transfers.Buckets[0];
|
||||
|
||||
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.Transfers.IsConnected.ShouldBeTrue(shell.Transfers.Status);
|
||||
shell.Transfers.ConnectedPinnedPaths.ShouldBeEmpty();
|
||||
shell.Transfers.HasConnectedPins.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>A bucket that opens and lists as empty, so a bucket connect can be proven with no network.</summary>
|
||||
private sealed class FakeObjectStoreFactory : IObjectStoreFactory
|
||||
{
|
||||
public IRemoteFileStore Open(ObjectStoreSecret store) => new FakeBucketStore();
|
||||
}
|
||||
|
||||
/// <summary>The minimum <see cref="IRemoteFileStore"/> a bucket connect touches: home, then a listing.</summary>
|
||||
private sealed class FakeBucketStore : IRemoteFileStore
|
||||
{
|
||||
public bool IsConnected => true;
|
||||
|
||||
public string HomeDirectory => "/";
|
||||
|
||||
public Task<IReadOnlyList<SftpEntry>> ListAsync(string path, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<IReadOnlyList<SftpEntry>>([]);
|
||||
|
||||
public Task<SftpEntry?> StatAsync(string path, CancellationToken cancellationToken) =>
|
||||
Task.FromResult<SftpEntry?>(null);
|
||||
|
||||
public Task<Stream> OpenReadAsync(string path, long offset, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("Not exercised by proving a bucket connect leaves no pins.");
|
||||
|
||||
public Task<Stream> OpenWriteAsync(string path, long offset, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("Not exercised by proving a bucket connect leaves no pins.");
|
||||
|
||||
public Task CreateDirectoryAsync(string path, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("Not exercised by proving a bucket connect leaves no pins.");
|
||||
|
||||
public Task DeleteAsync(string path, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("Not exercised by proving a bucket connect leaves no pins.");
|
||||
|
||||
public Task RenameAsync(string fromPath, string toPath, CancellationToken cancellationToken) =>
|
||||
throw new NotSupportedException("Not exercised by proving a bucket connect leaves no pins.");
|
||||
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The picker that replaced the desktop's connect bar, and the four things that put it away again. It
|
||||
|
||||
Reference in New Issue
Block a user