using System.Text; namespace DodoSSH.Client.Domain.Tests; /// /// A Spelunk key: an API key and the one Spelunk it may be sent to. /// /// /// What these pin is that the pair is stored as it will be compared — the address in its one normal form — /// that an address the key could leak across a network from is refused, and that a merge never pairs one /// device's key with another device's address. /// public sealed class SpelunkKeySecretTests { private const string Api = "https://api.spelunk.example.com"; [Fact] public void AKey_RoundTrips() { var key = Key(); SpelunkKeySecretCodec.TryDecode(SpelunkKeySecretCodec.Encode(key), out var document).ShouldBeTrue(); document.ShouldNotBeNull(); document.Key.ShouldBe(key); document.SchemaVersion.ShouldBe(SpelunkKeySecretCodec.CurrentSchemaVersion); document.IsReadOnly.ShouldBeFalse(); } [Fact] public void AKey_EncodesAtTheBaseVersion() { Encoding.UTF8.GetString(SpelunkKeySecretCodec.Encode(Key())) .ShouldBe( """{"schemaVersion":1,"label":"Spelunk","apiBase":"https://api.spelunk.example.com","apiKey":"spelunk_one"}"""); } [Theory] [InlineData("https://api.spelunk.example.com/")] [InlineData("HTTPS://API.SPELUNK.EXAMPLE.COM")] public void AnAddressNotInItsNormalForm_IsRefused(string apiBase) { // Stored the one way or not at all: it is compared byte for byte with what a host or a link names. (Key() with { ApiBase = apiBase }).TryValidate(out var reason).ShouldBeFalse(); reason.ShouldNotBeNull().ShouldContain("normal form"); } [Fact] public void PlainHttpAcrossANetwork_IsRefused() { (Key() with { ApiBase = "http://api.spelunk.example.com" }).TryValidate(out var reason).ShouldBeFalse(); reason.ShouldNotBeNull().ShouldContain("https"); } [Fact] public void PlainHttpToThisMachine_IsAllowed() { (Key() with { ApiBase = "http://localhost:5080" }).TryValidate(out _).ShouldBeTrue(); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("spelunk_one\r\nX-Other: header")] public void AKeyThatIsNotAKey_IsRefused(string apiKey) { (Key() with { ApiKey = apiKey }).TryValidate(out _).ShouldBeFalse(); } [Fact] public void APayloadWithAnAddressOutOfNormalForm_DoesNotDecode() { // The address decides where the key may go, so a payload that disagrees with how every client // writes it is refused rather than normalised into something nobody chose. var payload = Encoding.UTF8.GetBytes( """{"schemaVersion":1,"label":"Spelunk","apiBase":"https://api.spelunk.example.com/","apiKey":"k"}"""); SpelunkKeySecretCodec.TryDecode(payload, out _).ShouldBeFalse(); } [Fact] public void AKeyChangedOnOneSide_AndAnAddressOnTheOther_IsKeptAsAPair() { // The case this merge exists for: merging field by field would send the new key to the new address, // a pairing neither device made. var ancestor = Key(); var local = ancestor with { ApiKey = "spelunk_two" }; var remote = ancestor with { ApiBase = "https://spelunk.other.example" }; var result = SpelunkKeySecretMerge.Merge(ancestor, local, remote); (result.Merged.ApiBase, result.Merged.ApiKey).ShouldBeOneOf( (local.ApiBase, local.ApiKey), (remote.ApiBase, remote.ApiKey)); var conflict = result.Conflicts.ShouldHaveSingleItem(); conflict.Field.ShouldBe(SpelunkKeySecretMerge.PairField); } [Fact] public void AKeyReplacedOnBothSides_NeverReachesTheConflictLog() { var ancestor = Key(); var local = ancestor with { ApiKey = "spelunk_local_secret" }; var remote = ancestor with { ApiKey = "spelunk_remote_secret" }; var result = SpelunkKeySecretMerge.Merge(ancestor, local, remote); var conflict = result.Conflicts.ShouldHaveSingleItem(); $"{conflict.Kept} {conflict.Discarded}".ShouldNotContain("secret"); } [Fact] public void AKeyChangedOnOneSideOnly_IsTakenWithoutAConflict() { var ancestor = Key(); var local = ancestor with { ApiKey = "spelunk_two", Label = "renamed" }; var result = SpelunkKeySecretMerge.Merge(ancestor, local, ancestor); result.Merged.ShouldBe(local); result.HasConflicts.ShouldBeFalse(); } private static SpelunkKeySecret Key() => new() { Label = "Spelunk", ApiBase = Api, ApiKey = "spelunk_one", }; }