Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/SyncTrayzor.Tests/ChecksumFileUtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using SyncTrayzor.Utils;
using Xunit;

namespace SyncTrayzor.Tests
{
public class ChecksumFileUtilitiesTests
{
private static MemoryStream ToStream(string text) =>
new(Encoding.UTF8.GetBytes(text));

private static MemoryStream ToStream(byte[] bytes) => new(bytes);

// ── WriteChecksumToFile ──────────────────────────────────────────────

[Fact]
public void WriteChecksumToFile_ProducesValidChecksumLine()
{
var content = Encoding.UTF8.GetBytes("hello world");
using var fileStream = ToStream(content);
using var checksumStream = new MemoryStream();

using var sha256 = SHA256.Create();
ChecksumFileUtilities.WriteChecksumToFile(sha256, checksumStream, "test.txt", fileStream);

checksumStream.Position = 0;
var line = new StreamReader(checksumStream, Encoding.ASCII).ReadLine();

// Expected: "<hex> test.txt"
Assert.NotNull(line);
var parts = line!.Split(" ", 2);
Assert.Equal(2, parts.Length);
Assert.Equal("test.txt", parts[1].Trim());
Assert.Equal(64, parts[0].Length); // SHA-256 hex is 64 chars
}

// ── ValidateChecksum – positive case ────────────────────────────────

[Fact]
public void ValidateChecksum_ReturnsTrueForMatchingContent()
{
var content = Encoding.UTF8.GetBytes("hello world");
var checksumLine = ComputeChecksumLine(content, "test.txt");

using var checksumStream = ToStream(checksumLine);
using var fileStream = ToStream(content);

using var sha256 = SHA256.Create();
Assert.True(ChecksumFileUtilities.ValidateChecksum(sha256, checksumStream, "test.txt", fileStream));
}

// ── ValidateChecksum – negative case ────────────────────────────────

[Fact]
public void ValidateChecksum_ReturnsFalseForModifiedContent()
{
var originalContent = Encoding.UTF8.GetBytes("hello world");
var modifiedContent = Encoding.UTF8.GetBytes("hello WORLD");
var checksumLine = ComputeChecksumLine(originalContent, "test.txt");

using var checksumStream = ToStream(checksumLine);
using var fileStream = ToStream(modifiedContent);

using var sha256 = SHA256.Create();
Assert.False(ChecksumFileUtilities.ValidateChecksum(sha256, checksumStream, "test.txt", fileStream));
}

// ── ValidateChecksum – multi-entry checksum file ─────────────────────

[Fact]
public void ValidateChecksum_FindsCorrectEntryInMultiLineFile()
{
var content = Encoding.UTF8.GetBytes("data");
var otherContent = Encoding.UTF8.GetBytes("other");

var lines = ComputeChecksumLine(otherContent, "other.txt")
+ ComputeChecksumLine(content, "data.txt");

using var checksumStream = ToStream(lines);
using var fileStream = ToStream(content);

using var sha256 = SHA256.Create();
Assert.True(ChecksumFileUtilities.ValidateChecksum(sha256, checksumStream, "data.txt", fileStream));
}

// ── ValidateChecksum – missing filename ──────────────────────────────

[Fact]
public void ValidateChecksum_ThrowsWhenFilenameNotFound()
{
var content = Encoding.UTF8.GetBytes("hello");
var checksumLine = ComputeChecksumLine(content, "other.txt");

using var checksumStream = ToStream(checksumLine);
using var fileStream = ToStream(content);

using var sha256 = SHA256.Create();
Assert.Throws<ArgumentException>(() =>
ChecksumFileUtilities.ValidateChecksum(sha256, checksumStream, "missing.txt", fileStream));
}

// ── Round-trip: WriteChecksumToFile then ValidateChecksum ────────────

[Fact]
public void RoundTrip_WriteAndValidate()
{
var content = Encoding.UTF8.GetBytes("round-trip test content");
using var fileStreamWrite = ToStream(content);
using var checksumStream = new MemoryStream();

using var sha256Write = SHA256.Create();
ChecksumFileUtilities.WriteChecksumToFile(sha256Write, checksumStream, "file.bin", fileStreamWrite);

checksumStream.Position = 0;
using var fileStreamValidate = ToStream(content);
using var sha256Validate = SHA256.Create();
Assert.True(ChecksumFileUtilities.ValidateChecksum(sha256Validate, checksumStream, "file.bin", fileStreamValidate));
}

// ── Helpers ──────────────────────────────────────────────────────────

private static string ComputeChecksumLine(byte[] content, string filename)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(content);
var hex = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
return $"{hex} {filename}\n";
}
}
}
48 changes: 48 additions & 0 deletions src/SyncTrayzor.Tests/FormatUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using SyncTrayzor.Utils;
using Xunit;

namespace SyncTrayzor.Tests
{
public class FormatUtilsTests
{
// ── BytesToHuman ────────────────────────────────────────────────────

[Theory]
[InlineData(0, 0, "0B")]
[InlineData(1, 0, "1B")]
[InlineData(1023, 0, "1023B")]
[InlineData(1024, 0, "1KiB")]
[InlineData(1536, 0, "2KiB")] // 1.5 KiB rounds to 2 with 0 decimal places
[InlineData(1048576, 0, "1MiB")] // 1 MiB
[InlineData(1073741824, 0, "1GiB")] // 1 GiB
public void BytesToHuman_DefaultDecimalPlaces(double bytes, int decimalPlaces, string expected)
{
Assert.Equal(expected, FormatUtils.BytesToHuman(bytes, decimalPlaces));
}

[Theory]
[InlineData(1536, 1, "1.5KiB")] // exactly 1.5 KiB
[InlineData(1024, 2, "1.00KiB")]
[InlineData(1048576, 1, "1.0MiB")]
public void BytesToHuman_WithDecimalPlaces(double bytes, int decimalPlaces, string expected)
{
Assert.Equal(expected, FormatUtils.BytesToHuman(bytes, decimalPlaces));
}

[Fact]
public void BytesToHuman_ByteRangeNeverShowsDecimalPlaces()
{
// Bytes (order == 0) should never have decimal places regardless of the argument
var result = FormatUtils.BytesToHuman(512, decimalPlaces: 3);
Assert.Equal("512B", result);
}

[Fact]
public void BytesToHuman_CapsAtGiB()
{
// Even a very large value should be expressed in GiB, not some higher unit
var result = FormatUtils.BytesToHuman(1024.0 * 1024 * 1024 * 10, decimalPlaces: 0);
Assert.EndsWith("GiB", result);
}
}
}
144 changes: 144 additions & 0 deletions src/SyncTrayzor.Tests/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Linq;
using SyncTrayzor.Utils;
using Xunit;

namespace SyncTrayzor.Tests
{
public class StringExtensionsTests
{
// ── TrimStart(string prefix) ────────────────────────────────────────

[Fact]
public void TrimStart_RemovesPrefixWhenPresent()
{
Assert.Equal("world", "helloworld".TrimStart("hello"));
}

[Fact]
public void TrimStart_ReturnsOriginalWhenPrefixAbsent()
{
Assert.Equal("world", "world".TrimStart("hello"));
}

[Fact]
public void TrimStart_EmptyPrefixReturnsOriginal()
{
Assert.Equal("hello", "hello".TrimStart(""));
}

[Fact]
public void TrimStart_OnlyRemovesLeadingOccurrence()
{
// "abab" starts with "ab", so the result should be "ab" (second occurrence)
Assert.Equal("ab", "abab".TrimStart("ab"));
}

// ── TrimMatchingQuotes(char quote) ─────────────────────────────────

[Fact]
public void TrimMatchingQuotes_RemovesWrappingDoubleQuotes()
{
Assert.Equal("hello world", "\"hello world\"".TrimMatchingQuotes('"'));
}

[Fact]
public void TrimMatchingQuotes_ReturnsOriginalWhenNotWrapped()
{
Assert.Equal("hello", "hello".TrimMatchingQuotes('"'));
}

[Fact]
public void TrimMatchingQuotes_ReturnsOriginalWhenOnlyLeadingQuote()
{
Assert.Equal("\"hello", "\"hello".TrimMatchingQuotes('"'));
}

[Fact]
public void TrimMatchingQuotes_ReturnsOriginalWhenOnlyTrailingQuote()
{
Assert.Equal("hello\"", "hello\"".TrimMatchingQuotes('"'));
}

[Fact]
public void TrimMatchingQuotes_ReturnsEmptyStringForTwoQuotes()
{
Assert.Equal("", "\"\"".TrimMatchingQuotes('"'));
}

// ── SplitCommandLine ────────────────────────────────────────────────

[Fact]
public void SplitCommandLine_SplitsSimpleArgs()
{
var result = StringExtensions.SplitCommandLine("foo bar baz").ToList();
Assert.Equal(new[] { "foo", "bar", "baz" }, result);
}

[Fact]
public void SplitCommandLine_KeepsQuotedSpaces()
{
var result = StringExtensions.SplitCommandLine("\"hello world\" foo").ToList();
Assert.Equal(new[] { "hello world", "foo" }, result);
}

[Fact]
public void SplitCommandLine_EmptyStringReturnsNoElements()
{
var result = StringExtensions.SplitCommandLine("").ToList();
Assert.Empty(result);
}

[Fact]
public void SplitCommandLine_SingleArgReturnsOneElement()
{
var result = StringExtensions.SplitCommandLine("only").ToList();
Assert.Equal(new[] { "only" }, result);
}

[Fact]
public void SplitCommandLine_SkipsExtraSpaces()
{
var result = StringExtensions.SplitCommandLine("foo bar").ToList();
// extra space creates an empty token which is filtered out
Assert.Equal(new[] { "foo", "bar" }, result);
}

// ── JoinCommandLine ─────────────────────────────────────────────────

[Fact]
public void JoinCommandLine_SimpleArgsNoSpaces()
{
Assert.Equal("foo bar", StringExtensions.JoinCommandLine(new[] { "foo", "bar" }));
}

[Fact]
public void JoinCommandLine_QuotesArgWithSpace()
{
Assert.Equal("\"hello world\" foo", StringExtensions.JoinCommandLine(new[] { "hello world", "foo" }));
}

[Fact]
public void JoinCommandLine_EscapesEmbeddedQuotes()
{
// An arg that itself contains a double-quote should be quoted and the inner quote escaped
Assert.Equal("\"say \\\"hi\\\"\"", StringExtensions.JoinCommandLine(new[] { "say \"hi\"" }));
}

[Fact]
public void JoinCommandLine_EmptyInputReturnsEmptyString()
{
Assert.Equal("", StringExtensions.JoinCommandLine(new string[] { }));
}

// ── Round-trip: SplitCommandLine ∘ JoinCommandLine ─────────────────

[Fact]
public void RoundTrip_SplitThenJoin()
{
var original = new[] { "path to/file", "simple", "--flag=value" };
var joined = StringExtensions.JoinCommandLine(original);
var split = StringExtensions.SplitCommandLine(joined).ToArray();
Assert.Equal(original, split);
}
}
}
65 changes: 65 additions & 0 deletions src/SyncTrayzor.Tests/SyncthingCapabilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using SyncTrayzor.Syncthing;
using Xunit;

namespace SyncTrayzor.Tests
{
public class SyncthingCapabilitiesTests
{
// ── SupportsDebugFacilities ──────────────────────────────────────────

[Fact]
public void SupportsDebugFacilities_FalseBeforeIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(0, 11, 99) };
Assert.False(caps.SupportsDebugFacilities);
}

[Fact]
public void SupportsDebugFacilities_TrueAtIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(0, 12, 0) };
Assert.True(caps.SupportsDebugFacilities);
}

[Fact]
public void SupportsDebugFacilities_TrueAfterIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(1, 0, 0) };
Assert.True(caps.SupportsDebugFacilities);
}

// ── SupportsDevicePauseResume ────────────────────────────────────────

[Fact]
public void SupportsDevicePauseResume_FalseBeforeIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(0, 11, 0) };
Assert.False(caps.SupportsDevicePauseResume);
}

[Fact]
public void SupportsDevicePauseResume_TrueAtIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(0, 12, 0) };
Assert.True(caps.SupportsDevicePauseResume);
}

[Fact]
public void SupportsDevicePauseResume_TrueAfterIntroducedVersion()
{
var caps = new SyncthingCapabilities { SyncthingVersion = new Version(2, 0, 0) };
Assert.True(caps.SupportsDevicePauseResume);
}

// ── Default version (0.0.0) ──────────────────────────────────────────

[Fact]
public void DefaultVersion_NoCapabilitiesSupported()
{
var caps = new SyncthingCapabilities(); // default version is 0.0.0
Assert.False(caps.SupportsDebugFacilities);
Assert.False(caps.SupportsDevicePauseResume);
}
}
}
Loading
Loading