Public Access
Let a host carry pinned folders, merged path by path
This commit is contained in:
@@ -189,6 +189,76 @@ public sealed class ValueSemanticsTests
|
||||
map[EuWest].ShouldBe(EuWest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APinnedPathList_ComparesByContentsAndOrder()
|
||||
{
|
||||
// The half that differs from a tag set and matches a jump chain: a pinned-path list is what a
|
||||
// shortcut menu draws top to bottom, so reordering it is a change, not a no-op.
|
||||
PinnedPathList.Create(["/var/log", "/var/www/app"])
|
||||
.Equals(PinnedPathList.Create(["/var/log", "/var/www/app"]))
|
||||
.ShouldBeTrue();
|
||||
|
||||
(PinnedPathList.Create(["/var/log", "/var/www/app"])
|
||||
== PinnedPathList.Create(["/var/log", "/var/www/app"])).ShouldBeTrue();
|
||||
|
||||
PinnedPathList.Create(["/var/log", "/var/www/app"])
|
||||
.Equals(PinnedPathList.Create(["/var/www/app", "/var/log"]))
|
||||
.ShouldBeFalse();
|
||||
|
||||
PinnedPathList.Create(["/var/log"])
|
||||
.Equals(PinnedPathList.Create(["/var/log", "/var/www/app"]))
|
||||
.ShouldBeFalse();
|
||||
|
||||
PinnedPathList.Create([]).Equals(PinnedPathList.Empty).ShouldBeTrue();
|
||||
PinnedPathList.Create(["/var/log"]).Equals(null).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APinnedPathList_HashesByContentsAndOrder()
|
||||
{
|
||||
PinnedPathList.Create(["/var/log", "/var/www/app"]).GetHashCode()
|
||||
.ShouldBe(PinnedPathList.Create(["/var/log", "/var/www/app"]).GetHashCode());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APinnedPathList_ComparesPathsOrdinally()
|
||||
{
|
||||
// A remote path is meaningful only to the far end, and that end may run a case-sensitive
|
||||
// filesystem — so folding case here would be a decision this client has no business making.
|
||||
PinnedPathList.Create(["/var/log"]).Equals(PinnedPathList.Create(["/VAR/LOG"])).ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APinnedPathList_CollapsesARepeatedPathKeepingTheFirstOccurrence()
|
||||
{
|
||||
// A repeat arrives from a payload some other client wrote, or from the same path pinned twice on
|
||||
// one machine. Left alone it would compare unequal to the same list written once, and the engine
|
||||
// would push the host as changed on every pass for ever.
|
||||
PinnedPathList.Create(["/var/log", "/var/log"]).Equals(PinnedPathList.Create(["/var/log"]))
|
||||
.ShouldBeTrue();
|
||||
|
||||
PinnedPathList.Create(["/var/log", "/var/log"]).Count.ShouldBe(1);
|
||||
|
||||
// The first occurrence survives rather than the last, so a later repeat cannot silently move an
|
||||
// earlier entry to a new position in the list.
|
||||
PinnedPathList.Create(["/var/log", "/var/www/app", "/var/log"])[0].ShouldBe("/var/log");
|
||||
PinnedPathList.Create(["/var/log", "/var/www/app", "/var/log"]).Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void APinnedPathList_MapsToItsOwnPathsInListOrder()
|
||||
{
|
||||
// The value repeats the key on purpose, exactly as TagSet.ToIdMap's does: no key can then hold two
|
||||
// different values, so the only disagreement a keyed merge can report is one side adding what the
|
||||
// other removed. Unlike TagSet, the map is built in list order rather than a canonical one, which
|
||||
// is what lets the merge reproduce "base order, then additions".
|
||||
var map = PinnedPathList.Create(["/var/log", "/var/www/app"]).ToPathMap();
|
||||
|
||||
map.Keys.ShouldBe(new[] { "/var/log", "/var/www/app" });
|
||||
map["/var/log"].ShouldBe("/var/log");
|
||||
map["/var/www/app"].ShouldBe("/var/www/app");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryValidate_RejectsWhatCannotBeStored()
|
||||
{
|
||||
@@ -200,11 +270,39 @@ public sealed class ValueSemanticsTests
|
||||
Host(sshKeyId: Guid.Empty).TryValidate(out _).ShouldBeFalse();
|
||||
Host(credentialId: Guid.Empty).TryValidate(out _).ShouldBeFalse();
|
||||
Host(tags: [Guid.Empty]).TryValidate(out _).ShouldBeFalse();
|
||||
Host(pinnedPaths: [" "]).TryValidate(out _).ShouldBeFalse();
|
||||
Host(pinnedPaths: ["/has\0nul"]).TryValidate(out _).ShouldBeFalse();
|
||||
|
||||
// Null is "take the group's port", not an absent one, and it has to be storable — it is the whole
|
||||
// of what inheritance stores.
|
||||
Host(port: null).TryValidate(out _).ShouldBeTrue();
|
||||
Host().TryValidate(out _).ShouldBeTrue();
|
||||
|
||||
// A relative path is exactly as valid as an absolute one: it resolves against the account's home,
|
||||
// which this client never needs to know.
|
||||
Host(pinnedPaths: ["relative/within/home"]).TryValidate(out _).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryValidate_BoundsThePinnedPaths()
|
||||
{
|
||||
Host(pinnedPaths: [new string('a', HostSecret.MaxPinnedPathLength)])
|
||||
.TryValidate(out _).ShouldBeTrue();
|
||||
|
||||
Host(pinnedPaths: [new string('a', HostSecret.MaxPinnedPathLength + 1)])
|
||||
.TryValidate(out var tooLong).ShouldBeFalse();
|
||||
tooLong.ShouldNotBeNull().ShouldContain("longer than");
|
||||
|
||||
var atLimit = Enumerable.Range(0, HostSecret.MaxPinnedPaths)
|
||||
.Select(i => $"/path/{i}")
|
||||
.ToArray();
|
||||
Host(pinnedPaths: atLimit).TryValidate(out _).ShouldBeTrue();
|
||||
|
||||
var overLimit = Enumerable.Range(0, HostSecret.MaxPinnedPaths + 1)
|
||||
.Select(i => $"/path/{i}")
|
||||
.ToArray();
|
||||
Host(pinnedPaths: overLimit).TryValidate(out var tooMany).ShouldBeFalse();
|
||||
tooMany.ShouldNotBeNull().ShouldContain("cannot pin more than");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user