using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.Cryptography; using StellaOps.Replay.Core; using StellaOps.Scanner.Cache; using StellaOps.Scanner.Cache.Abstractions; using StellaOps.Scanner.Cache.FileCas; using StellaOps.Scanner.Core; using StellaOps.Scanner.ProofSpine; using StellaOps.Scanner.Reachability.Slices; using StellaOps.Scanner.WebService.Services; using StellaOps.TestKit; using System.Collections.Immutable; using System.Text; using System.Text.Json; using Xunit; namespace StellaOps.Scanner.WebService.Tests; public sealed class SliceQueryServiceRetrievalTests : IDisposable { private readonly string _tempRoot; private readonly IFileContentAddressableStore _cas; private readonly SliceQueryService _service; public SliceQueryServiceRetrievalTests() { _tempRoot = Path.Combine(Path.GetTempPath(), $"stella-slice-query-{Guid.NewGuid():N}"); var scannerCacheOptions = Microsoft.Extensions.Options.Options.Create(new ScannerCacheOptions { RootPath = _tempRoot, FileTtl = TimeSpan.FromDays(1), MaxBytes = 1024 * 1024 * 10 }); _cas = new FileContentAddressableStore( scannerCacheOptions, NullLogger.Instance, TimeProvider.System); var cryptoHash = CryptoHashFactory.CreateDefault(); var sliceHasher = new SliceHasher(cryptoHash); var sliceSigner = new SliceDsseSigner( new TestDsseSigningService(), new TestCryptoProfile(), sliceHasher, TimeProvider.System); var casStorage = new SliceCasStorage(sliceHasher, sliceSigner, cryptoHash); _service = new SliceQueryService( cache: new SliceCache(Microsoft.Extensions.Options.Options.Create(new SliceCacheOptions())), extractor: new SliceExtractor(new VerdictComputer()), casStorage: casStorage, diffComputer: new StellaOps.Scanner.Reachability.Slices.Replay.SliceDiffComputer(), hasher: sliceHasher, cas: _cas, scannerCacheOptions: scannerCacheOptions, scanRepo: new NullScanMetadataRepository(), timeProvider: TimeProvider.System, options: Microsoft.Extensions.Options.Options.Create(new SliceQueryServiceOptions()), logger: NullLogger.Instance); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceAsync_WhenSliceExistsInCas_ReturnsSlice() { const string digestHex = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; var bytes = JsonSerializer.SerializeToUtf8Bytes(CreateSlice("scan-a")); await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(bytes))); var result = await _service.GetSliceAsync($"sha256:{digestHex}"); Assert.NotNull(result); Assert.Equal("scan-a", result!.Manifest.ScanId); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceAsync_WhenSliceMissingInCas_ReturnsNull() { var result = await _service.GetSliceAsync("sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); Assert.Null(result); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceAsync_WhenSlicePayloadCorrupt_ThrowsDeterministicError() { const string digestHex = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; await _cas.PutAsync(new FileCasPutRequest(digestHex, new MemoryStream(Encoding.UTF8.GetBytes("not-json")))); var action = async () => await _service.GetSliceAsync($"sha256:{digestHex}"); var ex = await Assert.ThrowsAsync(action); Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceDsseAsync_WhenEnvelopeExists_ReturnsEnvelope() { const string digestHex = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; var envelope = new DsseEnvelope( PayloadType: "application/vnd.stellaops.slice+json", Payload: Convert.ToBase64String(Encoding.UTF8.GetBytes("{}")), Signatures: [new DsseSignature("k1", "s1")]); var envelopeBytes = JsonSerializer.SerializeToUtf8Bytes(envelope); await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(envelopeBytes))); var result = await _service.GetSliceDsseAsync($"sha256:{digestHex}"); Assert.NotNull(result); var typed = Assert.IsType(result); Assert.Equal("application/vnd.stellaops.slice+json", typed.PayloadType); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceDsseAsync_WhenEnvelopeCorrupt_ThrowsDeterministicError() { const string digestHex = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; await _cas.PutAsync(new FileCasPutRequest($"{digestHex}.dsse", new MemoryStream(Encoding.UTF8.GetBytes("{broken-json")))); var action = async () => await _service.GetSliceDsseAsync($"sha256:{digestHex}"); var ex = await Assert.ThrowsAsync(action); Assert.Contains("corrupt", ex.Message, StringComparison.OrdinalIgnoreCase); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task GetSliceDsseAsync_WhenEnvelopeMissing_ReturnsNull() { var result = await _service.GetSliceDsseAsync("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); Assert.Null(result); } public void Dispose() { try { Directory.Delete(_tempRoot, recursive: true); } catch (IOException) { } catch (UnauthorizedAccessException) { } } private static ReachabilitySlice CreateSlice(string scanId) { return new ReachabilitySlice { Inputs = new SliceInputs { GraphDigest = "sha256:graph" }, Query = new SliceQuery { CveId = "CVE-2026-0001", TargetSymbols = ImmutableArray.Create("target") }, Subgraph = new SliceSubgraph { Nodes = ImmutableArray.Empty, Edges = ImmutableArray.Empty }, Verdict = new SliceVerdict { Status = SliceVerdictStatus.Unknown, Confidence = 0.4 }, Manifest = ScanManifest.CreateBuilder(scanId, "sha256:artifact") .WithScannerVersion("1.0.0") .WithWorkerVersion("1.0.0") .WithConcelierSnapshot("sha256:concelier") .WithExcititorSnapshot("sha256:excititor") .WithLatticePolicyHash("sha256:policy") .Build() }; } private sealed class TestCryptoProfile : ICryptoProfile { public string KeyId => "test-key"; public string Algorithm => "hs256"; } private sealed class TestDsseSigningService : IDsseSigningService { public Task SignAsync(object payload, string payloadType, ICryptoProfile cryptoProfile, CancellationToken cancellationToken = default) { var payloadBytes = CanonicalJson.SerializeToUtf8Bytes(payload); var envelope = new DsseEnvelope( PayloadType: payloadType, Payload: Convert.ToBase64String(payloadBytes), Signatures: [new DsseSignature(cryptoProfile.KeyId, "sig")]); return Task.FromResult(envelope); } public Task VerifyAsync(DsseEnvelope envelope, CancellationToken cancellationToken = default) => Task.FromResult(new DsseVerificationOutcome(true, true, null)); } private sealed class NullScanMetadataRepository : IScanMetadataRepository { public Task GetScanMetadataAsync(string scanId, CancellationToken cancellationToken = default) => Task.FromResult(null); } }