From 944522066806459340c93a16c72c254e1e0e97e0 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 22 Jun 2026 09:36:26 +0800 Subject: [PATCH 1/3] Add seed tests and CI workflow --- .github/workflows/dotnet-tests.yml | 24 +++++++++ OneGateApp.Tests/OneGateApp.Tests.csproj | 21 ++++++++ OneGateApp.Tests/RawResourceTests.cs | 69 ++++++++++++++++++++++++ OneGateApp.Tests/ResourceParityTests.cs | 37 +++++++++++++ OneGateApp.Tests/TestPaths.cs | 21 ++++++++ 5 files changed, 172 insertions(+) create mode 100644 .github/workflows/dotnet-tests.yml create mode 100644 OneGateApp.Tests/OneGateApp.Tests.csproj create mode 100644 OneGateApp.Tests/RawResourceTests.cs create mode 100644 OneGateApp.Tests/ResourceParityTests.cs create mode 100644 OneGateApp.Tests/TestPaths.cs 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."); + } +} From 188e42d133289f7e0228daaad1ed5924d3cfe98b Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sat, 4 Jul 2026 21:44:53 +0800 Subject: [PATCH 2/3] Add test project to solution file --- OneGateApp.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/OneGateApp.slnx b/OneGateApp.slnx index 99e8eb8..0e90ede 100644 --- a/OneGateApp.slnx +++ b/OneGateApp.slnx @@ -3,5 +3,6 @@ + From c1ae4cc142db6015a6e1f01e4533cbd58e0ee393 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 5 Jul 2026 15:43:54 +0800 Subject: [PATCH 3/3] Reference app project from seed tests --- OneGateApp.Tests/OneGateApp.Tests.csproj | 11 ++++++++++- OneGateApp.Tests/TestPaths.cs | 25 ++++++++++++------------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/OneGateApp.Tests/OneGateApp.Tests.csproj b/OneGateApp.Tests/OneGateApp.Tests.csproj index db296aa..c575564 100644 --- a/OneGateApp.Tests/OneGateApp.Tests.csproj +++ b/OneGateApp.Tests/OneGateApp.Tests.csproj @@ -5,17 +5,26 @@ enable enable false + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\OneGateApp\OneGateApp.csproj')) + + + + <_Parameter1>OneGateAppProjectPath + <_Parameter2>$(OneGateAppProjectPath) + + + - \ No newline at end of file + diff --git a/OneGateApp.Tests/TestPaths.cs b/OneGateApp.Tests/TestPaths.cs index 1f76636..5b17263 100644 --- a/OneGateApp.Tests/TestPaths.cs +++ b/OneGateApp.Tests/TestPaths.cs @@ -2,20 +2,19 @@ namespace OneGateApp.Tests; static class TestPaths { - public static string RepositoryRoot { get; } = FindRepositoryRoot(); + static string AppProjectPath { get; } = GetAssemblyMetadata("OneGateAppProjectPath") + ?? throw new InvalidOperationException("OneGateAppProjectPath metadata is missing."); + public static string AppProjectDirectory { get; } = Path.GetDirectoryName(AppProjectPath) + ?? throw new InvalidOperationException("Could not locate the OneGateApp project directory."); + public static string RepositoryRoot { get; } = Directory.GetParent(AppProjectDirectory)?.FullName + ?? throw new InvalidOperationException("Could not locate the OneGateApp repository root."); - static string FindRepositoryRoot() + static string? GetAssemblyMetadata(string key) { - 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."); + return Attribute + .GetCustomAttributes(typeof(TestPaths).Assembly, typeof(System.Reflection.AssemblyMetadataAttribute)) + .OfType() + .FirstOrDefault(p => p.Key == key) + ?.Value; } }