using System.Text; namespace DodoSSH.Client.Domain.Tests; /// /// A saved command: what it stores, and what it refuses to change about it. /// /// /// Two things carry weight. That the text survives a round trip untouched, because a shell is not forgiving /// about whitespace it did not expect. And that only ever becomes /// true because somebody set it — never because a decode defaulted it, and never because a merge picked a /// side. /// public sealed class SnippetSecretTests { [Fact] public void ASnippet_RoundTrips() { var snippet = new SnippetSecret { Label = "restart the api", Command = "sudo systemctl restart dodossh-api", Notes = "check the on-call rota first", RunsOnInsert = true, }; SnippetSecretCodec.TryDecode(SnippetSecretCodec.Encode(snippet), out var document) .ShouldBeTrue(); document.ShouldNotBeNull(); document.Snippet.ShouldBe(snippet); document.SchemaVersion.ShouldBe(SnippetSecretCodec.CurrentSchemaVersion); document.IsReadOnly.ShouldBeFalse(); } /// /// The property the whole type is built around. A heredoc's terminator has to arrive on a line of its /// own with nothing after it; trim the trailing newline and the shell waits for one that never comes, /// which looks to the user like the snippet hanging the terminal. /// [Theory] [InlineData(" indented\n")] [InlineData("cat <<'EOF'\nline one\nEOF\n")] [InlineData("first\r\nsecond\r\n")] [InlineData("trailing space ")] [InlineData("")] public void TheCommandSurvivesAsItWasWritten(string command) { var snippet = new SnippetSecret { Label = "verbatim", Command = command }; SnippetSecretCodec.TryDecode(SnippetSecretCodec.Encode(snippet), out var document) .ShouldBeTrue(); document.ShouldNotBeNull().Snippet.Command.ShouldBe(command); } [Fact] public void ARunFlagThatIsMissingFromThePayload_ReadsAsOff() { // The safe direction, and the one a truncated or hand-written payload has to fall in: a snippet // whose flag could not be read must not be one that runs on its own. var payload = Encoding.UTF8.GetBytes( """ {"schemaVersion":1,"label":"restart","command":"sudo reboot"} """); SnippetSecretCodec.TryDecode(payload, out var document).ShouldBeTrue(); document.ShouldNotBeNull().Snippet.RunsOnInsert.ShouldBeFalse(); } [Theory] [InlineData("", "echo hi")] [InlineData(" ", "echo hi")] [InlineData("named", "")] public void AnUnusableSnippet_IsRefused(string label, string command) { new SnippetSecret { Label = label, Command = command } .TryValidate(out var reason) .ShouldBeFalse(); reason.ShouldNotBeNull(); } /// /// Whitespace is a legitimate command — a bare space at a prompt, an indented continuation line — so the /// blank check on the text is deliberately IsNullOrEmpty rather than the whitespace-aware one used /// on the label. /// [Fact] public void ACommandThatIsNothingButWhitespace_IsAllowed() { new SnippetSecret { Label = "a space", Command = " " }.TryValidate(out _).ShouldBeTrue(); } // ---- The merge ---- [Fact] public void AnUncontestedEdit_IsTakenFromWhicheverSideMadeIt() { var ancestor = Snippet(); var local = ancestor with { Notes = "now with a note" }; var result = SnippetSecretMerge.Merge(ancestor, local, ancestor); result.HasConflicts.ShouldBeFalse(); result.Merged.Notes.ShouldBe("now with a note"); } /// /// /// The property that matters about this field, stated over every combination there is: a merge never /// produces a snippet that runs on its own unless one of the two sides asked for one. It holds for a /// structural reason rather than a defensive one — a three-way clash needs both sides to differ from the /// ancestor and from each other, which two values cannot do — and that is exactly why it is worth a test. /// The reasoning is easy to lose, and the field is the whole safety story. /// /// /// An earlier draft of the merge special-cased a clash here to resolve to . This /// test is what showed the branch was unreachable. /// /// [Theory] [InlineData(false, false, false, false)] [InlineData(false, true, false, true)] [InlineData(false, false, true, true)] [InlineData(false, true, true, true)] [InlineData(true, false, true, false)] [InlineData(true, true, false, false)] [InlineData(true, true, true, true)] public void TheRunFlag_OnlyEverBecomesTrueBecauseASideAskedForIt( bool ancestor, bool local, bool remote, bool expected) { var start = Snippet() with { RunsOnInsert = ancestor }; var result = SnippetSecretMerge.Merge( start, start with { RunsOnInsert = local }, start with { RunsOnInsert = remote }); result.Merged.RunsOnInsert.ShouldBe(expected); result.Conflicts.ShouldNotContain( conflict => conflict.Field == nameof(SnippetSecret.RunsOnInsert), "a two-valued field cannot produce a three-way conflict"); } [Fact] public void TwoDifferentEditsToTheCommand_AreReportedWithBothTexts() { // Nothing is redacted here, unlike a credential: a snippet is text somebody wrote on purpose, and a // notice that hid the discarded version would leave them unable to tell which one survived. var ancestor = Snippet(); var result = SnippetSecretMerge.Merge( ancestor, ancestor with { Command = "systemctl restart api" }, ancestor with { Command = "systemctl reload api" }); var conflict = result.Conflicts.ShouldHaveSingleItem(); conflict.Field.ShouldBe(nameof(SnippetSecret.Command)); conflict.Kept.ShouldBe("systemctl reload api"); conflict.Discarded.ShouldBe("systemctl restart api"); } private static SnippetSecret Snippet() => new() { Label = "restart the api", Command = "sudo systemctl restart dodossh-api", }; }