Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -10,13 +10,16 @@ using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using Xunit;
|
||||
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.BinaryIndex.Core.Tests;
|
||||
|
||||
public class ElfFeatureExtractorTests
|
||||
{
|
||||
private readonly ElfFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: ELF magic bytes
|
||||
@@ -30,7 +33,8 @@ public class ElfFeatureExtractorTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithNonElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: Not ELF
|
||||
@@ -44,7 +48,8 @@ public class ElfFeatureExtractorTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithEmptyStream_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
@@ -57,7 +62,8 @@ public class ElfFeatureExtractorTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithValidElf64_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: Minimal ELF64 header (little-endian, x86_64, executable)
|
||||
@@ -77,7 +83,8 @@ public class ElfFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithElf64SharedLib_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: ELF64 shared library
|
||||
@@ -95,7 +102,8 @@ public class ElfFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithAarch64_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: ELF64 aarch64
|
||||
@@ -113,7 +121,8 @@ public class ElfFeatureExtractorTests
|
||||
metadata.Architecture.Should().Be("aarch64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same ELF content
|
||||
@@ -163,7 +172,8 @@ public class PeFeatureExtractorTests
|
||||
{
|
||||
private readonly PeFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithDosMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: DOS/PE magic bytes
|
||||
@@ -177,7 +187,8 @@ public class PeFeatureExtractorTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: ELF magic
|
||||
@@ -191,7 +202,8 @@ public class PeFeatureExtractorTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithPe64_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: PE32+ x86_64 executable
|
||||
@@ -207,7 +219,8 @@ public class PeFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithDll_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: PE DLL
|
||||
@@ -224,7 +237,8 @@ public class PeFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithX86_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: PE32 x86
|
||||
@@ -238,7 +252,8 @@ public class PeFeatureExtractorTests
|
||||
metadata.Architecture.Should().Be("x86");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same PE content
|
||||
@@ -293,7 +308,8 @@ public class MachoFeatureExtractorTests
|
||||
{
|
||||
private readonly MachoFeatureExtractor _extractor = new();
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithMacho64Magic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: Mach-O 64-bit magic
|
||||
@@ -307,7 +323,8 @@ public class MachoFeatureExtractorTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithFatBinaryMagic_ReturnsTrue()
|
||||
{
|
||||
// Arrange: Universal binary magic
|
||||
@@ -321,7 +338,8 @@ public class MachoFeatureExtractorTests
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CanExtract_WithElfMagic_ReturnsFalse()
|
||||
{
|
||||
// Arrange: ELF magic
|
||||
@@ -335,7 +353,8 @@ public class MachoFeatureExtractorTests
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithMacho64Executable_ReturnsCorrectMetadata()
|
||||
{
|
||||
// Arrange: Mach-O 64-bit x86_64 executable
|
||||
@@ -354,7 +373,8 @@ public class MachoFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.Executable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithDylib_ReturnsSharedLibrary()
|
||||
{
|
||||
// Arrange: Mach-O dylib
|
||||
@@ -371,7 +391,8 @@ public class MachoFeatureExtractorTests
|
||||
metadata.Type.Should().Be(BinaryType.SharedLibrary);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractMetadataAsync_WithArm64_ReturnsCorrectArchitecture()
|
||||
{
|
||||
// Arrange: Mach-O arm64
|
||||
@@ -388,7 +409,8 @@ public class MachoFeatureExtractorTests
|
||||
metadata.Architecture.Should().Be("aarch64");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ExtractIdentityAsync_ProducesConsistentBinaryKey()
|
||||
{
|
||||
// Arrange: Same Mach-O content
|
||||
@@ -437,7 +459,8 @@ public class MachoFeatureExtractorTests
|
||||
|
||||
public class BinaryIdentityDeterminismTests
|
||||
{
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AllExtractors_SameContent_ProduceSameHash()
|
||||
{
|
||||
// Arrange: Create identical binary content
|
||||
@@ -472,7 +495,8 @@ public class BinaryIdentityDeterminismTests
|
||||
identity2.BinaryKey.Should().Be(identity3.BinaryKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task DifferentContent_ProducesDifferentHash()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Core\StellaOps.BinaryIndex.Core.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.FixIndex\StellaOps.BinaryIndex.FixIndex.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BasicBlockFingerprintGeneratorTests.cs
|
||||
// Sprint: SPRINT_20251226_013_BINIDX_fingerprint_factory
|
||||
// Task: FPRINT-21 — Add unit tests for fingerprint generation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Fingerprints.Generators;
|
||||
using StellaOps.BinaryIndex.Fingerprints.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Fingerprints.Tests.Generators;
|
||||
|
||||
public class BasicBlockFingerprintGeneratorTests
|
||||
{
|
||||
private readonly BasicBlockFingerprintGenerator _generator;
|
||||
|
||||
public BasicBlockFingerprintGeneratorTests()
|
||||
{
|
||||
_generator = new BasicBlockFingerprintGenerator(
|
||||
NullLogger<BasicBlockFingerprintGenerator>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Algorithm_ReturnsBasicBlock()
|
||||
{
|
||||
_generator.Algorithm.Should().Be(FingerprintAlgorithm.BasicBlock);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(15, false)] // Too small
|
||||
[InlineData(16, true)] // Minimum size
|
||||
[InlineData(100, true)] // Normal size
|
||||
[InlineData(1000, true)] // Large
|
||||
public void CanProcess_ChecksMinimumSize(int size, bool expected)
|
||||
{
|
||||
var input = CreateInput(new byte[size]);
|
||||
_generator.CanProcess(input).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_ProducesValidFingerprint()
|
||||
{
|
||||
// Sample x86_64 code (simplified)
|
||||
var binaryData = new byte[]
|
||||
{
|
||||
0x55, // push rbp
|
||||
0x48, 0x89, 0xe5, // mov rbp, rsp
|
||||
0x48, 0x83, 0xec, 0x10, // sub rsp, 0x10
|
||||
0x89, 0x7d, 0xfc, // mov [rbp-4], edi
|
||||
0x8b, 0x45, 0xfc, // mov eax, [rbp-4]
|
||||
0x83, 0xc0, 0x01, // add eax, 1
|
||||
0x89, 0xc7, // mov edi, eax
|
||||
0xe8, 0x00, 0x00, 0x00, 0x00, // call (placeholder)
|
||||
0xc9, // leave
|
||||
0xc3 // ret
|
||||
};
|
||||
|
||||
var input = CreateInput(binaryData);
|
||||
|
||||
var result = await _generator.GenerateAsync(input);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Hash.Should().HaveCount(16); // 16-byte fingerprint
|
||||
result.FingerprintId.Should().HaveLength(32); // Hex-encoded
|
||||
result.Algorithm.Should().Be(FingerprintAlgorithm.BasicBlock);
|
||||
result.Confidence.Should().BeGreaterThan(0);
|
||||
result.Metadata.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_ProducesDeterministicFingerprints()
|
||||
{
|
||||
var binaryData = new byte[]
|
||||
{
|
||||
0x55, 0x48, 0x89, 0xe5, // Function prologue
|
||||
0x48, 0x83, 0xec, 0x20, // sub rsp, 0x20
|
||||
0xc3 // ret
|
||||
};
|
||||
|
||||
var input = CreateInput(binaryData);
|
||||
|
||||
var result1 = await _generator.GenerateAsync(input);
|
||||
var result2 = await _generator.GenerateAsync(input);
|
||||
|
||||
result1.Hash.Should().BeEquivalentTo(result2.Hash);
|
||||
result1.FingerprintId.Should().Be(result2.FingerprintId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_DifferentCodeProducesDifferentFingerprints()
|
||||
{
|
||||
var binaryData1 = new byte[]
|
||||
{
|
||||
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x10,
|
||||
0x89, 0x7d, 0xfc, 0x8b, 0x45, 0xfc, 0xc3
|
||||
};
|
||||
|
||||
var binaryData2 = new byte[]
|
||||
{
|
||||
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20,
|
||||
0x89, 0x7d, 0xec, 0x8b, 0x45, 0xec, 0xc3
|
||||
};
|
||||
|
||||
var result1 = await _generator.GenerateAsync(CreateInput(binaryData1));
|
||||
var result2 = await _generator.GenerateAsync(CreateInput(binaryData2));
|
||||
|
||||
result1.FingerprintId.Should().NotBe(result2.FingerprintId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateAsync_IncludesMetadata()
|
||||
{
|
||||
var binaryData = new byte[]
|
||||
{
|
||||
0x55, 0x48, 0x89, 0xe5, // Block 1
|
||||
0xe8, 0x00, 0x00, 0x00, 0x00, // call
|
||||
0x48, 0x85, 0xc0, // Block 2
|
||||
0x74, 0x05, // je (conditional)
|
||||
0xb8, 0x01, 0x00, 0x00, 0x00, // Block 3: mov eax, 1
|
||||
0xc3, // ret
|
||||
0xb8, 0x00, 0x00, 0x00, 0x00, // Block 4: mov eax, 0
|
||||
0xc3 // ret
|
||||
};
|
||||
|
||||
var result = await _generator.GenerateAsync(CreateInput(binaryData));
|
||||
|
||||
result.Metadata.Should().NotBeNull();
|
||||
result.Metadata!.BasicBlockCount.Should().BeGreaterThan(0);
|
||||
result.Metadata!.FunctionSize.Should().Be(binaryData.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("x86_64")]
|
||||
[InlineData("amd64")]
|
||||
[InlineData("aarch64")]
|
||||
public async Task GenerateAsync_SupportsMultipleArchitectures(string arch)
|
||||
{
|
||||
var input = CreateInput(new byte[32], architecture: arch);
|
||||
|
||||
var result = await _generator.GenerateAsync(input);
|
||||
|
||||
result.Should().NotBeNull();
|
||||
result.Hash.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
private static FingerprintInput CreateInput(
|
||||
byte[] binaryData,
|
||||
string architecture = "x86_64")
|
||||
{
|
||||
return new FingerprintInput
|
||||
{
|
||||
BinaryData = binaryData,
|
||||
Architecture = architecture,
|
||||
CveId = "CVE-2024-TEST",
|
||||
Component = "test-component"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FingerprintMatcherTests.cs
|
||||
// Sprint: SPRINT_20251226_013_BINIDX_fingerprint_factory
|
||||
// Task: FPRINT-22 — Add integration tests for matching pipeline
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.BinaryIndex.Fingerprints.Matching;
|
||||
using StellaOps.BinaryIndex.Fingerprints.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Fingerprints.Tests.Matching;
|
||||
|
||||
public class FingerprintMatcherTests
|
||||
{
|
||||
private readonly Mock<IFingerprintRepository> _repositoryMock;
|
||||
private readonly FingerprintMatcher _matcher;
|
||||
|
||||
public FingerprintMatcherTests()
|
||||
{
|
||||
_repositoryMock = new Mock<IFingerprintRepository>();
|
||||
_matcher = new FingerprintMatcher(
|
||||
NullLogger<FingerprintMatcher>.Instance,
|
||||
_repositoryMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_NoCandidate_ReturnsNoMatch()
|
||||
{
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray<VulnFingerprint>.Empty);
|
||||
|
||||
var fingerprint = new byte[16];
|
||||
var result = await _matcher.MatchAsync(fingerprint);
|
||||
|
||||
result.IsMatch.Should().BeFalse();
|
||||
result.Similarity.Should().Be(0);
|
||||
result.MatchedFingerprint.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_ExactMatch_ReturnsMatch()
|
||||
{
|
||||
var testFingerprint = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
var storedFingerprint = CreateStoredFingerprint(testFingerprint);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
var result = await _matcher.MatchAsync(testFingerprint);
|
||||
|
||||
result.IsMatch.Should().BeTrue();
|
||||
result.Similarity.Should().Be(1.0m);
|
||||
result.MatchedFingerprint.Should().NotBeNull();
|
||||
result.MatchedFingerprint!.CveId.Should().Be("CVE-2024-TEST");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_SimilarMatch_ReturnsMatchAboveThreshold()
|
||||
{
|
||||
var testFingerprint = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
// Change one byte - should still match with high similarity
|
||||
var storedHash = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17 };
|
||||
var storedFingerprint = CreateStoredFingerprint(storedHash);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
var result = await _matcher.MatchAsync(testFingerprint, new MatchOptions { MinSimilarity = 0.9m });
|
||||
|
||||
result.IsMatch.Should().BeTrue();
|
||||
result.Similarity.Should().BeGreaterThan(0.9m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_DissimilarFingerprint_ReturnsNoMatch()
|
||||
{
|
||||
var testFingerprint = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
var storedHash = new byte[] { 255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 245, 244, 243, 242, 241, 240 };
|
||||
var storedFingerprint = CreateStoredFingerprint(storedHash);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
var result = await _matcher.MatchAsync(testFingerprint);
|
||||
|
||||
result.IsMatch.Should().BeFalse();
|
||||
result.Similarity.Should().BeLessThan(0.5m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_RespectsThreshold()
|
||||
{
|
||||
var testFingerprint = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
var storedHash = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 20, 20 };
|
||||
var storedFingerprint = CreateStoredFingerprint(storedHash);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
// With high threshold - no match
|
||||
var highThresholdResult = await _matcher.MatchAsync(testFingerprint, new MatchOptions { MinSimilarity = 0.99m });
|
||||
highThresholdResult.IsMatch.Should().BeFalse();
|
||||
|
||||
// With lower threshold - match
|
||||
var lowThresholdResult = await _matcher.MatchAsync(testFingerprint, new MatchOptions { MinSimilarity = 0.8m });
|
||||
lowThresholdResult.IsMatch.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_RespectsValidatedFilter()
|
||||
{
|
||||
var testFingerprint = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
|
||||
var storedFingerprint = CreateStoredFingerprint(testFingerprint, validated: false);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
var result = await _matcher.MatchAsync(testFingerprint, new MatchOptions { RequireValidated = true });
|
||||
|
||||
result.IsMatch.Should().BeFalse(); // Filtered out because not validated
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchBatchAsync_ProcessesAllFingerprints()
|
||||
{
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray<VulnFingerprint>.Empty);
|
||||
|
||||
var fingerprints = new List<byte[]>
|
||||
{
|
||||
new byte[16],
|
||||
new byte[16],
|
||||
new byte[16]
|
||||
};
|
||||
|
||||
var results = await _matcher.MatchBatchAsync(fingerprints);
|
||||
|
||||
results.Should().HaveCount(3);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 4 }, 1.0)]
|
||||
[InlineData(new byte[] { 1, 2, 3, 4 }, new byte[] { 1, 2, 3, 5 }, 0.9)] // 1 byte different
|
||||
[InlineData(new byte[] { 0, 0, 0, 0 }, new byte[] { 255, 255, 255, 255 }, 0.0)] // Completely different
|
||||
public void CalculateSimilarity_ReturnsExpectedValues(byte[] fp1, byte[] fp2, double expectedApprox)
|
||||
{
|
||||
var similarity = _matcher.CalculateSimilarity(fp1, fp2, FingerprintAlgorithm.BasicBlock);
|
||||
|
||||
similarity.Should().BeApproximately((decimal)expectedApprox, 0.15m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MatchAsync_IncludesMatchDetails()
|
||||
{
|
||||
var testFingerprint = new byte[16];
|
||||
var storedFingerprint = CreateStoredFingerprint(testFingerprint);
|
||||
|
||||
_repositoryMock
|
||||
.Setup(r => r.SearchByHashAsync(
|
||||
It.IsAny<byte[]>(),
|
||||
It.IsAny<FingerprintAlgorithm>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(ImmutableArray.Create(storedFingerprint));
|
||||
|
||||
var result = await _matcher.MatchAsync(testFingerprint);
|
||||
|
||||
result.Details.Should().NotBeNull();
|
||||
result.Details!.MatchingAlgorithm.Should().Be(FingerprintAlgorithm.BasicBlock);
|
||||
result.Details.CandidatesEvaluated.Should().Be(1);
|
||||
result.Details.MatchTimeMs.Should().BeGreaterOrEqualTo(0);
|
||||
}
|
||||
|
||||
private static VulnFingerprint CreateStoredFingerprint(byte[] hash, bool validated = false)
|
||||
{
|
||||
return new VulnFingerprint
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CveId = "CVE-2024-TEST",
|
||||
Component = "test-component",
|
||||
Algorithm = FingerprintAlgorithm.BasicBlock,
|
||||
FingerprintId = Convert.ToHexString(hash).ToLowerInvariant(),
|
||||
FingerprintHash = hash,
|
||||
Architecture = "x86_64",
|
||||
Validated = validated,
|
||||
IndexedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.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="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Fingerprints\StellaOps.BinaryIndex.Fingerprints.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,337 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryIdentityRepositoryTests.cs
|
||||
// Sprint: SPRINT_20251226_011_BINIDX
|
||||
// Task: BINCAT-18 - Integration tests with Testcontainers PostgreSQL
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using StellaOps.BinaryIndex.Core.Models;
|
||||
using StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for BinaryIdentityRepository using real PostgreSQL.
|
||||
/// </summary>
|
||||
[Collection(nameof(BinaryIndexDatabaseCollection))]
|
||||
public sealed class BinaryIdentityRepositoryTests
|
||||
{
|
||||
private readonly BinaryIndexIntegrationFixture _fixture;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public BinaryIdentityRepositoryTests(BinaryIndexIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_InsertsNewIdentity_ReturnsWithId()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"elf:x86_64:{Guid.NewGuid():N}",
|
||||
BuildId = "abc123def456",
|
||||
BuildIdType = "gnu-build-id",
|
||||
FileSha256 = "sha256:abcd1234567890abcdef1234567890abcdef12345678",
|
||||
TextSha256 = "sha256:text1234",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64",
|
||||
OsAbi = "linux",
|
||||
Type = BinaryType.SharedLibrary,
|
||||
IsStripped = false
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().NotBe(Guid.Empty);
|
||||
result.BinaryKey.Should().Be(identity.BinaryKey);
|
||||
result.BuildId.Should().Be(identity.BuildId);
|
||||
result.BuildIdType.Should().Be(identity.BuildIdType);
|
||||
result.FileSha256.Should().Be(identity.FileSha256);
|
||||
result.Format.Should().Be(BinaryFormat.Elf);
|
||||
result.Architecture.Should().Be("x86_64");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_UpdatesExistingIdentity_PreservesOriginalId()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
var binaryKey = $"elf:x86_64:{Guid.NewGuid():N}";
|
||||
|
||||
var original = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = binaryKey,
|
||||
BuildId = "original-build-id",
|
||||
BuildIdType = "gnu-build-id",
|
||||
FileSha256 = "sha256:original",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
var inserted = await repository.UpsertAsync(original, CancellationToken.None);
|
||||
|
||||
var updated = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = binaryKey,
|
||||
BuildId = "original-build-id",
|
||||
BuildIdType = "gnu-build-id",
|
||||
FileSha256 = "sha256:original",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64",
|
||||
LastSeenSnapshotId = Guid.NewGuid()
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await repository.UpsertAsync(updated, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Id.Should().Be(inserted.Id, "upsert should preserve original row ID");
|
||||
result.LastSeenSnapshotId.Should().Be(updated.LastSeenSnapshotId);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByBuildIdAsync_ExistingBuildId_ReturnsIdentity()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
var buildId = $"build-{Guid.NewGuid():N}";
|
||||
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"elf:x86_64:{Guid.NewGuid():N}",
|
||||
BuildId = buildId,
|
||||
BuildIdType = "gnu-build-id",
|
||||
FileSha256 = "sha256:test",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByBuildIdAsync(buildId, "gnu-build-id", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.BuildId.Should().Be(buildId);
|
||||
result.BinaryKey.Should().Be(identity.BinaryKey);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByBuildIdAsync_NonExistentBuildId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByBuildIdAsync("non-existent-build-id", "gnu-build-id", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByKeyAsync_ExistingKey_ReturnsIdentity()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
var binaryKey = $"pe:x86_64:{Guid.NewGuid():N}";
|
||||
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = binaryKey,
|
||||
FileSha256 = "sha256:pe-test",
|
||||
Format = BinaryFormat.Pe,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByKeyAsync(binaryKey, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.BinaryKey.Should().Be(binaryKey);
|
||||
result.Format.Should().Be(BinaryFormat.Pe);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByKeyAsync_NonExistentKey_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByKeyAsync("non-existent-key", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBatchAsync_MultipleExistingKeys_ReturnsAllMatches()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
var identities = Enumerable.Range(0, 5)
|
||||
.Select(i => new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"macho:arm64:{Guid.NewGuid():N}",
|
||||
FileSha256 = $"sha256:batch-test-{i}",
|
||||
Format = BinaryFormat.Macho,
|
||||
Architecture = "arm64"
|
||||
})
|
||||
.ToList();
|
||||
|
||||
foreach (var identity in identities)
|
||||
{
|
||||
await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
}
|
||||
|
||||
var keys = identities.Select(i => i.BinaryKey).ToList();
|
||||
|
||||
// Act
|
||||
var results = await repository.GetBatchAsync(keys, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(5);
|
||||
results.Select(r => r.BinaryKey).Should().BeEquivalentTo(keys);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBatchAsync_MixedExistentAndNonExistentKeys_ReturnsOnlyExisting()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
var existingKey = $"elf:x86_64:{Guid.NewGuid():N}";
|
||||
var nonExistentKey = $"elf:x86_64:non-existent-{Guid.NewGuid():N}";
|
||||
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = existingKey,
|
||||
FileSha256 = "sha256:mixed-test",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64"
|
||||
};
|
||||
|
||||
await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var results = await repository.GetBatchAsync([existingKey, nonExistentKey], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
results.Should().HaveCount(1);
|
||||
results[0].BinaryKey.Should().Be(existingKey);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetBatchAsync_EmptyKeysList_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
// Act
|
||||
var results = await repository.GetBatchAsync([], CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
results.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_AllBinaryFormats_PersistsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
var testCases = new[]
|
||||
{
|
||||
(Format: BinaryFormat.Elf, Arch: "x86_64"),
|
||||
(Format: BinaryFormat.Pe, Arch: "x86"),
|
||||
(Format: BinaryFormat.Macho, Arch: "arm64")
|
||||
};
|
||||
|
||||
foreach (var (format, arch) in testCases)
|
||||
{
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"{format.ToString().ToLowerInvariant()}:{arch}:{Guid.NewGuid():N}",
|
||||
FileSha256 = $"sha256:format-test-{format}",
|
||||
Format = format,
|
||||
Architecture = arch
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Format.Should().Be(format, $"format {format} should be persisted correctly");
|
||||
result.Architecture.Should().Be(arch);
|
||||
}
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task UpsertAsync_AllBinaryTypes_PersistsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new BinaryIdentityRepository(dbContext);
|
||||
|
||||
foreach (var binaryType in Enum.GetValues<BinaryType>())
|
||||
{
|
||||
var identity = new BinaryIdentity
|
||||
{
|
||||
BinaryKey = $"elf:x86_64:{Guid.NewGuid():N}",
|
||||
FileSha256 = $"sha256:type-test-{binaryType}",
|
||||
Format = BinaryFormat.Elf,
|
||||
Architecture = "x86_64",
|
||||
Type = binaryType
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await repository.UpsertAsync(identity, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Type.Should().Be(binaryType, $"binary type {binaryType} should be persisted correctly");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection definition for sharing the PostgreSQL container across tests.
|
||||
/// </summary>
|
||||
[CollectionDefinition(nameof(BinaryIndexDatabaseCollection))]
|
||||
public sealed class BinaryIndexDatabaseCollection : ICollectionFixture<BinaryIndexIntegrationFixture>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// BinaryIndexIntegrationFixture.cs
|
||||
// Sprint: SPRINT_20251226_011_BINIDX
|
||||
// Task: BINCAT-18 - Integration tests with Testcontainers PostgreSQL
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
using Npgsql;
|
||||
using StellaOps.BinaryIndex.Core.Services;
|
||||
using StellaOps.Infrastructure.Postgres.Testing;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL integration test fixture for BinaryIndex module.
|
||||
/// Spins up a real PostgreSQL container and runs schema migrations.
|
||||
/// </summary>
|
||||
public sealed class BinaryIndexIntegrationFixture : PostgresIntegrationFixture
|
||||
{
|
||||
private const string TestTenantId = "00000000-0000-0000-0000-000000000001";
|
||||
private NpgsqlDataSource? _dataSource;
|
||||
|
||||
protected override Assembly? GetMigrationAssembly()
|
||||
=> typeof(BinaryIndexDbContext).Assembly;
|
||||
|
||||
protected override string GetModuleName() => "BinaryIndex";
|
||||
|
||||
protected override string? GetResourcePrefix() => "StellaOps.BinaryIndex.Persistence.Migrations";
|
||||
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
_dataSource = NpgsqlDataSource.Create(ConnectionString);
|
||||
}
|
||||
|
||||
public override async Task DisposeAsync()
|
||||
{
|
||||
if (_dataSource != null)
|
||||
{
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new database context with the test tenant configured.
|
||||
/// </summary>
|
||||
public BinaryIndexDbContext CreateDbContext(string? tenantId = null)
|
||||
{
|
||||
if (_dataSource == null)
|
||||
{
|
||||
throw new InvalidOperationException("Fixture not initialized. Call InitializeAsync first.");
|
||||
}
|
||||
|
||||
var tenant = new TestTenantContext(tenantId ?? TestTenantId);
|
||||
return new BinaryIndexDbContext(_dataSource, tenant);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default test tenant ID.
|
||||
/// </summary>
|
||||
public string GetTestTenantId() => TestTenantId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple tenant context implementation for testing.
|
||||
/// </summary>
|
||||
internal sealed class TestTenantContext : ITenantContext
|
||||
{
|
||||
public TestTenantContext(string tenantId)
|
||||
{
|
||||
TenantId = tenantId;
|
||||
}
|
||||
|
||||
public string TenantId { get; }
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// CorpusSnapshotRepositoryTests.cs
|
||||
// Sprint: SPRINT_20251226_011_BINIDX
|
||||
// Task: BINCAT-18 - Integration tests with Testcontainers PostgreSQL
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.BinaryIndex.Corpus;
|
||||
using StellaOps.BinaryIndex.Persistence.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.BinaryIndex.Persistence.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for CorpusSnapshotRepository using real PostgreSQL.
|
||||
/// </summary>
|
||||
[Collection(nameof(BinaryIndexDatabaseCollection))]
|
||||
public sealed class CorpusSnapshotRepositoryTests
|
||||
{
|
||||
private readonly BinaryIndexIntegrationFixture _fixture;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
public CorpusSnapshotRepositoryTests(BinaryIndexIntegrationFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_NewSnapshot_ReturnsWithId()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
var snapshot = new CorpusSnapshot(
|
||||
Id: Guid.NewGuid(),
|
||||
Distro: "debian",
|
||||
Release: "bookworm",
|
||||
Architecture: "amd64",
|
||||
MetadataDigest: $"sha256:{Guid.NewGuid():N}",
|
||||
CapturedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
// Act
|
||||
var result = await repository.CreateAsync(snapshot, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Id.Should().Be(snapshot.Id);
|
||||
result.Distro.Should().Be("debian");
|
||||
result.Release.Should().Be("bookworm");
|
||||
result.Architecture.Should().Be("amd64");
|
||||
result.MetadataDigest.Should().Be(snapshot.MetadataDigest);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_ExistingSnapshot_ReturnsSnapshot()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
var snapshot = new CorpusSnapshot(
|
||||
Id: Guid.NewGuid(),
|
||||
Distro: "ubuntu",
|
||||
Release: "noble",
|
||||
Architecture: "arm64",
|
||||
MetadataDigest: $"sha256:{Guid.NewGuid():N}",
|
||||
CapturedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
await repository.CreateAsync(snapshot, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByIdAsync(snapshot.Id, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(snapshot.Id);
|
||||
result.Distro.Should().Be("ubuntu");
|
||||
result.Release.Should().Be("noble");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetByIdAsync_NonExistentId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
// Act
|
||||
var result = await repository.GetByIdAsync(Guid.NewGuid(), CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindByKeyAsync_ExistingKey_ReturnsLatestSnapshot()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
var distro = $"testdistro-{Guid.NewGuid():N}";
|
||||
var release = "v1.0";
|
||||
var architecture = "x86_64";
|
||||
|
||||
// Create older snapshot
|
||||
var older = new CorpusSnapshot(
|
||||
Id: Guid.NewGuid(),
|
||||
Distro: distro,
|
||||
Release: release,
|
||||
Architecture: architecture,
|
||||
MetadataDigest: "sha256:older",
|
||||
CapturedAt: DateTimeOffset.UtcNow.AddDays(-1));
|
||||
|
||||
await repository.CreateAsync(older, CancellationToken.None);
|
||||
|
||||
// Create newer snapshot
|
||||
var newer = new CorpusSnapshot(
|
||||
Id: Guid.NewGuid(),
|
||||
Distro: distro,
|
||||
Release: release,
|
||||
Architecture: architecture,
|
||||
MetadataDigest: "sha256:newer",
|
||||
CapturedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
await repository.CreateAsync(newer, CancellationToken.None);
|
||||
|
||||
// Act
|
||||
var result = await repository.FindByKeyAsync(distro, release, architecture, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(newer.Id, "should return most recent snapshot");
|
||||
result.MetadataDigest.Should().Be("sha256:newer");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task FindByKeyAsync_NonExistentKey_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
// Act
|
||||
var result = await repository.FindByKeyAsync("nonexistent", "v1.0", "x86_64", CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task CreateAsync_MultipleSnapshots_AllPersisted()
|
||||
{
|
||||
// Arrange
|
||||
var dbContext = _fixture.CreateDbContext();
|
||||
var repository = new CorpusSnapshotRepository(dbContext, NullLogger<CorpusSnapshotRepository>.Instance);
|
||||
|
||||
var distros = new[] { "debian", "ubuntu", "alpine" };
|
||||
var snapshots = distros.Select(d => new CorpusSnapshot(
|
||||
Id: Guid.NewGuid(),
|
||||
Distro: $"{d}-{Guid.NewGuid():N}",
|
||||
Release: "latest",
|
||||
Architecture: "amd64",
|
||||
MetadataDigest: $"sha256:{d}",
|
||||
CapturedAt: DateTimeOffset.UtcNow)).ToList();
|
||||
|
||||
// Act
|
||||
foreach (var snapshot in snapshots)
|
||||
{
|
||||
await repository.CreateAsync(snapshot, CancellationToken.None);
|
||||
}
|
||||
|
||||
// Assert
|
||||
foreach (var snapshot in snapshots)
|
||||
{
|
||||
var result = await repository.GetByIdAsync(snapshot.Id, CancellationToken.None);
|
||||
result.Should().NotBeNull();
|
||||
result!.Id.Should().Be(snapshot.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?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.BinaryIndex.Persistence.Tests</RootNamespace>
|
||||
<AssemblyName>StellaOps.BinaryIndex.Persistence.Tests</AssemblyName>
|
||||
<Description>PostgreSQL integration tests for BinaryIndex module using Testcontainers</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.BinaryIndex.Persistence\StellaOps.BinaryIndex.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.TestKit/StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user