// // Copyright (c) StellaOps. Licensed under BUSL-1.1. // // ----------------------------------------------------------------------------- // FacetSealIntegrationTests.cs // Sprint: SPRINT_20260105_002_002_FACET // Task: FCT-020 - Integration tests: Extraction from real image layers // Description: Integration tests for facet seal extraction from tar and OCI layers. // ----------------------------------------------------------------------------- using System.Formats.Tar; using System.IO.Compression; using System.Text; using FluentAssertions; using Microsoft.Extensions.Time.Testing; using StellaOps.Facet; using Xunit; namespace StellaOps.Scanner.Surface.FS.Tests; /// /// Integration tests for facet seal extraction from tar and OCI layers. /// [Trait("Category", "Integration")] public sealed class FacetSealIntegrationTests : IDisposable { private readonly FakeTimeProvider _timeProvider; private readonly GlobFacetExtractor _facetExtractor; private readonly FacetSealExtractor _sealExtractor; private readonly string _testDir; public FacetSealIntegrationTests() { _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 1, 6, 12, 0, 0, TimeSpan.Zero)); _facetExtractor = new GlobFacetExtractor(_timeProvider); _sealExtractor = new FacetSealExtractor(_facetExtractor, _timeProvider); _testDir = Path.Combine(Path.GetTempPath(), $"facet-integration-{Guid.NewGuid():N}"); Directory.CreateDirectory(_testDir); } public void Dispose() { if (Directory.Exists(_testDir)) { Directory.Delete(_testDir, recursive: true); } } #region Helper Methods private MemoryStream CreateTarArchive(Dictionary files) { var stream = new MemoryStream(); using (var tarWriter = new TarWriter(stream, TarEntryFormat.Pax, leaveOpen: true)) { foreach (var (path, content) in files) { var entry = new PaxTarEntry(TarEntryType.RegularFile, path.TrimStart('/')) { DataStream = new MemoryStream(Encoding.UTF8.GetBytes(content)) }; tarWriter.WriteEntry(entry); } } stream.Position = 0; return stream; } private MemoryStream CreateOciLayer(Dictionary files) { var tarStream = CreateTarArchive(files); var gzipStream = new MemoryStream(); using (var gzip = new GZipStream(gzipStream, CompressionMode.Compress, leaveOpen: true)) { tarStream.CopyTo(gzip); } gzipStream.Position = 0; return gzipStream; } #endregion #region Tar Extraction Tests [Fact] public async Task ExtractFromTarAsync_ValidTar_ExtractsFacets() { // Arrange var files = new Dictionary { { "/var/lib/dpkg/status", "Package: nginx\nVersion: 1.0" }, { "/etc/nginx/nginx.conf", "server { listen 80; }" }, { "/usr/bin/nginx", "binary_content" } }; using var tarStream = CreateTarArchive(files); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); result!.Facets.Should().NotBeEmpty(); result.CombinedMerkleRoot.Should().StartWith("sha256:"); result.Stats.Should().NotBeNull(); result.Stats!.TotalFilesProcessed.Should().BeGreaterThanOrEqualTo(3); } [Fact] public async Task ExtractFromTarAsync_EmptyTar_ReturnsEmptyFacets() { // Arrange using var tarStream = CreateTarArchive(new Dictionary()); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); result!.Facets.Should().BeEmpty(); } [Fact] public async Task ExtractFromTarAsync_MatchesDpkgFacet() { // Arrange var files = new Dictionary { { "/var/lib/dpkg/status", "Package: openssl\nVersion: 3.0.0" }, { "/var/lib/dpkg/info/openssl.list", "/usr/lib/libssl.so" } }; using var tarStream = CreateTarArchive(files); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); var dpkgFacet = result!.Facets.FirstOrDefault(f => f.FacetId == "os-packages-dpkg"); dpkgFacet.Should().NotBeNull(); dpkgFacet!.FileCount.Should().BeGreaterThanOrEqualTo(1); } [Fact] public async Task ExtractFromTarAsync_MatchesNodeModulesFacet() { // Arrange var files = new Dictionary { { "/app/node_modules/express/package.json", "{\"name\":\"express\",\"version\":\"4.18.0\"}" }, { "/app/package-lock.json", "{\"lockfileVersion\":3}" } }; using var tarStream = CreateTarArchive(files); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); var npmFacet = result!.Facets.FirstOrDefault(f => f.FacetId == "lang-deps-npm"); npmFacet.Should().NotBeNull(); } #endregion #region OCI Layer Extraction Tests [Fact] public async Task ExtractFromOciLayersAsync_SingleLayer_ExtractsFacets() { // Arrange var files = new Dictionary { { "/var/lib/dpkg/status", "Package: curl\nVersion: 7.0" }, { "/etc/hosts", "127.0.0.1 localhost" } }; using var layerStream = CreateOciLayer(files); var layers = new[] { layerStream as Stream }; // Act var result = await _sealExtractor.ExtractFromOciLayersAsync( layers, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); result!.Facets.Should().NotBeEmpty(); result.CombinedMerkleRoot.Should().StartWith("sha256:"); } [Fact] public async Task ExtractFromOciLayersAsync_MultipleLayers_MergesFacets() { // Arrange - base layer has dpkg, upper layer adds config var baseLayerFiles = new Dictionary { { "/var/lib/dpkg/status", "Package: base\nVersion: 1.0" } }; var upperLayerFiles = new Dictionary { { "/etc/nginx/nginx.conf", "server {}" } }; using var baseLayer = CreateOciLayer(baseLayerFiles); using var upperLayer = CreateOciLayer(upperLayerFiles); var layers = new[] { baseLayer as Stream, upperLayer as Stream }; // Act var result = await _sealExtractor.ExtractFromOciLayersAsync( layers, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); result!.Stats.Should().NotBeNull(); result.Stats!.TotalFilesProcessed.Should().BeGreaterThanOrEqualTo(2); } #endregion #region Determinism Tests [Fact] public async Task ExtractFromTarAsync_SameTar_ProducesSameMerkleRoot() { // Arrange var files = new Dictionary { { "/var/lib/dpkg/status", "Package: test\nVersion: 1.0" }, { "/etc/test.conf", "config content" } }; using var tarStream1 = CreateTarArchive(files); using var tarStream2 = CreateTarArchive(files); // Act var result1 = await _sealExtractor.ExtractFromTarAsync( tarStream1, ct: TestContext.Current.CancellationToken); var result2 = await _sealExtractor.ExtractFromTarAsync( tarStream2, ct: TestContext.Current.CancellationToken); // Assert result1.Should().NotBeNull(); result2.Should().NotBeNull(); result1!.CombinedMerkleRoot.Should().Be(result2!.CombinedMerkleRoot); } [Fact] public async Task ExtractFromTarAsync_DifferentContent_ProducesDifferentMerkleRoot() { // Arrange var files1 = new Dictionary { { "/var/lib/dpkg/status", "Package: test\nVersion: 1.0" } }; var files2 = new Dictionary { { "/var/lib/dpkg/status", "Package: test\nVersion: 2.0" } }; using var tarStream1 = CreateTarArchive(files1); using var tarStream2 = CreateTarArchive(files2); // Act var result1 = await _sealExtractor.ExtractFromTarAsync( tarStream1, ct: TestContext.Current.CancellationToken); var result2 = await _sealExtractor.ExtractFromTarAsync( tarStream2, ct: TestContext.Current.CancellationToken); // Assert result1.Should().NotBeNull(); result2.Should().NotBeNull(); result1!.CombinedMerkleRoot.Should().NotBe(result2!.CombinedMerkleRoot); } #endregion #region Options Tests [Fact] public async Task ExtractFromTarAsync_Disabled_ReturnsNull() { // Arrange var files = new Dictionary { { "/var/lib/dpkg/status", "Package: test" } }; using var tarStream = CreateTarArchive(files); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, FacetSealExtractionOptions.Disabled, TestContext.Current.CancellationToken); // Assert result.Should().BeNull(); } [Fact] public async Task ExtractFromOciLayersAsync_Disabled_ReturnsNull() { // Arrange using var layer = CreateOciLayer(new Dictionary { { "/etc/test.conf", "content" } }); // Act var result = await _sealExtractor.ExtractFromOciLayersAsync( [layer], FacetSealExtractionOptions.Disabled, TestContext.Current.CancellationToken); // Assert result.Should().BeNull(); } #endregion #region Multi-Facet Category Tests [Fact] public async Task ExtractFromTarAsync_MultipleCategories_AllCategoriesRepresented() { // Arrange - files for multiple facet categories var files = new Dictionary { // OS Packages { "/var/lib/dpkg/status", "Package: nginx" }, // Language Dependencies { "/app/node_modules/express/package.json", "{\"name\":\"express\"}" }, // Configuration { "/etc/nginx/nginx.conf", "server {}" }, // Certificates { "/etc/ssl/certs/ca-cert.pem", "-----BEGIN CERTIFICATE-----" } }; using var tarStream = CreateTarArchive(files); // Act var result = await _sealExtractor.ExtractFromTarAsync( tarStream, ct: TestContext.Current.CancellationToken); // Assert result.Should().NotBeNull(); result!.Facets.Should().HaveCountGreaterThanOrEqualTo(2); var categories = result.Facets.Select(f => f.Category).Distinct().ToList(); categories.Should().HaveCountGreaterThan(1); } #endregion }