Add Canonical JSON serialization library with tests and documentation
- Implemented CanonJson class for deterministic JSON serialization and hashing. - Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters. - Created project files for the Canonical JSON library and its tests, including necessary package references. - Added README.md for library usage and API reference. - Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// NativeUnknownClassifierTests.cs
|
||||
// Sprint: SPRINT_3500_0013_0001_native_unknowns
|
||||
// Task: NUC-005
|
||||
// Description: Unit tests for NativeUnknownClassifier service.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Unknowns.Core.Models;
|
||||
using StellaOps.Unknowns.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Unknowns.Core.Tests.Services;
|
||||
|
||||
public sealed class NativeUnknownClassifierTests
|
||||
{
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
private readonly NativeUnknownClassifier _classifier;
|
||||
|
||||
public NativeUnknownClassifierTests()
|
||||
{
|
||||
_timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 12, 19, 12, 0, 0, TimeSpan.Zero));
|
||||
_classifier = new NativeUnknownClassifier(_timeProvider, "test-classifier");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyMissingBuildId_Creates_Unknown_With_Correct_Properties()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libfoo.so.1",
|
||||
Architecture = "x86_64",
|
||||
LayerDigest = "sha256:abc123",
|
||||
LayerIndex = 2,
|
||||
FileDigest = "sha256:def456",
|
||||
FileSize = 1024000
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyMissingBuildId("tenant-1", context);
|
||||
|
||||
// Assert
|
||||
unknown.Should().NotBeNull();
|
||||
unknown.Kind.Should().Be(UnknownKind.MissingBuildId);
|
||||
unknown.SubjectType.Should().Be(UnknownSubjectType.Binary);
|
||||
unknown.SubjectRef.Should().Be("/usr/lib/libfoo.so.1");
|
||||
unknown.TenantId.Should().Be("tenant-1");
|
||||
unknown.Severity.Should().Be(UnknownSeverity.Medium);
|
||||
unknown.CreatedBy.Should().Be("test-classifier");
|
||||
unknown.ValidFrom.Should().Be(_timeProvider.GetUtcNow());
|
||||
unknown.SysFrom.Should().Be(_timeProvider.GetUtcNow());
|
||||
unknown.Context.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyUnknownBuildId_Creates_Unknown_With_BuildId_Reference()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libbar.so.2",
|
||||
BuildId = "gnu-build-id:abc123def456",
|
||||
Architecture = "aarch64",
|
||||
LayerDigest = "sha256:xyz789"
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyUnknownBuildId("tenant-2", context);
|
||||
|
||||
// Assert
|
||||
unknown.Should().NotBeNull();
|
||||
unknown.Kind.Should().Be(UnknownKind.UnknownBuildId);
|
||||
unknown.SubjectRef.Should().Be("gnu-build-id:abc123def456");
|
||||
unknown.Severity.Should().Be(UnknownSeverity.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyUnknownBuildId_Throws_When_BuildId_Missing()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/lib/libfoo.so"
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
var act = () => _classifier.ClassifyUnknownBuildId("tenant-1", context);
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithMessage("*BuildId*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyUnresolvedLibrary_Creates_Unknown_With_Import_Info()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/bin/myapp",
|
||||
UnresolvedImport = "libcrypto.so.1.1",
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyUnresolvedLibrary("tenant-3", context);
|
||||
|
||||
// Assert
|
||||
unknown.Should().NotBeNull();
|
||||
unknown.Kind.Should().Be(UnknownKind.UnresolvedNativeLibrary);
|
||||
unknown.SubjectRef.Should().Contain("libcrypto.so.1.1");
|
||||
unknown.Severity.Should().Be(UnknownSeverity.Medium);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyHeuristicDependency_Creates_Unknown_With_Confidence()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "elf",
|
||||
FilePath = "/usr/bin/dynamic-loader",
|
||||
HeuristicPattern = "dlopen(\"libplugin-%s.so\", RTLD_NOW)",
|
||||
HeuristicConfidence = 0.75,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyHeuristicDependency("tenant-4", context);
|
||||
|
||||
// Assert
|
||||
unknown.Should().NotBeNull();
|
||||
unknown.Kind.Should().Be(UnknownKind.HeuristicDependency);
|
||||
unknown.Severity.Should().Be(UnknownSeverity.Low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyUnsupportedFormat_Creates_Unknown_With_Reason()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "pe",
|
||||
FilePath = "C:\\Windows\\System32\\legacy.dll",
|
||||
UnsupportedReason = "PE/COFF format with non-standard overlay",
|
||||
Architecture = "i686"
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyUnsupportedFormat("tenant-5", context);
|
||||
|
||||
// Assert
|
||||
unknown.Should().NotBeNull();
|
||||
unknown.Kind.Should().Be(UnknownKind.UnsupportedBinaryFormat);
|
||||
unknown.Severity.Should().Be(UnknownSeverity.Info);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_Classifications_Have_Unique_Subject_Hashes()
|
||||
{
|
||||
// Arrange
|
||||
var context1 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/a.so", LayerDigest = "sha256:layer1" };
|
||||
var context2 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/b.so", LayerDigest = "sha256:layer1" };
|
||||
var context3 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/a.so", LayerDigest = "sha256:layer2" };
|
||||
|
||||
// Act
|
||||
var unknown1 = _classifier.ClassifyMissingBuildId("tenant", context1);
|
||||
var unknown2 = _classifier.ClassifyMissingBuildId("tenant", context2);
|
||||
var unknown3 = _classifier.ClassifyMissingBuildId("tenant", context3);
|
||||
|
||||
// Assert - Different files or layers should produce different hashes
|
||||
unknown1.SubjectHash.Should().NotBe(unknown2.SubjectHash);
|
||||
unknown1.SubjectHash.Should().NotBe(unknown3.SubjectHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Same_Binary_Produces_Same_Subject_Hash()
|
||||
{
|
||||
// Arrange - Same file path and layer
|
||||
var context1 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/same.so", LayerDigest = "sha256:samelayer" };
|
||||
var context2 = new NativeUnknownContext { Format = "elf", FilePath = "/lib/same.so", LayerDigest = "sha256:samelayer" };
|
||||
|
||||
// Act
|
||||
var unknown1 = _classifier.ClassifyMissingBuildId("tenant", context1);
|
||||
var unknown2 = _classifier.ClassifyMissingBuildId("tenant", context2);
|
||||
|
||||
// Assert - Same file+layer should produce same hash (for deduplication)
|
||||
unknown1.SubjectHash.Should().Be(unknown2.SubjectHash);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void ClassifyMissingBuildId_Throws_When_TenantId_Invalid(string? tenantId)
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext { Format = "elf", FilePath = "/lib/foo.so" };
|
||||
|
||||
// Act & Assert
|
||||
var act = () => _classifier.ClassifyMissingBuildId(tenantId!, context);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClassifyMissingBuildId_Throws_When_Context_Null()
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => _classifier.ClassifyMissingBuildId("tenant", null!);
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Throws_When_TimeProvider_Null()
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => new NativeUnknownClassifier(null!, "test");
|
||||
act.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Constructor_Throws_When_CreatedBy_Invalid(string? createdBy)
|
||||
{
|
||||
// Act & Assert
|
||||
var act = () => new NativeUnknownClassifier(TimeProvider.System, createdBy!);
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Context_Is_Serialized_To_JsonDocument()
|
||||
{
|
||||
// Arrange
|
||||
var context = new NativeUnknownContext
|
||||
{
|
||||
Format = "macho",
|
||||
FilePath = "/Applications/MyApp.app/Contents/MacOS/MyApp",
|
||||
BuildId = "macho-uuid:12345678-1234-5678-9abc-def012345678",
|
||||
Architecture = "arm64"
|
||||
};
|
||||
|
||||
// Act
|
||||
var unknown = _classifier.ClassifyUnknownBuildId("tenant", context);
|
||||
|
||||
// Assert
|
||||
unknown.Context.Should().NotBeNull();
|
||||
var root = unknown.Context!.RootElement;
|
||||
root.GetProperty("format").GetString().Should().Be("macho");
|
||||
root.GetProperty("filePath").GetString().Should().Contain("MyApp");
|
||||
root.GetProperty("architecture").GetString().Should().Be("arm64");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" ?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>StellaOps.Unknowns.Core.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Time.Testing" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Unknowns.Core/StellaOps.Unknowns.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user