stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -0,0 +1,25 @@
# Artifact Core Tests Charter
## Mission
Own test coverage for artifact core and infrastructure behaviors. Keep tests deterministic and offline-friendly.
## Responsibilities
- Maintain `StellaOps.Artifact.Core.Tests` coverage.
- Validate store, index, and bom-ref encoding behavior with stable fixtures.
- Record remediation status updates in the active sprint tracker.
## Key Paths
- `ArtifactStore*Tests*.cs`
- `CycloneDxExtractor*Tests*.cs`
- `S3UnifiedArtifactStore*Tests*.cs`
## Required Reading
- `docs/operations/artifact-migration-runbook.md`
- `docs/modules/platform/architecture-overview.md`
- `docs/technical/testing/TEST_SUITE_OVERVIEW.md`
- `docs/code-of-conduct/TESTING_PRACTICES.md`
## Working Agreement
- Use fixed IDs/timestamps and stable ordering in fixtures.
- Keep tests offline; no runtime network calls.
- Update sprint status when work starts or finishes.

View File

@@ -0,0 +1,68 @@
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Unit")]
public sealed class ArtifactIndexRepositoryTests
{
[Fact]
public async Task InMemoryIndex_IndexAndFind_SucceedsAsync()
{
var repo = new InMemoryArtifactIndexRepository(ArtifactTestFixtures.TimeProvider);
var entry = CreateEntry(1, "artifact-001", "abc123");
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_SoftDeletesAsync()
{
var repo = new InMemoryArtifactIndexRepository(ArtifactTestFixtures.TimeProvider);
var entry = CreateEntry(2, "artifact-001", "abc123");
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_ReturnsMatchesAsync()
{
var repo = new InMemoryArtifactIndexRepository(ArtifactTestFixtures.TimeProvider);
const string sha256 = "abc123def456";
await repo.IndexAsync(CreateEntry(3, "artifact-1", sha256));
await repo.IndexAsync(CreateEntry(4, "artifact-2", sha256));
var found = await repo.FindBySha256Async(sha256);
Assert.Equal(2, found.Count);
}
private static ArtifactIndexEntry CreateEntry(int id, string artifactId, string sha256)
{
return new ArtifactIndexEntry
{
Id = CreateGuid(id),
TenantId = ArtifactTestFixtures.TenantId,
BomRef = ArtifactTestFixtures.DefaultBomRef,
SerialNumber = ArtifactTestFixtures.DefaultSerialNumber,
ArtifactId = artifactId,
StorageKey = $"artifacts/{artifactId}.json",
Type = ArtifactType.Sbom,
ContentType = "application/json",
Sha256 = sha256,
SizeBytes = 1024,
CreatedAt = ArtifactTestFixtures.FixedNow
};
}
private static Guid CreateGuid(int value) => Guid.Parse($"00000000-0000-0000-0000-{value:D12}");
}

View File

@@ -0,0 +1,53 @@
using System.Diagnostics;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStorePerformanceTests
{
[Fact]
public async Task ListByBomRef_1000Artifacts_Under100msAsync()
{
const int artifactCount = 1000;
const int maxDurationMs = 100;
var store = CreateStore();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, ArtifactTestFixtures.TenantId, bomRef);
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
}
var sw = Stopwatch.StartNew();
var list = await store.ListAsync(bomRef);
sw.Stop();
Assert.Equal(artifactCount, list.Count);
Assert.True(sw.ElapsedMilliseconds < maxDurationMs);
}
[Fact]
public async Task ParallelStore_1000Artifacts_UnderThresholdAsync()
{
const int artifactCount = 1000;
const int maxDurationMs = 10000;
var store = CreateStore();
var bomRef = "pkg:docker/perf-parallel/app@sha256:abc123";
var artifacts = GenerateTestArtifacts(artifactCount, ArtifactTestFixtures.TenantId, bomRef);
var sw = Stopwatch.StartNew();
await Parallel.ForEachAsync(artifacts, new ParallelOptions { MaxDegreeOfParallelism = 10 },
async (artifact, ct) =>
{
await store.StoreAsync(artifact, ct);
});
sw.Stop();
var stored = await store.ListAsync(bomRef);
Assert.Equal(artifactCount, stored.Count);
Assert.True(sw.ElapsedMilliseconds < maxDurationMs);
}
}

View File

@@ -0,0 +1,62 @@
using System.Diagnostics;
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStorePerformanceTests
{
[Fact]
public async Task MixedOperations_CompletesSuccessfullyAsync()
{
const int operationCount = 1000;
var store = CreateStore();
var bomRef = "pkg:docker/mixed-test/app@sha256:abc123";
var preloadArtifacts = GenerateTestArtifacts(500, ArtifactTestFixtures.TenantId, bomRef);
foreach (var artifact in preloadArtifacts)
{
await store.StoreAsync(artifact);
}
var random = new Random(42);
var sw = Stopwatch.StartNew();
for (var i = 0; i < operationCount; i++)
{
var op = random.Next(4);
switch (op)
{
case 0:
using (var stream = new MemoryStream(new byte[] { (byte)(i % 256) }))
{
await store.StoreAsync(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = $"urn:uuid:{CreateGuid(10000 + i)}",
ArtifactId = $"mixed-artifact-{i}",
Content = stream,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = ArtifactTestFixtures.TenantId
});
}
break;
case 1:
var idx = random.Next(preloadArtifacts.Count);
await store.ReadAsync(bomRef, preloadArtifacts[idx].SerialNumber, preloadArtifacts[idx].ArtifactId);
break;
case 2:
await store.ListAsync(bomRef);
break;
case 3:
var checkIdx = random.Next(preloadArtifacts.Count);
await store.ExistsAsync(bomRef, preloadArtifacts[checkIdx].SerialNumber, preloadArtifacts[checkIdx].ArtifactId);
break;
}
}
sw.Stop();
}
}

View File

@@ -0,0 +1,57 @@
using System.Diagnostics;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStorePerformanceTests
{
[Fact]
public async Task Store1000Artifacts_CompletesUnderThresholdAsync()
{
const int artifactCount = 1000;
const int maxDurationMs = 30000;
var store = CreateStore();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, ArtifactTestFixtures.TenantId, bomRef);
var sw = Stopwatch.StartNew();
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
}
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < maxDurationMs,
$"Store operation took {sw.ElapsedMilliseconds}ms, expected under {maxDurationMs}ms");
}
[Fact]
public async Task Retrieve1000Artifacts_CompletesUnderThresholdAsync()
{
const int artifactCount = 1000;
const int maxDurationMs = 10000;
var store = CreateStore();
var bomRef = "pkg:docker/perf-test/app@sha256:abc123def456";
var artifacts = GenerateTestArtifacts(artifactCount, ArtifactTestFixtures.TenantId, bomRef);
var stored = new List<(string BomRef, string Serial, string Id)>();
foreach (var artifact in artifacts)
{
await store.StoreAsync(artifact);
stored.Add((artifact.BomRef, artifact.SerialNumber, artifact.ArtifactId));
}
var sw = Stopwatch.StartNew();
foreach (var (bRef, serial, id) in stored)
{
var result = await store.ReadAsync(bRef, serial, id);
Assert.True(result.Found);
}
sw.Stop();
Assert.True(sw.ElapsedMilliseconds < maxDurationMs,
$"Retrieve operation took {sw.ElapsedMilliseconds}ms, expected under {maxDurationMs}ms");
}
}

View File

@@ -1,11 +1,4 @@
// -----------------------------------------------------------------------------
// 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 System.Text;
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using Xunit;
@@ -13,230 +6,30 @@ using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Performance")]
public sealed class ArtifactStorePerformanceTests
public sealed partial 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 InMemoryArtifactStore CreateStore() => new(ArtifactTestFixtures.TimeProvider);
private static List<ArtifactStoreRequest> GenerateTestArtifacts(int count, Guid tenantId, string bomRef)
{
var artifacts = new List<ArtifactStoreRequest>();
var artifacts = new List<ArtifactStoreRequest>(count);
for (var i = 0; i < count; i++)
{
var content = System.Text.Encoding.UTF8.GetBytes($"{{\"index\": {i}, \"data\": \"test-{Guid.NewGuid()}\"}}");
var content = Encoding.UTF8.GetBytes($"{{\"index\":{i},\"data\":\"test-{i:D4}\"}}");
artifacts.Add(new ArtifactStoreRequest
{
BomRef = bomRef,
SerialNumber = $"urn:uuid:{Guid.NewGuid()}",
SerialNumber = $"urn:uuid:{CreateGuid(i + 1)}",
ArtifactId = $"artifact-{i:D5}",
Content = new MemoryStream(content),
ContentType = "application/json",
Type = (ArtifactType)(i % 5), // Rotate through types
Type = (ArtifactType)(i % 5),
TenantId = tenantId
});
}
return artifacts;
}
private static Guid CreateGuid(int value) => Guid.Parse($"00000000-0000-0000-0000-{value:D12}");
}

View File

@@ -0,0 +1,43 @@
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStoreTests
{
[Fact]
public async Task InMemoryStore_Exists_ReturnsTrueForExistingAsync()
{
var store = ArtifactTestFixtures.CreateStore();
var bomRef = ArtifactTestFixtures.DefaultBomRef;
var serial = ArtifactTestFixtures.DefaultSerialNumber;
var artifactId = "artifact-001";
using var contentStream = new MemoryStream(new byte[] { 1, 2, 3 });
var request = ArtifactTestFixtures.CreateRequest(artifactId, contentStream, bomRef, serial);
await store.StoreAsync(request);
Assert.True(await store.ExistsAsync(bomRef, serial, artifactId));
Assert.False(await store.ExistsAsync(bomRef, serial, "nonexistent"));
}
[Fact]
public async Task InMemoryStore_Delete_RemovesArtifactAsync()
{
var store = ArtifactTestFixtures.CreateStore();
var bomRef = ArtifactTestFixtures.DefaultBomRef;
var serial = ArtifactTestFixtures.DefaultSerialNumber;
var artifactId = "artifact-001";
using var contentStream = new MemoryStream(new byte[] { 1, 2, 3 });
var request = ArtifactTestFixtures.CreateRequest(artifactId, contentStream, bomRef, serial);
await store.StoreAsync(request);
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));
}
}

View File

@@ -0,0 +1,30 @@
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStoreTests
{
[Fact]
public async Task InMemoryStore_List_ReturnsMatchingArtifactsAsync()
{
var store = ArtifactTestFixtures.CreateStore();
var bomRef = ArtifactTestFixtures.DefaultBomRef;
for (var i = 0; i < 2; i++)
{
using var contentStream = new MemoryStream(new byte[] { (byte)i });
var request = ArtifactTestFixtures.CreateRequest($"artifact-{i}", contentStream, bomRef);
await store.StoreAsync(request);
}
using var otherStream = new MemoryStream(new byte[] { 99 });
var otherRequest = ArtifactTestFixtures.CreateRequest("artifact-other", otherStream, "pkg:docker/other/app@sha256:xyz");
await store.StoreAsync(otherRequest);
var list = await store.ListAsync(bomRef);
Assert.Equal(2, list.Count);
Assert.All(list, a => Assert.Equal(bomRef, a.BomRef));
}
}

View File

@@ -0,0 +1,24 @@
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class ArtifactStoreTests
{
[Fact]
public async Task InMemoryStore_StoreExisting_ReturnsWasCreatedFalseAsync()
{
var store = ArtifactTestFixtures.CreateStore();
using var firstStream = new MemoryStream(new byte[] { 1 });
var request = ArtifactTestFixtures.CreateRequest("artifact-001", firstStream);
var first = await store.StoreAsync(request);
Assert.True(first.WasCreated);
using var secondStream = new MemoryStream(new byte[] { 2 });
request = request with { Content = secondStream };
var second = await store.StoreAsync(request);
Assert.False(second.WasCreated);
}
}

View File

@@ -0,0 +1,31 @@
using System.Text;
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Unit")]
public sealed partial class ArtifactStoreTests
{
[Fact]
public async Task InMemoryStore_StoreAndRead_SucceedsAsync()
{
var store = ArtifactTestFixtures.CreateStore();
using var contentStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"test\": true}"));
var request = ArtifactTestFixtures.CreateRequest("artifact-001", contentStream);
var storeResult = await store.StoreAsync(request);
Assert.True(storeResult.Success);
Assert.True(storeResult.WasCreated);
Assert.NotNull(storeResult.Sha256);
Assert.Equal(contentStream.Length, storeResult.SizeBytes);
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);
}
}

View File

@@ -1,429 +0,0 @@
// -----------------------------------------------------------------------------
// 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 ExtractParsedAsync_ValidCycloneDx_ExtractsParsedSbom()
{
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.ExtractParsedAsync(stream);
Assert.Equal("cyclonedx", result.Format);
Assert.Equal("1.5", result.SpecVersion);
Assert.Equal("urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", result.SerialNumber);
Assert.Equal("ACME Application", result.Metadata.Name);
Assert.Equal("acme-app", result.Metadata.RootComponentRef);
Assert.Contains(result.Components, component => component.BomRef == "acme-app");
Assert.Contains(result.Components, component => component.BomRef == "component-1");
}
[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,29 @@
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
namespace StellaOps.Artifact.Tests;
internal static class ArtifactTestFixtures
{
internal static readonly DateTimeOffset FixedNow = new(2026, 2, 3, 0, 0, 0, TimeSpan.Zero);
internal static readonly TimeProvider TimeProvider = new FixedTimeProvider(FixedNow);
internal static readonly Guid TenantId = Guid.Parse("11111111-1111-1111-1111-111111111111");
internal const string DefaultBomRef = "pkg:docker/test/app@sha256:abc123";
internal const string DefaultSerialNumber = "urn:uuid:12345678-1234-1234-1234-123456789012";
internal static InMemoryArtifactStore CreateStore() => new(TimeProvider);
internal static ArtifactStoreRequest CreateRequest(string artifactId, Stream content, string? bomRef = null, string? serialNumber = null)
{
return new ArtifactStoreRequest
{
BomRef = bomRef ?? DefaultBomRef,
SerialNumber = serialNumber ?? DefaultSerialNumber,
ArtifactId = artifactId,
Content = content,
ContentType = "application/json",
Type = ArtifactType.Sbom,
TenantId = TenantId
};
}
}

View File

@@ -0,0 +1,40 @@
using StellaOps.Artifact.Core;
using Xunit;
namespace StellaOps.Artifact.Tests;
[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(" "));
}
}

View File

@@ -0,0 +1,48 @@
using System.Text;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class CycloneDxExtractorTests
{
[Fact]
public async Task ExtractAsync_ValidCycloneDx_ExtractsMetadataAsync()
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ValidCycloneDx));
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_SucceedsAsync()
{
var sbom = "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.4\",\"version\":1,\"components\":[]}";
using var stream = new MemoryStream(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_ReturnsErrorAsync()
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes("not valid json"));
var result = await _extractor.ExtractAsync(stream);
Assert.False(result.Success);
Assert.NotNull(result.Error);
}
}

View File

@@ -0,0 +1,22 @@
using System.Text;
using Xunit;
namespace StellaOps.Artifact.Tests;
public sealed partial class CycloneDxExtractorTests
{
[Fact]
public async Task ExtractParsedAsync_ValidCycloneDx_ExtractsParsedSbomAsync()
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(ValidCycloneDx));
var result = await _extractor.ExtractParsedAsync(stream);
Assert.Equal("cyclonedx", result.Format);
Assert.Equal("1.5", result.SpecVersion);
Assert.Equal("urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", result.SerialNumber);
Assert.Equal("ACME Application", result.Metadata.Name);
Assert.Equal("acme-app", result.Metadata.RootComponentRef);
Assert.Contains(result.Components, component => component.BomRef == "acme-app");
Assert.Contains(result.Components, component => component.BomRef == "component-1");
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Artifact.Core;
using StellaOps.Concelier.SbomIntegration.Parsing;
using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Unit")]
public sealed partial class CycloneDxExtractorTests
{
private const string ValidCycloneDx = "{\"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\"}]}";
private readonly CycloneDxExtractor _extractor = new(
new ParsedSbomParser(NullLogger<ParsedSbomParser>.Instance));
}

View File

@@ -0,0 +1,98 @@
using StellaOps.Artifact.Infrastructure;
namespace StellaOps.Artifact.Tests;
internal sealed class FakeS3UnifiedClient : IS3UnifiedClient
{
private readonly Dictionary<string, Dictionary<string, StoredObject>> _objects = new();
private readonly object _lock = new();
public int StoredObjectCount
{
get
{
lock (_lock)
{
return _objects.Values.Sum(bucket => bucket.Count);
}
}
}
public Task<bool> ObjectExistsAsync(string bucketName, string key, CancellationToken ct)
{
lock (_lock)
{
return Task.FromResult(_objects.TryGetValue(bucketName, out var bucket) && bucket.ContainsKey(key));
}
}
public Task PutObjectAsync(string bucketName, string key, Stream content, string contentType, IDictionary<string, string> metadata, CancellationToken ct)
{
using var ms = new MemoryStream();
content.CopyTo(ms);
var data = ms.ToArray();
lock (_lock)
{
var bucket = GetBucket(bucketName);
bucket[key] = new StoredObject(data, new Dictionary<string, string>(metadata));
}
return Task.CompletedTask;
}
public Task<Stream?> GetObjectAsync(string bucketName, string key, CancellationToken ct)
{
lock (_lock)
{
if (!_objects.TryGetValue(bucketName, out var bucket) || !bucket.TryGetValue(key, out var stored))
{
return Task.FromResult<Stream?>(null);
}
return Task.FromResult<Stream?>(new MemoryStream(stored.Content, writable: false));
}
}
public Task<IDictionary<string, string>?> GetObjectMetadataAsync(string bucketName, string key, CancellationToken ct)
{
lock (_lock)
{
if (!_objects.TryGetValue(bucketName, out var bucket) || !bucket.TryGetValue(key, out var stored))
{
return Task.FromResult<IDictionary<string, string>?>(null);
}
return Task.FromResult<IDictionary<string, string>?>(new Dictionary<string, string>(stored.Metadata));
}
}
public Task DeleteObjectAsync(string bucketName, string key, CancellationToken ct)
{
lock (_lock)
{
if (_objects.TryGetValue(bucketName, out var bucket))
{
bucket.Remove(key);
}
}
return Task.CompletedTask;
}
public Task<IReadOnlyList<string>> ListObjectsAsync(string bucketName, string prefix, CancellationToken ct)
{
lock (_lock)
{
if (!_objects.TryGetValue(bucketName, out var bucket))
{
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
}
var result = bucket.Keys
.Where(key => key.StartsWith(prefix, StringComparison.Ordinal))
.OrderBy(key => key, StringComparer.Ordinal)
.ToList();
return Task.FromResult<IReadOnlyList<string>>(result);
}
}
private Dictionary<string, StoredObject> GetBucket(string bucketName)
{
if (!_objects.TryGetValue(bucketName, out var bucket))
{
bucket = new Dictionary<string, StoredObject>(StringComparer.Ordinal);
_objects[bucketName] = bucket;
}
return bucket;
}
private sealed record StoredObject(byte[] Content, IDictionary<string, string> Metadata);
}

View File

@@ -0,0 +1,13 @@
namespace StellaOps.Artifact.Tests;
internal sealed class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FixedTimeProvider(DateTimeOffset utcNow)
{
_utcNow = utcNow;
}
public override DateTimeOffset GetUtcNow() => _utcNow;
}

View File

@@ -0,0 +1,79 @@
using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Artifact.Core;
using StellaOps.Artifact.Infrastructure;
using StellaOps.Determinism;
using Xunit;
namespace StellaOps.Artifact.Tests;
[Trait("Category", "Integration")]
[Trait("Intent", "Operational")]
public sealed class S3UnifiedArtifactStoreTests
{
[Fact]
public async Task StoreAndRead_RoundTripsContentAsync()
{
var client = new FakeS3UnifiedClient();
var indexRepository = new InMemoryArtifactIndexRepository(ArtifactTestFixtures.TimeProvider);
var store = CreateStore(client, indexRepository);
using var contentStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"value\":1}"));
var request = ArtifactTestFixtures.CreateRequest("artifact-001", contentStream);
var stored = await store.StoreAsync(request);
Assert.True(stored.Success);
var read = await store.ReadAsync(request.BomRef, request.SerialNumber, request.ArtifactId);
Assert.True(read.Found);
using var reader = new StreamReader(read.Content!);
var payload = await reader.ReadToEndAsync();
Assert.Equal("{\"value\":1}", payload);
}
[Fact]
public async Task Store_DeduplicatesBySha256Async()
{
var client = new FakeS3UnifiedClient();
var indexRepository = new InMemoryArtifactIndexRepository(ArtifactTestFixtures.TimeProvider);
var store = CreateStore(client, indexRepository);
var payload = Encoding.UTF8.GetBytes("{\"value\":2}");
using var firstStream = new MemoryStream(payload);
using var secondStream = new MemoryStream(payload);
var firstRequest = ArtifactTestFixtures.CreateRequest("artifact-001", firstStream);
var secondRequest = ArtifactTestFixtures.CreateRequest("artifact-002", secondStream);
var first = await store.StoreAsync(firstRequest);
var second = await store.StoreAsync(secondRequest);
Assert.True(first.Success);
Assert.True(second.Success);
Assert.Equal(1, client.StoredObjectCount);
Assert.Equal(first.StorageKey, second.StorageKey);
}
private static S3UnifiedArtifactStore CreateStore(FakeS3UnifiedClient client, InMemoryArtifactIndexRepository indexRepository)
{
var options = new S3UnifiedArtifactStoreOptions
{
BucketName = "test-bucket",
Prefix = "artifacts",
EnableDeduplication = true,
AllowOverwrite = false,
MaxArtifactSizeBytes = 1024 * 1024
};
return new S3UnifiedArtifactStore(
client,
indexRepository,
Options.Create(options),
ArtifactTestFixtures.TimeProvider,
new SequentialGuidProvider(Guid.Parse("00000000-0000-0000-0000-000000000001")),
NullLogger<S3UnifiedArtifactStore>.Instance);
}
}