using Microsoft.Extensions.Logging.Abstractions; using Moq; using StellaOps.Findings.Ledger.Domain; using StellaOps.Findings.Ledger.Infrastructure; using StellaOps.Findings.Ledger.Infrastructure.Attestation; using StellaOps.Findings.Ledger.Services; using Xunit; namespace StellaOps.Findings.Ledger.Tests.Attestation; public class AttestationPointerServiceTests { private readonly Mock _ledgerEventRepository; private readonly Mock _writeService; private readonly InMemoryAttestationPointerRepository _repository; private readonly FakeTimeProvider _timeProvider; private readonly AttestationPointerService _service; public AttestationPointerServiceTests() { _ledgerEventRepository = new Mock(); _writeService = new Mock(); _repository = new InMemoryAttestationPointerRepository(); _timeProvider = new FakeTimeProvider(new DateTimeOffset(2025, 1, 1, 12, 0, 0, TimeSpan.Zero)); _writeService.Setup(w => w.AppendAsync(It.IsAny(), It.IsAny())) .ReturnsAsync((LedgerEventDraft draft, CancellationToken _) => { var record = new LedgerEventRecord( draft.TenantId, draft.ChainId, draft.SequenceNumber, draft.EventId, draft.EventType, draft.PolicyVersion, draft.FindingId, draft.ArtifactId, draft.SourceRunId, draft.ActorId, draft.ActorType, draft.OccurredAt, draft.RecordedAt, draft.Payload, "event-hash", draft.ProvidedPreviousHash ?? LedgerEventConstants.EmptyHash, "merkle-leaf-hash", draft.CanonicalEnvelope.ToJsonString()); return LedgerWriteResult.Success(record); }); _service = new AttestationPointerService( _ledgerEventRepository.Object, _writeService.Object, _repository, _timeProvider, NullLogger.Instance); } [Fact] public async Task CreatePointer_CreatesNewPointer() { var input = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-123", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:abc123def456789012345678901234567890123456789012345678901234abcd", AttestationId: Guid.NewGuid(), StorageUri: "s3://attestations/test.json", PayloadType: "application/vnd.in-toto+json", PredicateType: "https://slsa.dev/provenance/v1"), VerificationResult: new VerificationResult( Verified: true, VerifiedAt: _timeProvider.GetUtcNow(), Verifier: "stellaops-attestor", VerifierVersion: "2025.01.0"), CreatedBy: "test-system"); var result = await _service.CreatePointerAsync(input); Assert.True(result.Success); Assert.NotNull(result.PointerId); Assert.NotNull(result.LedgerEventId); Assert.Null(result.Error); var saved = await _repository.GetByIdAsync("tenant-1", result.PointerId!.Value, CancellationToken.None); Assert.NotNull(saved); Assert.Equal(input.FindingId, saved!.FindingId); Assert.Equal(input.AttestationType, saved.AttestationType); Assert.Equal(input.AttestationRef.Digest, saved.AttestationRef.Digest); } [Fact] public async Task CreatePointer_IsIdempotent() { var input = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-456", AttestationType: AttestationType.SlsaProvenance, Relationship: AttestationRelationship.AttestedBy, AttestationRef: new AttestationRef( Digest: "sha256:def456789012345678901234567890123456789012345678901234567890abcd"), CreatedBy: "test-system"); var result1 = await _service.CreatePointerAsync(input); var result2 = await _service.CreatePointerAsync(input); Assert.True(result1.Success); Assert.True(result2.Success); Assert.Equal(result1.PointerId, result2.PointerId); var pointers = await _repository.GetByFindingIdAsync("tenant-1", "finding-456", CancellationToken.None); Assert.Single(pointers); } [Fact] public async Task GetPointers_ReturnsAllPointersForFinding() { var input1 = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-multi", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:aaa111222333444555666777888999000111222333444555666777888999000a")); var input2 = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-multi", AttestationType: AttestationType.VexAttestation, Relationship: AttestationRelationship.DerivedFrom, AttestationRef: new AttestationRef( Digest: "sha256:bbb111222333444555666777888999000111222333444555666777888999000b")); await _service.CreatePointerAsync(input1); await _service.CreatePointerAsync(input2); var pointers = await _service.GetPointersAsync("tenant-1", "finding-multi"); Assert.Equal(2, pointers.Count); Assert.Contains(pointers, p => p.AttestationType == AttestationType.DsseEnvelope); Assert.Contains(pointers, p => p.AttestationType == AttestationType.VexAttestation); } [Fact] public async Task GetSummary_CalculatesCorrectCounts() { var verified = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-summary", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:ver111222333444555666777888999000111222333444555666777888999000a"), VerificationResult: new VerificationResult(Verified: true, VerifiedAt: _timeProvider.GetUtcNow())); var unverified = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-summary", AttestationType: AttestationType.SbomAttestation, Relationship: AttestationRelationship.DerivedFrom, AttestationRef: new AttestationRef( Digest: "sha256:unv111222333444555666777888999000111222333444555666777888999000b")); await _service.CreatePointerAsync(verified); await _service.CreatePointerAsync(unverified); var summary = await _service.GetSummaryAsync("tenant-1", "finding-summary"); Assert.Equal("finding-summary", summary.FindingId); Assert.Equal(2, summary.AttestationCount); Assert.Equal(1, summary.VerifiedCount); Assert.Equal(OverallVerificationStatus.PartiallyVerified, summary.OverallVerificationStatus); Assert.Contains(AttestationType.DsseEnvelope, summary.AttestationTypes); Assert.Contains(AttestationType.SbomAttestation, summary.AttestationTypes); } [Fact] public async Task Search_FiltersByAttestationType() { var input1 = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-search-1", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:sea111222333444555666777888999000111222333444555666777888999000a")); var input2 = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-search-2", AttestationType: AttestationType.SlsaProvenance, Relationship: AttestationRelationship.AttestedBy, AttestationRef: new AttestationRef( Digest: "sha256:sea222333444555666777888999000111222333444555666777888999000111b")); await _service.CreatePointerAsync(input1); await _service.CreatePointerAsync(input2); var query = new AttestationPointerQuery( TenantId: "tenant-1", AttestationTypes: new[] { AttestationType.DsseEnvelope }); var results = await _service.SearchAsync(query); Assert.Single(results); Assert.Equal(AttestationType.DsseEnvelope, results[0].AttestationType); } [Fact] public async Task UpdateVerificationResult_UpdatesExistingPointer() { var input = new AttestationPointerInput( TenantId: "tenant-1", FindingId: "finding-update", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:upd111222333444555666777888999000111222333444555666777888999000a")); var createResult = await _service.CreatePointerAsync(input); Assert.True(createResult.Success); var verificationResult = new VerificationResult( Verified: true, VerifiedAt: _timeProvider.GetUtcNow(), Verifier: "external-verifier", VerifierVersion: "1.0.0", Checks: new[] { new VerificationCheck(VerificationCheckType.SignatureValid, true, "ECDSA verified"), new VerificationCheck(VerificationCheckType.CertificateValid, true, "Chain verified") }); var success = await _service.UpdateVerificationResultAsync( "tenant-1", createResult.PointerId!.Value, verificationResult); Assert.True(success); var updated = await _repository.GetByIdAsync("tenant-1", createResult.PointerId!.Value, CancellationToken.None); Assert.NotNull(updated?.VerificationResult); Assert.True(updated.VerificationResult!.Verified); Assert.Equal("external-verifier", updated.VerificationResult.Verifier); Assert.Equal(2, updated.VerificationResult.Checks!.Count); } [Fact] public async Task TenantIsolation_PreventsAccessAcrossTenants() { var input = new AttestationPointerInput( TenantId: "tenant-a", FindingId: "finding-isolated", AttestationType: AttestationType.DsseEnvelope, Relationship: AttestationRelationship.VerifiedBy, AttestationRef: new AttestationRef( Digest: "sha256:iso111222333444555666777888999000111222333444555666777888999000a")); var result = await _service.CreatePointerAsync(input); Assert.True(result.Success); var fromTenantA = await _service.GetPointersAsync("tenant-a", "finding-isolated"); var fromTenantB = await _service.GetPointersAsync("tenant-b", "finding-isolated"); Assert.Single(fromTenantA); Assert.Empty(fromTenantB); } private sealed class FakeTimeProvider : TimeProvider { private DateTimeOffset _now; public FakeTimeProvider(DateTimeOffset now) => _now = now; public override DateTimeOffset GetUtcNow() => _now; public void Advance(TimeSpan duration) => _now = _now.Add(duration); } } /// /// In-memory implementation for testing. /// internal sealed class InMemoryAttestationPointerRepository : IAttestationPointerRepository { private readonly List _records = new(); private readonly object _lock = new(); public Task InsertAsync(AttestationPointerRecord record, CancellationToken cancellationToken) { lock (_lock) { _records.Add(record); } return Task.CompletedTask; } public Task GetByIdAsync(string tenantId, Guid pointerId, CancellationToken cancellationToken) { lock (_lock) { var result = _records.FirstOrDefault(r => r.TenantId == tenantId && r.PointerId == pointerId); return Task.FromResult(result); } } public Task> GetByFindingIdAsync(string tenantId, string findingId, CancellationToken cancellationToken) { lock (_lock) { var results = _records .Where(r => r.TenantId == tenantId && r.FindingId == findingId) .OrderByDescending(r => r.CreatedAt) .ToList(); return Task.FromResult>(results); } } public Task> GetByDigestAsync(string tenantId, string digest, CancellationToken cancellationToken) { lock (_lock) { var results = _records .Where(r => r.TenantId == tenantId && r.AttestationRef.Digest == digest) .OrderByDescending(r => r.CreatedAt) .ToList(); return Task.FromResult>(results); } } public Task> SearchAsync(AttestationPointerQuery query, CancellationToken cancellationToken) { lock (_lock) { var results = _records.Where(r => r.TenantId == query.TenantId); if (query.FindingIds is { Count: > 0 }) { results = results.Where(r => query.FindingIds.Contains(r.FindingId)); } if (query.AttestationTypes is { Count: > 0 }) { results = results.Where(r => query.AttestationTypes.Contains(r.AttestationType)); } if (query.VerificationStatus.HasValue) { results = query.VerificationStatus.Value switch { AttestationVerificationFilter.Verified => results.Where(r => r.VerificationResult?.Verified == true), AttestationVerificationFilter.Unverified => results.Where(r => r.VerificationResult is null), AttestationVerificationFilter.Failed => results.Where(r => r.VerificationResult?.Verified == false), _ => results }; } if (query.CreatedAfter.HasValue) { results = results.Where(r => r.CreatedAt >= query.CreatedAfter.Value); } if (query.CreatedBefore.HasValue) { results = results.Where(r => r.CreatedAt <= query.CreatedBefore.Value); } if (!string.IsNullOrWhiteSpace(query.SignerIdentity)) { results = results.Where(r => r.AttestationRef.SignerInfo?.Subject == query.SignerIdentity); } if (!string.IsNullOrWhiteSpace(query.PredicateType)) { results = results.Where(r => r.AttestationRef.PredicateType == query.PredicateType); } var list = results .OrderByDescending(r => r.CreatedAt) .Skip(query.Offset) .Take(query.Limit) .ToList(); return Task.FromResult>(list); } } public Task GetSummaryAsync(string tenantId, string findingId, CancellationToken cancellationToken) { lock (_lock) { var pointers = _records.Where(r => r.TenantId == tenantId && r.FindingId == findingId).ToList(); if (pointers.Count == 0) { return Task.FromResult(new FindingAttestationSummary( findingId, 0, 0, null, Array.Empty(), OverallVerificationStatus.NoAttestations)); } var verifiedCount = pointers.Count(p => p.VerificationResult?.Verified == true); var latest = pointers.Max(p => p.CreatedAt); var types = pointers.Select(p => p.AttestationType).Distinct().ToList(); var status = pointers.Count switch { 0 => OverallVerificationStatus.NoAttestations, _ when verifiedCount == pointers.Count => OverallVerificationStatus.AllVerified, _ when verifiedCount > 0 => OverallVerificationStatus.PartiallyVerified, _ => OverallVerificationStatus.NoneVerified }; return Task.FromResult(new FindingAttestationSummary( findingId, pointers.Count, verifiedCount, latest, types, status)); } } public Task> GetSummariesAsync(string tenantId, IReadOnlyList findingIds, CancellationToken cancellationToken) { var tasks = findingIds.Select(fid => GetSummaryAsync(tenantId, fid, cancellationToken)); return Task.WhenAll(tasks).ContinueWith(t => (IReadOnlyList)t.Result.ToList()); } public Task ExistsAsync(string tenantId, string findingId, string digest, AttestationType attestationType, CancellationToken cancellationToken) { lock (_lock) { var exists = _records.Any(r => r.TenantId == tenantId && r.FindingId == findingId && r.AttestationRef.Digest == digest && r.AttestationType == attestationType); return Task.FromResult(exists); } } public Task UpdateVerificationResultAsync(string tenantId, Guid pointerId, VerificationResult verificationResult, CancellationToken cancellationToken) { lock (_lock) { var idx = _records.FindIndex(r => r.TenantId == tenantId && r.PointerId == pointerId); if (idx >= 0) { var old = _records[idx]; _records[idx] = old with { VerificationResult = verificationResult }; } } return Task.CompletedTask; } public Task GetCountAsync(string tenantId, string findingId, CancellationToken cancellationToken) { lock (_lock) { var count = _records.Count(r => r.TenantId == tenantId && r.FindingId == findingId); return Task.FromResult(count); } } public Task> GetFindingIdsWithAttestationsAsync(string tenantId, AttestationVerificationFilter? verificationFilter, IReadOnlyList? attestationTypes, int limit, int offset, CancellationToken cancellationToken) { lock (_lock) { var results = _records.Where(r => r.TenantId == tenantId); if (attestationTypes is { Count: > 0 }) { results = results.Where(r => attestationTypes.Contains(r.AttestationType)); } if (verificationFilter.HasValue) { results = verificationFilter.Value switch { AttestationVerificationFilter.Verified => results.Where(r => r.VerificationResult?.Verified == true), AttestationVerificationFilter.Unverified => results.Where(r => r.VerificationResult is null), AttestationVerificationFilter.Failed => results.Where(r => r.VerificationResult?.Verified == false), _ => results }; } var list = results .Select(r => r.FindingId) .Distinct() .OrderBy(f => f) .Skip(offset) .Take(limit) .ToList(); return Task.FromResult>(list); } } }