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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user