Public Access
Merge branch 'claude/groups-vault-sharing-e4b154'
# Conflicts: # src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs
This commit is contained in:
@@ -727,12 +727,216 @@ public sealed class VaultSharingTests : IAsyncLifetime
|
||||
vault.ShowsEditorVaultChoice.ShouldBeFalse("an item cannot be moved between vaults");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A group is a shelf, and a shared vault is what makes it everybody's shelf. The assertions are the
|
||||
/// three things that were missing while the group list was the active vault's alone: it is listed at
|
||||
/// all, the row says which vault it is in, and a rename typed into it goes back to that vault rather
|
||||
/// than forking a second group of the new name into the personal one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Reloaded between the write and the read, so what is asserted is what came back out of the vault
|
||||
/// rather than the row the save left behind.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AGroupFiledIntoASharedVault_IsListedThereAndRenamedThere()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
|
||||
await CreateVaultAsync(vaults, "Platform secrets");
|
||||
|
||||
var vault = shell.Vault!;
|
||||
var sharedVaultId = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
|
||||
vault.ShowsGroupEditorVaultChoice.ShouldBeTrue("there are two vaults to choose between");
|
||||
|
||||
vault.GroupEditorSelectedVault =
|
||||
vault.GroupEditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
|
||||
|
||||
vault.GroupEditorLabel = "production";
|
||||
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
var group = vault.Groups.ShouldHaveSingleItem();
|
||||
|
||||
group.VaultId.ShouldBe(sharedVaultId, vault.Status);
|
||||
group.VaultBadge.ShouldBe("PLATFORM SECRETS", "a card in a session holding two vaults says which");
|
||||
|
||||
vault.SelectedGroup = group;
|
||||
vault.EditGroupCommand.Execute(null);
|
||||
|
||||
vault.ShowsGroupEditorVaultChoice.ShouldBeFalse("an item cannot be moved between vaults");
|
||||
|
||||
vault.DrawerSubtitle.ShouldBe(
|
||||
"Platform secrets", "with no picker drawn, the header is what says whose shelf this is");
|
||||
|
||||
vault.GroupEditorLabel = "live";
|
||||
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
var renamed = vault.Groups.ShouldHaveSingleItem();
|
||||
|
||||
renamed.Label.ShouldBe("live");
|
||||
renamed.VaultId.ShouldBe(sharedVaultId, "a rename must not fork a copy into the personal vault");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The group editor's picker is the group's, exactly as the host editor's is the host's: moving it must
|
||||
/// not move the keychain screen's standing preference, and moving that one must not move a group
|
||||
/// half-typed here.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The second half is the one worth the test. The picker is read when the form opens and the vault is
|
||||
/// captured there, so a click on the other screen between typing the name and pressing ADD cannot
|
||||
/// redirect the group somebody was making.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheGroupEditorChoosesItsOwnVault_WithoutMovingTheKeychainScreensPicker()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
|
||||
await CreateVaultAsync(vaults, "Platform secrets");
|
||||
|
||||
var vault = shell.Vault!;
|
||||
var sharedVaultId = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
var personal = vault.SelectedTargetVault!;
|
||||
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
|
||||
vault.GroupEditorSelectedVault =
|
||||
vault.GroupEditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
|
||||
|
||||
vault.GroupEditorLabel = "production";
|
||||
|
||||
// Moved back after the editor opened, the way a click on the keychain screen would. The group must
|
||||
// still land in the shared vault.
|
||||
vault.SelectedTargetVault = personal;
|
||||
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
|
||||
vault.Groups.ShouldHaveSingleItem().VaultId.ShouldBe(sharedVaultId, vault.Status);
|
||||
|
||||
vault.SelectedTargetVault.ShouldBe(
|
||||
personal, "the editor's picker is the group's, not the screen's standing preference");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A parent belongs to one vault, and a group filed under one in another vault would be a level half
|
||||
/// the people holding the key cannot resolve — their hosts would inherit a port and a username from
|
||||
/// nothing. The same rule the host editor's group picker follows, one level up the same tree.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AGroupsParentPicker_OffersOnlyTheVaultItIsGoingInto()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
|
||||
await CreateVaultAsync(vaults, "Platform secrets");
|
||||
|
||||
var vault = shell.Vault!;
|
||||
var sharedVaultId = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
// In the personal vault, which is where the standing preference points.
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
vault.GroupEditorLabel = "estate";
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
|
||||
vault.Groups.ShouldHaveSingleItem().Label.ShouldBe("estate", vault.Status);
|
||||
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
|
||||
vault.GroupEditorParentChoices
|
||||
.Any(choice => string.Equals(choice.Label, "estate", StringComparison.Ordinal))
|
||||
.ShouldBeTrue("a group in the personal vault may be filed under a personal group");
|
||||
|
||||
vault.GroupEditorSelectedVault =
|
||||
vault.GroupEditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
|
||||
|
||||
vault.GroupEditorParentChoices.ShouldHaveSingleItem()
|
||||
.EntityId.ShouldBeNull("only 'no parent' is left once the group is going somewhere else");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Dragging a host card onto a group card is the one gesture that files a host without opening its
|
||||
/// editor, and it can now be aimed across a vault boundary, because both grids draw every readable
|
||||
/// vault. The write it would make is the exact thing the host editor's group picker was fixed to
|
||||
/// prevent: an id only the other vault's holders can resolve.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Refused and said so, rather than quietly treated as "no group" — the user is plainly filing
|
||||
/// something, and unfiling it instead would be the wrong answer delivered silently.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task AHostDraggedOntoAnotherVaultsGroup_IsRefusedRatherThanFiledUnderIt()
|
||||
{
|
||||
await UnlockedAsync();
|
||||
|
||||
var vaults = shell.Vaults;
|
||||
|
||||
await CreateVaultAsync(vaults, "Platform secrets");
|
||||
|
||||
var vault = shell.Vault!;
|
||||
var sharedVaultId = vaults.SelectedVault!.VaultId;
|
||||
|
||||
await vault.LoadAsync(Token);
|
||||
|
||||
vault.NewGroupCommand.Execute(null);
|
||||
|
||||
vault.GroupEditorSelectedVault =
|
||||
vault.GroupEditorVaultChoices.Single(choice => choice.VaultId == sharedVaultId);
|
||||
|
||||
vault.GroupEditorLabel = "production";
|
||||
await vault.SaveGroupCommand.ExecuteAsync(null);
|
||||
|
||||
// The host stays in the personal vault, which is where a new one goes without being told otherwise.
|
||||
vault.NewHostCommand.Execute(null);
|
||||
vault.EditorLabel = "prod-db";
|
||||
vault.EditorHostname = "db.internal";
|
||||
await vault.SaveHostCommand.ExecuteAsync(null);
|
||||
|
||||
var host = vault.Hosts.Single(row => string.Equals(row.Label, "prod-db", StringComparison.Ordinal));
|
||||
var group = vault.Groups.Single(row => row.VaultId == sharedVaultId);
|
||||
|
||||
host.VaultId.ShouldNotBe(sharedVaultId, "this test is meaningless with both in one vault");
|
||||
|
||||
await vault.MoveHostToGroupCommand.ExecuteAsync(new HostGroupMove(host, group.EntityId));
|
||||
|
||||
vault.Status.ShouldContain("its own vault");
|
||||
|
||||
vault.Hosts
|
||||
.Single(row => string.Equals(row.Label, "prod-db", StringComparison.Ordinal))
|
||||
.Host.GroupId
|
||||
.ShouldBeNull("the host is left where it was rather than filed under an unresolvable group");
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The mirror image of the host test above, and it goes the other way on purpose. A host filed into a
|
||||
/// shared vault has to stay there, because hosts are read across every readable vault and so come back.
|
||||
/// Tags are not — the editable list is the active vault's alone, like groups and buckets — so a tag
|
||||
/// filed anywhere else would be created, pushed, reported as added and then invisible, with nothing on
|
||||
/// the keychain screen able to rename or delete it and no active-vault switcher to go and find it with.
|
||||
/// shared vault has to stay there, because hosts are read across every readable vault and so come back;
|
||||
/// so does a group, since its list spans them too. Tags are not — the editable list is the active
|
||||
/// vault's alone, like buckets — so a tag filed anywhere else would be created, pushed, reported as
|
||||
/// added and then invisible, with nothing on the keychain screen able to rename or delete it and no
|
||||
/// active-vault switcher to go and find it with.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task ATagIgnoresTheTargetPicker_BecauseItsListOnlyEverShowsOneVault()
|
||||
|
||||
Reference in New Issue
Block a user