Public Access
Quick connect: type user@host into Ctrl+K to open a terminal on a machine without saving it
The desktop had no way to reach a machine that was not in the keychain; the phone has had a manual box on its terminal screen all along. The palette's query now reads as an address when it has an @ in it: a "not saved" row offers to dial it, Enter asks for the password in place, and a second Enter connects. Escape on the password step goes back to the address. It goes through the phone's own path, now split out as VaultViewModel.ConnectToEndpointAsync, so the tab, the host-key question and the connection-log entry are the same on both heads and nothing is written to the keychain. Password only, as on the phone: a key is the reason to save the host. The typed row takes the highlight unless a saved host dials exactly that address, so typing a new address cannot land on a saved machine whose address merely starts the same way. Covered by eleven new QuickConnectTests; manual check 7.10a covers the real dial.
This commit is contained in:
1 parent
902761d37b
commit
988248fa57
8 files changed
+840
-99
No files matched your search
@@ -321,6 +321,263 @@ public sealed class QuickConnectTests : IAsyncLifetime
|
||||
Token);
|
||||
}
|
||||
|
||||
// ---- A machine that is not saved ----
|
||||
|
||||
/// <remarks>
|
||||
/// The palette's answer to an address somebody has just been given: the query reads as one, a row offers
|
||||
/// to dial it, and that row is the one Enter takes — nothing saved answers an address nobody has saved.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TypingAnAddressOffersToConnectWithoutSavingIt()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
shell.SearchText = "root@10.0.0.5:2222";
|
||||
Relayout(window);
|
||||
|
||||
shell.HasAdHocTarget.ShouldBeTrue();
|
||||
shell.IsAdHocSelected.ShouldBeTrue("the typed address is what the query is plainly about");
|
||||
shell.SelectedSearchResult.ShouldBeNull("exactly one row is lit");
|
||||
|
||||
palette.AdHocTargetRow.IsEffectivelyVisible.ShouldBeTrue();
|
||||
VisibleTexts(palette.AdHocTargetRow).ShouldContain("root@10.0.0.5:2222", StringComparer.Ordinal);
|
||||
VisibleTexts(palette.AdHocTargetRow).ShouldContain("not saved", StringComparer.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The default port is printed rather than implied, because it is what will be dialled — and it is what
|
||||
/// makes the row comparable with a saved host's address, which always carries one.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AnAddressWithNoPortIsOfferedOnTwentyTwo()
|
||||
{
|
||||
await OnThePaletteAsync((_, _) =>
|
||||
{
|
||||
shell.SearchText = "root@box";
|
||||
|
||||
shell.AdHocAddress.ShouldBe("root@box:22");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A query with no <c>@</c> is a search and nothing else, so a name that matches no host offers no row to
|
||||
/// dial — but it does say what the box would take instead, because a name that matches nothing is often a
|
||||
/// machine that was never saved.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ANameThatMatchesNothingSaysHowToReachAnUnsavedMachine()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
shell.SearchText = "nothing-is-called-this";
|
||||
Relayout(window);
|
||||
|
||||
shell.HasAdHocTarget.ShouldBeFalse();
|
||||
palette.AdHocTargetRow.IsEffectivelyVisible.ShouldBeFalse();
|
||||
VisibleTexts(palette).ShouldContain(
|
||||
text => text.Contains("user@host", StringComparison.Ordinal),
|
||||
"the hint names the other thing the box takes");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A query that looks like an address and cannot be one says why under the box, rather than producing no
|
||||
/// row and no reason — which would read as a palette that ignored the one thing it was asked.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AnAddressThatCannotBeReadSaysWhy()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
shell.SearchText = "root@box:99999";
|
||||
Relayout(window);
|
||||
|
||||
shell.HasAdHocTarget.ShouldBeFalse();
|
||||
VisibleTexts(palette).ShouldContain(
|
||||
text => text.Contains("port", StringComparison.Ordinal),
|
||||
"the refusal is the manual parser's own sentence");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The password is asked for in the palette itself, and the caret goes with the question: typing an
|
||||
/// address and pressing Enter has to leave somebody typing a password, not hunting for the box.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task EnterOnTheAddressAsksForItsPasswordInThePalette()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
palette.QueryBox.Focus().ShouldBeTrue();
|
||||
shell.SearchText = "root@10.0.0.5";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
Relayout(window);
|
||||
|
||||
shell.IsSearching.ShouldBeTrue("nothing has been dialled yet");
|
||||
shell.IsAskingForAdHocPassword.ShouldBeTrue();
|
||||
palette.PasswordBox.IsEffectivelyVisible.ShouldBeTrue();
|
||||
palette.QueryBox.IsEffectivelyVisible.ShouldBeFalse();
|
||||
palette.PasswordBox.IsFocused.ShouldBeTrue("the caret follows the question");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Escape from the password goes back to the address with what was typed intact, because a mistyped
|
||||
/// address is the likeliest reason to want out — and drops the password, which is a secret nobody asked
|
||||
/// to keep.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task EscapeFromThePasswordGoesBackToTheAddress()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
palette.QueryBox.Focus().ShouldBeTrue();
|
||||
shell.SearchText = "root@10.0.0.5";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
Relayout(window);
|
||||
shell.AdHocPassword = "hunter2";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Escape, RawInputModifiers.None);
|
||||
Relayout(window);
|
||||
|
||||
shell.IsSearching.ShouldBeTrue("Escape here steps back rather than closing");
|
||||
shell.IsAskingForAdHocPassword.ShouldBeFalse();
|
||||
shell.SearchText.ShouldBe("root@10.0.0.5");
|
||||
shell.AdHocPassword.ShouldBeEmpty();
|
||||
palette.QueryBox.IsFocused.ShouldBeTrue("the caret goes back to the address");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Enter with no password typed is refused in the palette rather than dialled: an SSH server asked for a
|
||||
/// password with an empty one answers "authentication failed", which is a sentence about the machine
|
||||
/// when the problem is the box.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AnEmptyPasswordIsRefusedRatherThanDialled()
|
||||
{
|
||||
var started = new List<ConnectionAttemptEventArgs>();
|
||||
vault.ConnectionStarting += (_, attempt) => started.Add(attempt);
|
||||
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
palette.QueryBox.Focus().ShouldBeTrue();
|
||||
shell.SearchText = "root@10.0.0.5";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
Relayout(window);
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
|
||||
shell.IsSearching.ShouldBeTrue();
|
||||
shell.HasAdHocRefusal.ShouldBeTrue();
|
||||
started.ShouldBeEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The whole path: an address, a password, and a tab for that address — with nothing added to the
|
||||
/// keychain. The dial itself fails in this suite, as every connect here does, and inside the vault's own
|
||||
/// handling; what the palette promises is the attempt and the closing.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ThePasswordConnectsToTheAddressAndSavesNothing()
|
||||
{
|
||||
var started = new List<ConnectionAttemptEventArgs>();
|
||||
vault.ConnectionStarting += (_, attempt) => started.Add(attempt);
|
||||
var hostsBefore = vault.Hosts.Count;
|
||||
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
palette.QueryBox.Focus().ShouldBeTrue();
|
||||
shell.SearchText = "root@10.0.0.5:2222";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
Relayout(window);
|
||||
shell.AdHocPassword = "hunter2";
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.Enter, RawInputModifiers.None);
|
||||
|
||||
shell.IsSearching.ShouldBeFalse("connecting closes the palette, as it does for a saved host");
|
||||
shell.AdHocPassword.ShouldBeEmpty("the password does not outlive the palette");
|
||||
|
||||
var attempt = started.ShouldHaveSingleItem();
|
||||
attempt.Address.ShouldBe("root@10.0.0.5:2222");
|
||||
attempt.HostId.ShouldBeNull("nothing in the keychain stands behind this tab");
|
||||
});
|
||||
|
||||
vault.Hosts.Count.ShouldBe(hostsBefore, "connecting without saving saves nothing");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The one time the typed address does not take the highlight: a saved host dials exactly that address,
|
||||
/// so it is the same machine with whatever the keychain knows about it, and Enter should reach it the way
|
||||
/// it was set up to be reached. The typed row is still offered, one arrow away.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AnAddressASavedHostDialsHighlightsThatHost()
|
||||
{
|
||||
await OnThePaletteAsync((_, _) =>
|
||||
{
|
||||
shell.SearchText = "deploy@host-3.internal";
|
||||
|
||||
shell.HasAdHocTarget.ShouldBeTrue();
|
||||
shell.IsAdHocSelected.ShouldBeFalse();
|
||||
shell.SelectedSearchResult.ShouldNotBeNull().Label.ShouldBe("host-3");
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The typed row sits above the list and the arrows treat it as the list's first row, clamped as the rest
|
||||
/// are — and the caret stays in the box throughout, for the reason the plain arrows test gives.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheArrowsMoveBetweenTheAddressAndTheSavedHosts()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
palette.QueryBox.Focus().ShouldBeTrue();
|
||||
|
||||
// Every seeded host's address starts with this, and none of them is exactly it.
|
||||
shell.SearchText = "deploy@host";
|
||||
shell.SearchResults.Count.ShouldBeGreaterThan(1, "an empty list would prove nothing");
|
||||
shell.IsAdHocSelected.ShouldBeTrue();
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.ArrowDown, RawInputModifiers.None);
|
||||
shell.IsAdHocSelected.ShouldBeFalse();
|
||||
shell.SelectedSearchResult.ShouldBe(shell.SearchResults[0]);
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.ArrowUp, RawInputModifiers.None);
|
||||
shell.IsAdHocSelected.ShouldBeTrue();
|
||||
shell.SelectedSearchResult.ShouldBeNull();
|
||||
|
||||
window.KeyPressQwerty(PhysicalKey.ArrowUp, RawInputModifiers.None);
|
||||
shell.IsAdHocSelected.ShouldBeTrue("clamped rather than wrapped");
|
||||
|
||||
palette.QueryBox.IsFocused.ShouldBeTrue();
|
||||
});
|
||||
}
|
||||
|
||||
/// <remarks>The pointer's version of Enter on the typed row: straight to its password.</remarks>
|
||||
[Fact]
|
||||
public async Task ClickingTheAddressAsksForItsPassword()
|
||||
{
|
||||
await OnThePaletteAsync((palette, window) =>
|
||||
{
|
||||
shell.SearchText = "root@10.0.0.5";
|
||||
Relayout(window);
|
||||
|
||||
var row = palette.AdHocTargetRow;
|
||||
window.MouseDown(Centre(row, window), MouseButton.Left);
|
||||
window.MouseUp(Centre(row, window), MouseButton.Left);
|
||||
|
||||
shell.IsAskingForAdHocPassword.ShouldBeTrue();
|
||||
shell.IsSearching.ShouldBeTrue();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
/// <summary>Opens the palette in a window the size the application's is, and runs one body against it.</summary>
|
||||
|
||||
Reference in new issue
Block a user