Public Access
Android: type user@host into the hosts search to open a terminal on a machine without saving it
The phone's only unsaved way in was the box on the terminal screen, which is drawn only while nothing is open, so a second machine could not be reached that way while a first one's session was up. The search well at the top of HOSTS is always there and the design already calls it "Search or connect...", so it now reads an address the way the desktop palette does: with an @ in it, a NOT SAVED card above the list offers a password box and CONNECT. It connects through VaultViewModel.ConnectToEndpointAsync, the same path as the manual box and the desktop palette, so nothing is written to the keychain. The search box and the password are cleared once it dials, the password goes whenever the address changes, and an address the manual parser refuses gets its sentence under the box. Covered by six new ShellFlowTests cases; manual check 8.20 covers the card on a device.
This commit is contained in:
1 parent
988248fa57
commit
5b2e3f3651
4 files changed
+285
-2
No files matched your search
@@ -1130,6 +1130,116 @@ public sealed partial class ShellFlowTests : IAsyncLifetime
|
||||
ssh.Requests.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The phone's hosts search box, read as an address: the card that dials it appears for <c>user@host</c>
|
||||
/// and for nothing else. A plain search must not grow one on every keystroke.
|
||||
/// </remarks>
|
||||
[Theory]
|
||||
[InlineData("root@box", "root@box:22")]
|
||||
[InlineData(" deploy@build.internal:2222 ", "deploy@build.internal:2222")]
|
||||
public async Task AnAddressTypedIntoTheHostsSearch_IsOfferedAsAMachineToConnectTo(
|
||||
string typed,
|
||||
string offered)
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
vault.HostFilter = "prod";
|
||||
vault.HasFilterAddress.ShouldBeFalse("a search without an @ is a search");
|
||||
vault.HasFilterAddressRefusal.ShouldBeFalse();
|
||||
|
||||
vault.HostFilter = typed;
|
||||
|
||||
vault.HasFilterAddress.ShouldBeTrue();
|
||||
vault.FilterAddressText.ShouldBe(offered);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The whole path from the search box: the address and the password typed there are what is dialled, a tab
|
||||
/// opens for it, and the keychain is exactly as it was — that is the point of connecting this way. The box
|
||||
/// and the password are both empty afterwards, so the list is not left filtered to a machine it does not
|
||||
/// hold and the password does not wait in the box for the next one.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsSearchConnectsToATypedAddress_AndSavesNothing()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||
|
||||
var hostsBefore = vault.Hosts.Count;
|
||||
|
||||
vault.HostFilter = "deploy@build.internal:2222";
|
||||
vault.FilterAddressPassword = "hunter2";
|
||||
|
||||
await vault.ConnectToFilterAddressCommand.ExecuteAsync(null);
|
||||
|
||||
var request = ssh.Requests.ShouldHaveSingleItem();
|
||||
request.Host.ShouldBe("build.internal");
|
||||
request.Port.ShouldBe(2222);
|
||||
request.Username.ShouldBe("deploy");
|
||||
request.Credential.ShouldBeOfType<SshPasswordCredential>().Password.ShouldBe("hunter2");
|
||||
|
||||
shell.Tabs.ShouldHaveSingleItem().Label.ShouldBe("deploy@build.internal");
|
||||
vault.Hosts.Count.ShouldBe(hostsBefore, "connecting without saving saves nothing");
|
||||
|
||||
vault.HostFilter.ShouldBeEmpty();
|
||||
vault.FilterAddressPassword.ShouldBeEmpty();
|
||||
vault.HasFilterAddress.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// An empty password is refused under the box rather than sent, as the manual box refuses one — and the
|
||||
/// address stays where it was typed, so filling the password in is all that is left to do.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheHostsSearchRefusesAnEmptyPassword_AndKeepsTheAddress()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||
|
||||
vault.HostFilter = "root@box";
|
||||
|
||||
await vault.ConnectToFilterAddressCommand.ExecuteAsync(null);
|
||||
|
||||
vault.FilterAddressRefusal.ShouldContain("password");
|
||||
vault.HostFilter.ShouldBe("root@box");
|
||||
ssh.Requests.ShouldBeEmpty();
|
||||
shell.Tabs.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A search that looks like an address and cannot be one says why, in the manual parser's own words,
|
||||
/// rather than offering nothing and leaving the box looking ignored.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AnAddressInTheHostsSearchThatCannotBeRead_SaysWhy()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
vault.HostFilter = "root@box:70000";
|
||||
|
||||
vault.HasFilterAddress.ShouldBeFalse();
|
||||
vault.FilterAddressRefusal.ShouldContain("between 1 and 65535");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A password belongs to the machine it was typed for. Changing the address in the box has to take it
|
||||
/// away, or the next machine typed would be dialled with somebody else's password.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ChangingTheAddressInTheHostsSearch_DropsThePasswordTypedForTheLastOne()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
vault.HostFilter = "root@box";
|
||||
vault.FilterAddressPassword = "hunter2";
|
||||
|
||||
vault.HostFilter = "root@other-box";
|
||||
|
||||
vault.FilterAddressPassword.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <b>The retry used to re-run whichever host was selected.</b> That was right while a selected host was
|
||||
/// the only way to connect; with a manual target it would answer "do you trust this key" by dialling a
|
||||
|
||||
Reference in new issue
Block a user