doctor enhancements, setup, enhancements, ui functionality and design consolidation and , test projects fixes , product advisory attestation/rekor and delta verfications enhancements

This commit is contained in:
master
2026-01-19 09:02:59 +02:00
parent 8c4bf54aed
commit 17419ba7c4
809 changed files with 170738 additions and 12244 deletions

View File

@@ -0,0 +1,243 @@
// -----------------------------------------------------------------------------
// ArtifactStorePerformanceTests.cs
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
// Task: AS-002 - Performance test: 1000 artifacts store/retrieve
// Description: Performance benchmarks for artifact store operations
// -----------------------------------------------------------------------------
using System.Diagnostics;
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using Xunit;
using Xunit.Abstractions;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Performance")]
public sealed class ArtifactStorePerformanceTests
{
private readonly ITestOutputHelper _output;
public ArtifactStorePerformanceTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
public async Task Store1000Artifacts_CompletesUnderThreshold()
{
// Arrange
const int artifactCount = 1000;
const int maxDurationMs = 30000; // 30 seconds for 1000 artifacts (30ms each avg)
var store = new InMemoryArtifactStore();
var tenantId = Guid.NewGuid();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, tenantId, bomRef);
// Act
var sw = Stopwatch.StartNew();
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
}
sw.Stop();
// Assert
_output.WriteLine($"Stored {artifactCount} artifacts in {sw.ElapsedMilliseconds}ms");
_output.WriteLine($"Average: {sw.ElapsedMilliseconds / (double)artifactCount:F2}ms per artifact");
_output.WriteLine($"Throughput: {artifactCount / sw.Elapsed.TotalSeconds:F2} artifacts/second");
Assert.True(sw.ElapsedMilliseconds < maxDurationMs,
$"Store operation took {sw.ElapsedMilliseconds}ms, expected under {maxDurationMs}ms");
}
[Fact]
public async Task Retrieve1000Artifacts_CompletesUnderThreshold()
{
// Arrange
const int artifactCount = 1000;
const int maxDurationMs = 10000; // 10 seconds for 1000 reads (10ms each avg)
var store = new InMemoryArtifactStore();
var tenantId = Guid.NewGuid();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, tenantId, bomRef);
// Store all artifacts first
var storedArtifacts = new List<(string bomRef, string serial, string id)>();
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
storedArtifacts.Add((artifact.BomRef, artifact.SerialNumber!, artifact.ArtifactId));
}
// Act - Read them all back
var sw = Stopwatch.StartNew();
foreach (var (bRef, serial, id) in storedArtifacts)
{
var result = await store.ReadAsync(bRef, serial, id);
Assert.True(result.Found);
}
sw.Stop();
// Assert
_output.WriteLine($"Retrieved {artifactCount} artifacts in {sw.ElapsedMilliseconds}ms");
_output.WriteLine($"Average: {sw.ElapsedMilliseconds / (double)artifactCount:F2}ms per artifact");
_output.WriteLine($"Throughput: {artifactCount / sw.Elapsed.TotalSeconds:F2} artifacts/second");
Assert.True(sw.ElapsedMilliseconds < maxDurationMs,
$"Retrieve operation took {sw.ElapsedMilliseconds}ms, expected under {maxDurationMs}ms");
}
[Fact]
public async Task ListByBomRef_1000Artifacts_Under100ms()
{
// Arrange
const int artifactCount = 1000;
const int maxDurationMs = 100; // 100ms as per completion criteria
var store = new InMemoryArtifactStore();
var tenantId = Guid.NewGuid();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, tenantId, bomRef);
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
}
// Act
var sw = Stopwatch.StartNew();
var results = await store.ListAsync(bomRef);
sw.Stop();
// Assert
_output.WriteLine($"Listed {results.Count} artifacts in {sw.ElapsedMilliseconds}ms");
Assert.Equal(artifactCount, results.Count);
Assert.True(sw.ElapsedMilliseconds < maxDurationMs,
$"List operation took {sw.ElapsedMilliseconds}ms, expected under {maxDurationMs}ms");
}
[Fact]
public async Task ParallelStore_1000Artifacts_HandlesContention()
{
// Arrange
const int artifactCount = 1000;
const int maxDurationMs = 60000; // 60 seconds with contention
var store = new InMemoryArtifactStore();
var tenantId = Guid.NewGuid();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, tenantId, bomRef);
// Act - Store in parallel
var sw = Stopwatch.StartNew();
await Parallel.ForEachAsync(
artifacts,
new ParallelOptions { MaxDegreeOfParallelism = 10 },
async (artifact, ct) =>
{
await store.StoreAsync(artifact, ct);
});
sw.Stop();
// Assert
_output.WriteLine($"Parallel stored {artifactCount} artifacts in {sw.ElapsedMilliseconds}ms");
_output.WriteLine($"Parallelism: 10, Throughput: {artifactCount / sw.Elapsed.TotalSeconds:F2} artifacts/second");
var stored = await store.ListAsync(bomRef);
Assert.Equal(artifactCount, stored.Count);
Assert.True(sw.ElapsedMilliseconds < maxDurationMs);
}
[Fact]
public async Task MixedOperations_CompletesSuccessfully()
{
// Arrange
const int operationCount = 1000;
var store = new InMemoryArtifactStore();
var tenantId = Guid.NewGuid();
var bomRef = "pkg:docker/mixed-test/app@sha256:abc123";
// Pre-populate with 500 artifacts
var preloadArtifacts = GenerateTestArtifacts(500, tenantId, bomRef);
foreach (var artifact in preloadArtifacts)
{
await store.StoreAsync(artifact);
}
var random = new Random(42); // Deterministic seed for reproducibility
var sw = Stopwatch.StartNew();
// Act - Mix of operations
for (var i = 0; i < operationCount; i++)
{
var op = random.Next(4);
switch (op)
{
case 0: // Store
using (var stream = new MemoryStream(new byte[] { (byte)(i % 256) }))
{
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = $"urn:uuid:mixed-{i}",
ArtifactId = $"mixed-artifact-{i}",
Content = stream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = tenantId
});
}
break;
case 1: // Read existing
var idx = random.Next(preloadArtifacts.Count);
await store.ReadAsync(bomRef, preloadArtifacts[idx].SerialNumber, preloadArtifacts[idx].ArtifactId);
break;
case 2: // List
await store.ListAsync(bomRef);
break;
case 3: // Exists check
var checkIdx = random.Next(preloadArtifacts.Count);
await store.ExistsAsync(bomRef, preloadArtifacts[checkIdx].SerialNumber!, preloadArtifacts[checkIdx].ArtifactId);
break;
}
}
sw.Stop();
// Assert
_output.WriteLine($"Completed {operationCount} mixed operations in {sw.ElapsedMilliseconds}ms");
_output.WriteLine($"Operations/second: {operationCount / sw.Elapsed.TotalSeconds:F2}");
}
private static List<ArtifactStoreRequest> GenerateTestArtifacts(int count, Guid tenantId, string bomRef)
{
var artifacts = new List<ArtifactStoreRequest>();
for (var i = 0; i < count; i++)
{
var content = System.Text.Encoding.UTF8.GetBytes($"{{\"index\": {i}, \"data\": \"test-{Guid.NewGuid()}\"}}");
artifacts.Add(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = $"urn:uuid:{Guid.NewGuid()}",
ArtifactId = $"artifact-{i:D5}",
Content = new MemoryStream(content),
ContentType = "application/json",
Type = (ArtifactType)(i % 5), // Rotate through types
TenantId = tenantId
});
}
return artifacts;
}
}

View File

@@ -0,0 +1,387 @@
// -----------------------------------------------------------------------------
// ArtifactStoreTests.cs
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
// Tasks: AS-001, AS-002, AS-003 - Unit tests
// Description: Unit tests for unified artifact store
// -----------------------------------------------------------------------------
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Unit")]
public sealed class ArtifactStoreTests
{
[Fact]
public async Task InMemoryStore_StoreAndRead_Succeeds()
{
var store = new InMemoryArtifactStore();
var content = System.Text.Encoding.UTF8.GetBytes("{\"test\": true}");
using var contentStream = new MemoryStream(content);
var request = new ArtifactStoreRequest
{
BomRef = "pkg:docker/test/app@sha256:abc123",
SerialNumber = "urn:uuid:12345678-1234-1234-1234-123456789012",
ArtifactId = "artifact-001",
Content = contentStream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = Guid.NewGuid()
};
var storeResult = await store.StoreAsync(request);
Assert.True(storeResult.Success);
Assert.True(storeResult.WasCreated);
Assert.NotNull(storeResult.Sha256);
Assert.Equal(content.Length, storeResult.SizeBytes);
// Read it back
var readResult = await store.ReadAsync(
request.BomRef,
request.SerialNumber,
request.ArtifactId);
Assert.True(readResult.Found);
Assert.NotNull(readResult.Content);
Assert.NotNull(readResult.Metadata);
Assert.Equal(request.BomRef, readResult.Metadata.BomRef);
}
[Fact]
public async Task InMemoryStore_List_ReturnsMatchingArtifacts()
{
var store = new InMemoryArtifactStore();
var bomRef = "pkg:docker/test/app@sha256:abc123";
var tenantId = Guid.NewGuid();
// Store two artifacts with same bom-ref
for (var i = 0; i < 2; i++)
{
using var contentStream = new MemoryStream(new byte[] { (byte)i });
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = $"urn:uuid:serial-{i}",
ArtifactId = $"artifact-{i}",
Content = contentStream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = tenantId
});
}
// Store one with different bom-ref
using var otherStream = new MemoryStream(new byte[] { 99 });
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = "pkg:docker/other/app@sha256:xyz",
SerialNumber = "urn:uuid:other",
ArtifactId = "artifact-other",
Content = otherStream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = tenantId
});
var list = await store.ListAsync(bomRef);
Assert.Equal(2, list.Count);
Assert.All(list, a => Assert.Equal(bomRef, a.BomRef));
}
[Fact]
public async Task InMemoryStore_Exists_ReturnsTrueForExisting()
{
var store = new InMemoryArtifactStore();
var bomRef = "pkg:docker/test/app@sha256:abc123";
var serial = "urn:uuid:12345678-1234-1234-1234-123456789012";
var artifactId = "artifact-001";
using var contentStream = new MemoryStream(new byte[] { 1, 2, 3 });
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = serial,
ArtifactId = artifactId,
Content = contentStream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = Guid.NewGuid()
});
Assert.True(await store.ExistsAsync(bomRef, serial, artifactId));
Assert.False(await store.ExistsAsync(bomRef, serial, "nonexistent"));
}
[Fact]
public async Task InMemoryStore_Delete_RemovesArtifact()
{
var store = new InMemoryArtifactStore();
var bomRef = "pkg:docker/test/app@sha256:abc123";
var serial = "urn:uuid:12345678-1234-1234-1234-123456789012";
var artifactId = "artifact-001";
using var contentStream = new MemoryStream(new byte[] { 1, 2, 3 });
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = serial,
ArtifactId = artifactId,
Content = contentStream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = Guid.NewGuid()
});
Assert.True(await store.ExistsAsync(bomRef, serial, artifactId));
var deleted = await store.DeleteAsync(bomRef, serial, artifactId);
Assert.True(deleted);
Assert.False(await store.ExistsAsync(bomRef, serial, artifactId));
}
[Fact]
public async Task InMemoryStore_StoreExisting_ReturnsWasCreatedFalse()
{
var store = new InMemoryArtifactStore();
var request = new ArtifactStoreRequest
{
BomRef = "pkg:docker/test/app@sha256:abc123",
SerialNumber = "urn:uuid:12345678-1234-1234-1234-123456789012",
ArtifactId = "artifact-001",
Content = new MemoryStream(new byte[] { 1 }),
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = Guid.NewGuid()
};
var first = await store.StoreAsync(request);
Assert.True(first.WasCreated);
// Store again (with new stream)
request = request with { Content = new MemoryStream(new byte[] { 2 }) };
var second = await store.StoreAsync(request);
Assert.False(second.WasCreated);
}
}
[Trait("Category", "Unit")]
public sealed class BomRefEncoderTests
{
[Theory]
[InlineData("pkg:docker/acme/api@sha256:abc", "pkg_docker_acme_api_at_sha256_abc")]
[InlineData("simple-ref", "simple-ref")]
[InlineData("ref/with/slashes", "ref_with_slashes")]
[InlineData("pkg:npm/@scope/pkg", "pkg_npm__at_scope_pkg")]
public void Encode_HandlesSpecialCharacters(string input, string expected)
{
var result = BomRefEncoder.Encode(input);
Assert.Equal(expected, result);
}
[Fact]
public void BuildPath_CreatesCorrectStructure()
{
var bomRef = "pkg:docker/acme/api@sha256:abc";
var serial = "urn:uuid:12345";
var artifactId = "envelope-001";
var path = BomRefEncoder.BuildPath(bomRef, serial, artifactId);
Assert.StartsWith("artifacts/", path);
Assert.EndsWith(".json", path);
Assert.Contains("envelope-001", path);
}
[Fact]
public void Encode_EmptyInput_ReturnsUnknown()
{
Assert.Equal("unknown", BomRefEncoder.Encode(""));
Assert.Equal("unknown", BomRefEncoder.Encode(" "));
}
}
[Trait("Category", "Unit")]
public sealed class CycloneDxExtractorTests
{
private readonly CycloneDxExtractor _extractor = new();
[Fact]
public async Task ExtractAsync_ValidCycloneDx_ExtractsMetadata()
{
var sbom = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
"version": 1,
"metadata": {
"timestamp": "2026-01-18T12:00:00Z",
"component": {
"type": "application",
"bom-ref": "acme-app",
"name": "ACME Application",
"version": "1.0.0",
"purl": "pkg:docker/acme/app@1.0.0"
}
},
"components": [
{
"type": "library",
"bom-ref": "component-1",
"name": "some-lib",
"purl": "pkg:npm/some-lib@1.0.0"
}
]
}
""";
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(sbom));
var result = await _extractor.ExtractAsync(stream);
Assert.True(result.Success);
Assert.Equal("urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", result.SerialNumber);
Assert.Equal("1.5", result.SpecVersion);
Assert.Equal(1, result.Version);
Assert.Equal("acme-app", result.PrimaryBomRef);
Assert.Equal("ACME Application", result.PrimaryName);
Assert.Equal("1.0.0", result.PrimaryVersion);
Assert.Equal("pkg:docker/acme/app@1.0.0", result.PrimaryPurl);
Assert.Single(result.ComponentBomRefs);
Assert.Single(result.ComponentPurls);
}
[Fact]
public async Task ExtractAsync_MissingOptionalFields_Succeeds()
{
var sbom = """
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"version": 1,
"components": []
}
""";
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(sbom));
var result = await _extractor.ExtractAsync(stream);
Assert.True(result.Success);
Assert.Null(result.SerialNumber);
Assert.Equal("1.4", result.SpecVersion);
Assert.Null(result.PrimaryBomRef);
}
[Fact]
public async Task ExtractAsync_InvalidJson_ReturnsError()
{
var invalid = "not valid json";
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(invalid));
var result = await _extractor.ExtractAsync(stream);
Assert.False(result.Success);
Assert.NotNull(result.Error);
}
}
[Trait("Category", "Unit")]
public sealed class ArtifactIndexRepositoryTests
{
[Fact]
public async Task InMemoryIndex_IndexAndFind_Succeeds()
{
var repo = new InMemoryArtifactIndexRepository();
var entry = new ArtifactIndexEntry
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
BomRef = "pkg:docker/test/app@sha256:abc",
SerialNumber = "urn:uuid:12345",
ArtifactId = "artifact-001",
StorageKey = "artifacts/test/artifact-001.json",
Type = ArtifactType.Sbom,
ContentType = "application/json",
Sha256 = "abc123",
SizeBytes = 1024,
CreatedAt = DateTimeOffset.UtcNow
};
await repo.IndexAsync(entry);
var found = await repo.FindByBomRefAsync(entry.BomRef);
Assert.Single(found);
Assert.Equal(entry.ArtifactId, found[0].ArtifactId);
}
[Fact]
public async Task InMemoryIndex_Remove_SoftDeletes()
{
var repo = new InMemoryArtifactIndexRepository();
var entry = new ArtifactIndexEntry
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
BomRef = "pkg:docker/test/app@sha256:abc",
SerialNumber = "urn:uuid:12345",
ArtifactId = "artifact-001",
StorageKey = "artifacts/test/artifact-001.json",
Type = ArtifactType.Sbom,
ContentType = "application/json",
Sha256 = "abc123",
SizeBytes = 1024,
CreatedAt = DateTimeOffset.UtcNow
};
await repo.IndexAsync(entry);
await repo.RemoveAsync(entry.BomRef, entry.SerialNumber, entry.ArtifactId);
var found = await repo.FindByBomRefAsync(entry.BomRef);
Assert.Empty(found);
}
[Fact]
public async Task InMemoryIndex_FindBySha256_ReturnsMatches()
{
var repo = new InMemoryArtifactIndexRepository();
var sha256 = "abc123def456";
await repo.IndexAsync(new ArtifactIndexEntry
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
BomRef = "pkg:docker/test/app1",
SerialNumber = "urn:uuid:1",
ArtifactId = "artifact-1",
StorageKey = "artifacts/1.json",
Type = ArtifactType.Sbom,
ContentType = "application/json",
Sha256 = sha256,
SizeBytes = 1024,
CreatedAt = DateTimeOffset.UtcNow
});
await repo.IndexAsync(new ArtifactIndexEntry
{
Id = Guid.NewGuid(),
TenantId = Guid.NewGuid(),
BomRef = "pkg:docker/test/app2",
SerialNumber = "urn:uuid:2",
ArtifactId = "artifact-2",
StorageKey = "artifacts/2.json",
Type = ArtifactType.Sbom,
ContentType = "application/json",
Sha256 = sha256,
SizeBytes = 1024,
CreatedAt = DateTimeOffset.UtcNow
});
var found = await repo.FindBySha256Async(sha256);
Assert.Equal(2, found.Count);
}
}

View File

@@ -0,0 +1,31 @@
<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.Artifact.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StellaOps.Artifact.Core\StellaOps.Artifact.Core.csproj" />
<ProjectReference Include="..\StellaOps.Artifact.Infrastructure\StellaOps.Artifact.Infrastructure.csproj" />
</ItemGroup>
</Project>