Files
DodoSSH/tests/DodoSSH.Client.Sync.Tests/ItemKindsTests.cs
T
jaap-jan b08f2c58e3
ci / build and test (pull_request) Failing after 3m42s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Successful in 5m24s
Fill in the Spelunk API key on the Security page, as a synced Spelunk key that goes only to its own address
Settings → Security gains a SPELUNK section: an API address and a key, saved as a new keychain item, a Spelunk key (SyncEntityType 14, AadResourceType 17, server table spelunk_key via the AddSpelunkKeyItem migration). It is ciphertext only on the server, lives in the personal vault, syncs like every secret, and holds the key together with the one address it may be sent to. The key box empties once saved; the list shows name, address and how many hosts use each key, never the key. Saving a key for an address that already has one replaces it, and deleting goes through the shared confirmation card with the hosts that would stop connecting.

A host's route names a saved key where it named a credential, in the same apiKeyCredentialId field, so no host schema changes and an older build reads such a host as one whose key credential has gone. At connect a saved key is sent only to its own address; a host naming it with another Spelunk address is refused in words. Keys kept as a credential's password still work. The editor offers saved keys first and picks the one saved for an address as it is typed.

A link from a Spelunk with a saved key now connects straight away, including the first one: saving the key is the user's say-so that the first-link editor used to ask for.

The address and key merge as one pair, so two concurrent edits can never pair one device's key with another's address, and the key never reaches the conflict log.
2026-09-29 10:34:38 +02:00

78 lines
3.1 KiB
C#

using DodoSSH.Client.Domain;
using DodoSSH.Contracts;
namespace DodoSSH.Client.Sync.Tests;
/// <summary>
/// The registry of synchronised item types, and the one property that has to hold about it.
/// </summary>
/// <remarks>
/// Adding an item type touches a cipher, a codec, a merge, a repository and a view. The failure this pins is
/// the one that none of those would reveal: a type that can be created, encrypted, merged and listed
/// perfectly, and is never asked for in a pull — so it works on the machine that made it and exists nowhere
/// else. Deriving the filter from the registry is what prevents it; these tests are what notice if the
/// derivation stops holding.
/// </remarks>
public sealed class ItemKindsTests
{
[Fact]
public void ThePullFilterNamesEveryTypeThisBuildSynchronises()
{
ItemKinds.SyncedTypes.ShouldBe(
[
SyncEntityType.Host,
SyncEntityType.SshKey,
SyncEntityType.Credential,
SyncEntityType.KnownHostKey,
SyncEntityType.HostGroup,
SyncEntityType.Tag,
SyncEntityType.Snippet,
SyncEntityType.ConnectionLogEntry,
SyncEntityType.ActivityLogEntry,
SyncEntityType.ObjectStore,
SyncEntityType.SpelunkKey,
]);
}
[Fact]
public void ThePullFilterIsNotEmpty()
{
// Stated separately from the list above because the consequence of an empty one is quiet rather
// than loud: the contract says an empty filter means every type, so the client would ask the server
// for everything it holds and then discard most of the answer in ApplyAsync. The way it could
// actually become empty is a static initialisation order slip — SyncedTypes projects Registry, and a
// reordering of the two declarations would leave it reading an unassigned array.
ItemKinds.SyncedTypes.ShouldNotBeEmpty();
}
[Fact]
public async Task EveryTypeInThePullFilter_HasAReconciler()
{
using var harness = await SyncHarness.CreateAsync();
var reconcilers = ItemKinds.Reconcilers(
harness.First.Outbox, harness.First.Conflicts, harness.First.Keyring);
reconcilers.Keys.Order().ShouldBe(ItemKinds.SyncedTypes.Order());
}
[Fact]
public void AHostEditThatOnlyChangedItsPins_IsReportedAsChangingPinnedPaths()
{
// Pinned paths were the one host field Changes did not look at, so an edit that only pinned or
// unpinned a directory reached the keychain log as an update that changed nothing.
var before = new HostSecret
{
Label = "db-01",
Hostname = "db-01.internal",
PinnedPaths = PinnedPathList.Create(["/var/log"]),
};
var after = before with { PinnedPaths = PinnedPathList.Create(["/var/log", "/etc/nginx"]) };
HostKind.Instance.Changes(before, after).ShouldBe(["Pinned paths"]);
HostKind.Instance.Changes(before, before with { PinnedPaths = PinnedPathList.Empty })
.ShouldBe(["Pinned paths"]);
HostKind.Instance.Changes(before, before with { }).ShouldBeEmpty();
}
}