diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/dotnet-tests.yml new file mode 100644 index 0000000..edcc14e --- /dev/null +++ b/.github/workflows/dotnet-tests.yml @@ -0,0 +1,24 @@ +name: .NET tests + +on: + pull_request: + push: + branches: + - master + +jobs: + seed-tests: + name: Seed tests + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Run tests + run: dotnet test OneGateApp.Tests/OneGateApp.Tests.csproj --configuration Release --verbosity normal diff --git a/OneGateApp.Tests/OneGateApp.Tests.csproj b/OneGateApp.Tests/OneGateApp.Tests.csproj new file mode 100644 index 0000000..db296aa --- /dev/null +++ b/OneGateApp.Tests/OneGateApp.Tests.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OneGateApp.Tests/RawResourceTests.cs b/OneGateApp.Tests/RawResourceTests.cs new file mode 100644 index 0000000..4770c27 --- /dev/null +++ b/OneGateApp.Tests/RawResourceTests.cs @@ -0,0 +1,69 @@ +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace OneGateApp.Tests; + +public sealed partial class RawResourceTests +{ + [GeneratedRegex("^0x[0-9a-fA-F]{40}$", RegexOptions.CultureInvariant)] + private static partial Regex UInt160HashPattern(); + + [Theory] + [InlineData("protocol.json")] + [InlineData("tokens.json")] + [InlineData("nft.json")] + public void RawJsonResourcesAreValidJson(string fileName) + { + using JsonDocument document = LoadRawJson(fileName); + + Assert.NotEqual(JsonValueKind.Undefined, document.RootElement.ValueKind); + } + + [Fact] + public void ProtocolResourceContainsExpectedNeoMainnetConfiguration() + { + using JsonDocument document = LoadRawJson("protocol.json"); + JsonElement configuration = document.RootElement.GetProperty("ProtocolConfiguration"); + + Assert.Equal(860833102, configuration.GetProperty("Network").GetInt32()); + Assert.Equal(53, configuration.GetProperty("AddressVersion").GetInt32()); + Assert.True(configuration.GetProperty("MillisecondsPerBlock").GetInt32() > 0); + Assert.True(configuration.GetProperty("StandbyCommittee").GetArrayLength() >= 7); + Assert.True(configuration.GetProperty("SeedList").GetArrayLength() > 0); + } + + [Theory] + [InlineData("tokens.json", true)] + [InlineData("nft.json", false)] + public void TokenCatalogEntriesHaveRequiredFields(string fileName, bool requiresDecimals) + { + using JsonDocument document = LoadRawJson(fileName); + JsonElement catalog = document.RootElement; + + Assert.Equal(JsonValueKind.Array, catalog.ValueKind); + Assert.True(catalog.GetArrayLength() > 0); + + foreach (JsonElement token in catalog.EnumerateArray()) + { + string hash = token.GetProperty("hash").GetString()!; + string name = token.GetProperty("name").GetString()!; + string symbol = token.GetProperty("symbol").GetString()!; + + Assert.Matches(UInt160HashPattern(), hash); + Assert.False(string.IsNullOrWhiteSpace(name)); + Assert.False(string.IsNullOrWhiteSpace(symbol)); + + if (requiresDecimals) + { + int decimals = token.GetProperty("decimals").GetInt32(); + Assert.InRange(decimals, 0, 18); + } + } + } + + static JsonDocument LoadRawJson(string fileName) + { + string path = Path.Combine(TestPaths.RepositoryRoot, "OneGateApp", "Resources", "Raw", fileName); + return JsonDocument.Parse(File.ReadAllText(path)); + } +} diff --git a/OneGateApp.Tests/ResourceParityTests.cs b/OneGateApp.Tests/ResourceParityTests.cs new file mode 100644 index 0000000..f0d5994 --- /dev/null +++ b/OneGateApp.Tests/ResourceParityTests.cs @@ -0,0 +1,37 @@ +using System.Xml.Linq; + +namespace OneGateApp.Tests; + +public sealed class ResourceParityTests +{ + [Fact] + public void LocalizedStringResourcesExposeSameKeysAsNeutralResource() + { + string resourceDirectory = Path.Combine(TestPaths.RepositoryRoot, "OneGateApp", "Properties"); + string neutralPath = Path.Combine(resourceDirectory, "Strings.resx"); + string[] localizedPaths = Directory.GetFiles(resourceDirectory, "Strings.*.resx"); + + SortedSet neutralKeys = LoadResourceKeys(neutralPath); + Assert.NotEmpty(localizedPaths); + + foreach (string localizedPath in localizedPaths) + { + SortedSet localizedKeys = LoadResourceKeys(localizedPath); + string[] missing = neutralKeys.Except(localizedKeys).ToArray(); + string[] extra = localizedKeys.Except(neutralKeys).ToArray(); + + Assert.True(missing.Length == 0 && extra.Length == 0, + $"{Path.GetFileName(localizedPath)} resource keys differ. Missing: {string.Join(", ", missing)}. Extra: {string.Join(", ", extra)}."); + } + } + + static SortedSet LoadResourceKeys(string path) + { + XDocument document = XDocument.Load(path); + IEnumerable keys = document.Root! + .Elements("data") + .Select(element => element.Attribute("name")?.Value) + .Where(name => !string.IsNullOrWhiteSpace(name))!; + return new SortedSet(keys, StringComparer.Ordinal); + } +} diff --git a/OneGateApp.Tests/TestPaths.cs b/OneGateApp.Tests/TestPaths.cs new file mode 100644 index 0000000..1f76636 --- /dev/null +++ b/OneGateApp.Tests/TestPaths.cs @@ -0,0 +1,21 @@ +namespace OneGateApp.Tests; + +static class TestPaths +{ + public static string RepositoryRoot { get; } = FindRepositoryRoot(); + + static string FindRepositoryRoot() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null) + { + string projectPath = Path.Combine(directory.FullName, "OneGateApp", "OneGateApp.csproj"); + if (File.Exists(projectPath)) + return directory.FullName; + + directory = directory.Parent; + } + + throw new DirectoryNotFoundException("Could not locate the OneGateApp repository root."); + } +}