test fixes and new product advisories work

This commit is contained in:
master
2026-01-28 02:30:48 +02:00
parent 82caceba56
commit 644887997c
288 changed files with 69101 additions and 375 deletions

View File

@@ -231,4 +231,257 @@ public sealed class ImportValidatorTests
public Task<int> CleanupExpiredAsync(TimeSpan retentionPeriod, CancellationToken cancellationToken = default) =>
Task.FromResult(0);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_WithReferrerValidator_MissingReferrer_ShouldFailAndQuarantine()
{
// Arrange
var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}";
var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
using var rsa = RSA.Create(2048);
var pub = rsa.ExportSubjectPublicKeyInfo();
var payload = "bundle-body";
var payloadType = "application/vnd.stella.bundle";
var pae = BuildPae(payloadType, payload);
var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[]
{
new DsseSignature("k1", Convert.ToBase64String(sig))
});
var trustStore = new TrustStore();
trustStore.LoadActive(new Dictionary<string, byte[]> { ["k1"] = pub });
trustStore.StagePending(new Dictionary<string, byte[]> { ["k2"] = pub });
var quarantine = new CapturingQuarantineService();
var monotonicity = new CapturingMonotonicityChecker();
var referrerValidator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
var validator = new ImportValidator(
new DsseVerifier(),
new TufMetadataValidator(),
new MerkleRootCalculator(),
new RootRotationPolicy(),
monotonicity,
quarantine,
NullLogger<ImportValidator>.Instance,
referrerValidator);
// Manifest with referrer that doesn't exist in entries
var manifestJson = """
{
"version": "1.0.0",
"merkleRoot": "dummy",
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "abcd1234",
"size": 100,
"category": "sbom"
}
]
}
]
}
}
""";
var payloadEntries = new List<NamedStream> { new("a.txt", new MemoryStream("data"u8.ToArray())) };
var merkleRoot = new MerkleRootCalculator().ComputeRoot(payloadEntries);
manifestJson = manifestJson.Replace("\"merkleRoot\": \"dummy\"", $"\"merkleRoot\": \"{merkleRoot}\"");
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var bundlePath = Path.Combine(tempRoot, "bundle.tar.zst");
await File.WriteAllTextAsync(bundlePath, "bundle-bytes");
try
{
var request = new ImportValidationRequest(
TenantId: "tenant-a",
BundleType: "mirror-bundle",
BundleDigest: "sha256:bundle",
BundlePath: bundlePath,
ManifestJson: manifestJson,
ManifestVersion: "1.0.0",
ManifestCreatedAt: DateTimeOffset.Parse("2025-12-15T00:00:00Z"),
ForceActivate: false,
ForceActivateReason: null,
Envelope: envelope,
TrustRoots: new TrustRootConfig("/tmp/root.json", new[] { Fingerprint(pub) }, new[] { "rsassa-pss-sha256" }, null, null, new Dictionary<string, byte[]> { ["k1"] = pub }),
RootJson: root,
SnapshotJson: snapshot,
TimestampJson: timestamp,
PayloadEntries: payloadEntries,
TrustStore: trustStore,
ApproverIds: new[] { "approver-1", "approver-2" });
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeFalse();
result.Reason.Should().Contain("referrer-validation-failed");
result.ReferrerSummary.Should().NotBeNull();
result.ReferrerSummary!.MissingReferrers.Should().Be(1);
quarantine.Requests.Should().HaveCount(1);
}
finally
{
try
{
Directory.Delete(tempRoot, recursive: true);
}
catch
{
// best-effort cleanup
}
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task ValidateAsync_WithReferrerValidator_AllReferrersPresent_ShouldSucceed()
{
// Arrange
var root = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\"}";
var snapshot = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"meta\":{\"snapshot\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
var timestamp = "{\"version\":1,\"expiresUtc\":\"2030-01-01T00:00:00Z\",\"snapshot\":{\"meta\":{\"hashes\":{\"sha256\":\"abc\"}}}}";
using var rsa = RSA.Create(2048);
var pub = rsa.ExportSubjectPublicKeyInfo();
var payload = "bundle-body";
var payloadType = "application/vnd.stella.bundle";
var pae = BuildPae(payloadType, payload);
var sig = rsa.SignData(pae, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var envelope = new DsseEnvelope(payloadType, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload)), new[]
{
new DsseSignature("k1", Convert.ToBase64String(sig))
});
var trustStore = new TrustStore();
trustStore.LoadActive(new Dictionary<string, byte[]> { ["k1"] = pub });
trustStore.StagePending(new Dictionary<string, byte[]> { ["k2"] = pub });
var quarantine = new CapturingQuarantineService();
var monotonicity = new CapturingMonotonicityChecker();
var referrerValidator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
var validator = new ImportValidator(
new DsseVerifier(),
new TufMetadataValidator(),
new MerkleRootCalculator(),
new RootRotationPolicy(),
monotonicity,
quarantine,
NullLogger<ImportValidator>.Instance,
referrerValidator);
// Create referrer content and compute its hash
var referrerContent = "{\"sbom\":\"content\"}"u8.ToArray();
var referrerSha256 = Convert.ToHexString(SHA256.HashData(referrerContent)).ToLowerInvariant();
// Manifest with referrer that exists in entries
var manifestJsonTemplate = """
{
"version": "1.0.0",
"merkleRoot": "MERKLE_PLACEHOLDER",
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "CHECKSUM_PLACEHOLDER",
"size": SIZE_PLACEHOLDER,
"category": "sbom"
}
]
}
]
}
}
""";
var payloadEntries = new List<NamedStream>
{
new("a.txt", new MemoryStream("data"u8.ToArray())),
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(referrerContent))
};
var merkleRoot = new MerkleRootCalculator().ComputeRoot(payloadEntries);
var manifestJson = manifestJsonTemplate
.Replace("MERKLE_PLACEHOLDER", merkleRoot)
.Replace("CHECKSUM_PLACEHOLDER", referrerSha256)
.Replace("SIZE_PLACEHOLDER", referrerContent.Length.ToString());
var tempRoot = Path.Combine(Path.GetTempPath(), "stellaops-airgap-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempRoot);
var bundlePath = Path.Combine(tempRoot, "bundle.tar.zst");
await File.WriteAllTextAsync(bundlePath, "bundle-bytes");
try
{
// Reset streams for re-reading
foreach (var entry in payloadEntries)
{
entry.Stream.Seek(0, SeekOrigin.Begin);
}
var request = new ImportValidationRequest(
TenantId: "tenant-a",
BundleType: "mirror-bundle",
BundleDigest: "sha256:bundle",
BundlePath: bundlePath,
ManifestJson: manifestJson,
ManifestVersion: "1.0.0",
ManifestCreatedAt: DateTimeOffset.Parse("2025-12-15T00:00:00Z"),
ForceActivate: false,
ForceActivateReason: null,
Envelope: envelope,
TrustRoots: new TrustRootConfig("/tmp/root.json", new[] { Fingerprint(pub) }, new[] { "rsassa-pss-sha256" }, null, null, new Dictionary<string, byte[]> { ["k1"] = pub }),
RootJson: root,
SnapshotJson: snapshot,
TimestampJson: timestamp,
PayloadEntries: payloadEntries,
TrustStore: trustStore,
ApproverIds: new[] { "approver-1", "approver-2" });
// Act
var result = await validator.ValidateAsync(request);
// Assert
result.IsValid.Should().BeTrue();
result.ReferrerSummary.Should().NotBeNull();
result.ReferrerSummary!.TotalReferrers.Should().Be(1);
result.ReferrerSummary.ValidReferrers.Should().Be(1);
result.ReferrerSummary.MissingReferrers.Should().Be(0);
quarantine.Requests.Should().BeEmpty();
}
finally
{
try
{
Directory.Delete(tempRoot, recursive: true);
}
catch
{
// best-effort cleanup
}
}
}
}

View File

@@ -0,0 +1,599 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.AirGap.Importer.Validation;
using StellaOps.TestKit;
namespace StellaOps.AirGap.Importer.Tests.Validation;
public sealed class ReferrerValidatorTests
{
private readonly ReferrerValidator _validator;
public ReferrerValidatorTests()
{
_validator = new ReferrerValidator(NullLogger<ReferrerValidator>.Instance);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_NullManifest_ReturnsEmptySummary()
{
// Act
var result = _validator.Validate(null, []);
// Assert
result.Should().NotBeNull();
result.TotalSubjects.Should().Be(0);
result.TotalReferrers.Should().Be(0);
result.IsValid.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_EmptyManifest_ReturnsEmptySummary()
{
// Act
var result = _validator.Validate("", []);
// Assert
result.Should().NotBeNull();
result.TotalSubjects.Should().Be(0);
result.TotalReferrers.Should().Be(0);
result.IsValid.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ManifestWithoutReferrers_ReturnsEmptySummary()
{
// Arrange
var manifest = """{"version":"1.0.0","counts":{"advisories":5}}""";
// Act
var result = _validator.Validate(manifest, []);
// Assert
result.Should().NotBeNull();
result.TotalSubjects.Should().Be(0);
result.TotalReferrers.Should().Be(0);
result.IsValid.Should().BeTrue();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_AllReferrersPresent_ReturnsValid()
{
// Arrange
var content = "test content for referrer"u8.ToArray();
var sha256 = ComputeSha256(content);
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "{{sha256}}",
"size": {{content.Length}},
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.TotalSubjects.Should().Be(1);
result.TotalReferrers.Should().Be(1);
result.ValidReferrers.Should().Be(1);
result.MissingReferrers.Should().Be(0);
result.ChecksumMismatches.Should().Be(0);
result.SizeMismatches.Should().Be(0);
result.Issues.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_MissingReferrer_ReturnsInvalidWithIssue()
{
// Arrange
var manifest = """
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "abcd1234",
"size": 100,
"category": "sbom"
}
]
}
]
}
}
""";
// Act - no entries provided, so referrer is missing
var result = _validator.Validate(manifest, []);
// Assert
result.IsValid.Should().BeFalse();
result.MissingReferrers.Should().Be(1);
result.Issues.Should().HaveCount(1);
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerMissing);
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
result.Issues[0].SubjectDigest.Should().Be("sha256:abc123");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ChecksumMismatch_ReturnsInvalidWithIssue()
{
// Arrange
var content = "test content"u8.ToArray();
var wrongChecksum = "0000000000000000000000000000000000000000000000000000000000000000";
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "{{wrongChecksum}}",
"size": {{content.Length}},
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeFalse();
result.ChecksumMismatches.Should().Be(1);
result.Issues.Should().HaveCount(1);
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerChecksumMismatch);
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
result.Issues[0].ExpectedValue.Should().Be(wrongChecksum);
result.Issues[0].ActualValue.Should().NotBe(wrongChecksum);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_SizeMismatch_ReturnsInvalidWithIssue()
{
// Arrange
var content = "test content"u8.ToArray();
var sha256 = ComputeSha256(content);
var wrongSize = content.Length + 100;
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "{{sha256}}",
"size": {{wrongSize}},
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeFalse();
result.SizeMismatches.Should().Be(1);
result.Issues.Should().HaveCount(1);
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.ReferrerSizeMismatch);
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Error);
result.Issues[0].ExpectedValue.Should().Be(wrongSize.ToString());
result.Issues[0].ActualValue.Should().Be(content.Length.ToString());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_OrphanedReferrer_ReturnsValidWithWarning()
{
// Arrange - manifest has no referrers but bundle has referrer files
var manifest = """{"version":"1.0.0"}""";
var content = "orphaned content"u8.ToArray();
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-orphan.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue(); // Orphans are warnings, not errors
result.OrphanedReferrers.Should().Be(1);
result.Issues.Should().HaveCount(1);
result.Issues[0].IssueType.Should().Be(ReferrerValidationIssueType.OrphanedReferrer);
result.Issues[0].Severity.Should().Be(ReferrerValidationSeverity.Warning);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_MultipleSubjectsAndArtifacts_ValidatesAll()
{
// Arrange
var content1 = "content for subject 1 artifact 1"u8.ToArray();
var content2 = "content for subject 1 artifact 2"u8.ToArray();
var content3 = "content for subject 2 artifact 1"u8.ToArray();
var sha256_1 = ComputeSha256(content1);
var sha256_2 = ComputeSha256(content2);
var sha256_3 = ComputeSha256(content3);
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:subject1",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-subject1/sha256-ref001.json",
"sha256": "{{sha256_1}}",
"size": {{content1.Length}},
"category": "sbom"
},
{
"digest": "sha256:ref002",
"path": "referrers/sha256-subject1/sha256-ref002.json",
"sha256": "{{sha256_2}}",
"size": {{content2.Length}},
"category": "attestation"
}
]
},
{
"subject": "sha256:subject2",
"artifacts": [
{
"digest": "sha256:ref003",
"path": "referrers/sha256-subject2/sha256-ref003.json",
"sha256": "{{sha256_3}}",
"size": {{content3.Length}},
"category": "vex"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-subject1/sha256-ref001.json", new MemoryStream(content1)),
new("referrers/sha256-subject1/sha256-ref002.json", new MemoryStream(content2)),
new("referrers/sha256-subject2/sha256-ref003.json", new MemoryStream(content3))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.TotalSubjects.Should().Be(2);
result.TotalReferrers.Should().Be(3);
result.ValidReferrers.Should().Be(3);
result.Issues.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_MixedErrors_ReportsAllIssues()
{
// Arrange
var validContent = "valid content"u8.ToArray();
var validSha256 = ComputeSha256(validContent);
var wrongChecksum = "0000000000000000000000000000000000000000000000000000000000000000";
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:subject1",
"artifacts": [
{
"digest": "sha256:valid",
"path": "referrers/sha256-subject1/sha256-valid.json",
"sha256": "{{validSha256}}",
"size": {{validContent.Length}},
"category": "sbom"
},
{
"digest": "sha256:missing",
"path": "referrers/sha256-subject1/sha256-missing.json",
"sha256": "abcd1234",
"size": 100,
"category": "attestation"
},
{
"digest": "sha256:badchecksum",
"path": "referrers/sha256-subject1/sha256-badchecksum.json",
"sha256": "{{wrongChecksum}}",
"size": {{validContent.Length}},
"category": "vex"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-subject1/sha256-valid.json", new MemoryStream(validContent)),
new("referrers/sha256-subject1/sha256-badchecksum.json", new MemoryStream(validContent)),
new("referrers/sha256-subject1/sha256-orphan.json", new MemoryStream(validContent))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeFalse();
result.ValidReferrers.Should().Be(1);
result.MissingReferrers.Should().Be(1);
result.ChecksumMismatches.Should().Be(1);
result.OrphanedReferrers.Should().Be(1);
result.Issues.Should().HaveCount(3); // missing, checksum mismatch, orphan
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_PathNormalization_HandlesBackslashes()
{
// Arrange
var content = "test content"u8.ToArray();
var sha256 = ComputeSha256(content);
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers\\sha256-abc123\\sha256-ref001.json",
"sha256": "{{sha256}}",
"size": {{content.Length}},
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.ValidReferrers.Should().Be(1);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_CaseInsensitivePaths_MatchesCorrectly()
{
// Arrange
var content = "test content"u8.ToArray();
var sha256 = ComputeSha256(content);
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "REFERRERS/SHA256-ABC123/SHA256-REF001.JSON",
"sha256": "{{sha256}}",
"size": {{content.Length}},
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.ValidReferrers.Should().Be(1);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_ZeroSizeInManifest_SkipsSizeValidation()
{
// Arrange - when size is 0 or not specified, size validation is skipped
var content = "test content"u8.ToArray();
var sha256 = ComputeSha256(content);
var manifest = $$"""
{
"referrers": {
"subjects": [
{
"subject": "sha256:abc123",
"artifacts": [
{
"digest": "sha256:ref001",
"path": "referrers/sha256-abc123/sha256-ref001.json",
"sha256": "{{sha256}}",
"size": 0,
"category": "sbom"
}
]
}
]
}
}
""";
var entries = new List<NamedStream>
{
new("referrers/sha256-abc123/sha256-ref001.json", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.SizeMismatches.Should().Be(0);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_InvalidJson_ReturnsEmptySummary()
{
// Arrange
var manifest = "this is not valid json {{{";
// Act
var result = _validator.Validate(manifest, []);
// Assert
result.IsValid.Should().BeTrue();
result.TotalReferrers.Should().Be(0);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Validate_NonReferrerFiles_NotReportedAsOrphans()
{
// Arrange
var manifest = """{"version":"1.0.0"}""";
var content = "some content"u8.ToArray();
var entries = new List<NamedStream>
{
new("advisories/adv-001.json", new MemoryStream(content)),
new("sboms/sbom-001.json", new MemoryStream(content)),
new("manifest.yaml", new MemoryStream(content))
};
// Act
var result = _validator.Validate(manifest, entries);
// Assert
result.IsValid.Should().BeTrue();
result.OrphanedReferrers.Should().Be(0);
result.Issues.Should().BeEmpty();
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void IsValid_StaticMethod_ChecksCorrectly()
{
// Valid summary
var valid = new ReferrerValidationSummary
{
TotalReferrers = 5,
ValidReferrers = 5,
MissingReferrers = 0,
ChecksumMismatches = 0,
SizeMismatches = 0,
OrphanedReferrers = 2 // Warnings are OK
};
ReferrerValidator.IsValid(valid).Should().BeTrue();
// Invalid - missing
var missing = valid with { MissingReferrers = 1 };
ReferrerValidator.IsValid(missing).Should().BeFalse();
// Invalid - checksum
var checksum = valid with { ChecksumMismatches = 1 };
ReferrerValidator.IsValid(checksum).Should().BeFalse();
// Invalid - size
var size = valid with { SizeMismatches = 1 };
ReferrerValidator.IsValid(size).Should().BeFalse();
}
private static string ComputeSha256(byte[] data)
{
var hash = System.Security.Cryptography.SHA256.HashData(data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}