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",
};
}