Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Attestation;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Attestation;
|
||||
|
||||
public sealed class AttestorClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task SendPlaceholderAsync_PostsJsonPayload()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
var attestorUri = new Uri("https://attestor.example.com/api/v1/provenance");
|
||||
|
||||
await client.SendPlaceholderAsync(attestorUri, document, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(handler.CapturedRequest);
|
||||
Assert.Equal(HttpMethod.Post, handler.CapturedRequest!.Method);
|
||||
Assert.Equal(attestorUri, handler.CapturedRequest.RequestUri);
|
||||
|
||||
var content = await handler.CapturedRequest.Content!.ReadAsStringAsync();
|
||||
var json = JsonDocument.Parse(content);
|
||||
Assert.Equal(document.Subject.Digest, json.RootElement.GetProperty("imageDigest").GetString());
|
||||
Assert.Equal(document.Artifact.Digest, json.RootElement.GetProperty("sbomDigest").GetString());
|
||||
Assert.Equal(document.Provenance.ExpectedDsseSha256, json.RootElement.GetProperty("expectedDsseSha256").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendPlaceholderAsync_ThrowsOnFailure()
|
||||
{
|
||||
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.BadRequest)
|
||||
{
|
||||
Content = new StringContent("invalid")
|
||||
});
|
||||
using var httpClient = new HttpClient(handler);
|
||||
var client = new AttestorClient(httpClient);
|
||||
|
||||
var document = BuildDescriptorDocument();
|
||||
var attestorUri = new Uri("https://attestor.example.com/api/v1/provenance");
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => client.SendPlaceholderAsync(attestorUri, document, CancellationToken.None));
|
||||
}
|
||||
|
||||
private static DescriptorDocument BuildDescriptorDocument()
|
||||
{
|
||||
var subject = new DescriptorSubject("application/vnd.oci.image.manifest.v1+json", "sha256:img");
|
||||
var artifact = new DescriptorArtifact("application/vnd.cyclonedx+json", "sha256:sbom", 42, new System.Collections.Generic.Dictionary<string, string>());
|
||||
var provenance = new DescriptorProvenance("pending", "sha256:dsse", "nonce", "https://attestor.example.com/api/v1/provenance", "https://slsa.dev/provenance/v1");
|
||||
var generatorMetadata = new DescriptorGeneratorMetadata("generator", "1.0.0");
|
||||
var metadata = new System.Collections.Generic.Dictionary<string, string>();
|
||||
return new DescriptorDocument("schema", DateTimeOffset.UtcNow, generatorMetadata, subject, artifact, provenance, metadata);
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpResponseMessage response;
|
||||
|
||||
public RecordingHandler(HttpResponseMessage response)
|
||||
{
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public HttpRequestMessage? CapturedRequest { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
CapturedRequest = request;
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Cas;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Cas;
|
||||
|
||||
public sealed class LocalCasClientTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task VerifyWriteAsync_WritesProbeObject()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var client = new LocalCasClient(new LocalCasOptions
|
||||
{
|
||||
RootDirectory = temp.Path,
|
||||
Algorithm = "sha256"
|
||||
});
|
||||
|
||||
var result = await client.VerifyWriteAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal("sha256", result.Algorithm);
|
||||
Assert.True(File.Exists(result.Path));
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(result.Path);
|
||||
Assert.Equal("stellaops-buildx-probe"u8.ToArray(), bytes);
|
||||
|
||||
var expectedDigest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
Assert.Equal(expectedDigest, result.Digest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorGeneratorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CreateAsync_BuildsDeterministicDescriptor()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DescriptorGenerator.Schema, document.Schema);
|
||||
Assert.Equal(fakeTime.GetUtcNow(), document.GeneratedAt);
|
||||
Assert.Equal(request.ImageDigest, document.Subject.Digest);
|
||||
Assert.Equal(request.SbomMediaType, document.Artifact.MediaType);
|
||||
Assert.Equal(request.SbomName, document.Artifact.Annotations["org.opencontainers.image.title"]);
|
||||
Assert.Equal("pending", document.Provenance.Status);
|
||||
Assert.Equal(request.AttestorUri, document.Provenance.AttestorUri);
|
||||
Assert.Equal(request.PredicateType, document.Provenance.PredicateType);
|
||||
|
||||
var expectedSbomDigest = ComputeSha256File(sbomPath);
|
||||
Assert.Equal(expectedSbomDigest, document.Artifact.Digest);
|
||||
Assert.Equal(expectedSbomDigest, document.Metadata["sbomDigest"]);
|
||||
|
||||
var expectedDsse = ComputeExpectedDsse(request.ImageDigest, expectedSbomDigest, document.Provenance.Nonce);
|
||||
Assert.Equal(expectedDsse, document.Provenance.ExpectedDsseSha256);
|
||||
Assert.Equal(expectedDsse, document.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"]);
|
||||
Assert.Equal(document.Provenance.Nonce, document.Artifact.Annotations["org.stellaops.provenance.nonce"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_RepeatedInvocationsReuseDeterministicNonce()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var first = await generator.CreateAsync(request, CancellationToken.None);
|
||||
var second = await generator.CreateAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(first.Provenance.Nonce, second.Provenance.Nonce);
|
||||
Assert.Equal(first.Provenance.ExpectedDsseSha256, second.Provenance.ExpectedDsseSha256);
|
||||
Assert.Equal(first.Artifact.Annotations["org.stellaops.provenance.nonce"], second.Artifact.Annotations["org.stellaops.provenance.nonce"]);
|
||||
Assert.Equal(first.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"], second.Artifact.Annotations["org.stellaops.provenance.dsse.sha256"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateAsync_MetadataDifferencesYieldDistinctNonce()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
|
||||
var baseline = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main"
|
||||
}.Validate();
|
||||
|
||||
var variant = baseline with
|
||||
{
|
||||
BuildRef = "refs/heads/feature",
|
||||
Repository = "git.stella-ops.org/stellaops/feature"
|
||||
};
|
||||
variant = variant.Validate();
|
||||
|
||||
var baselineDocument = await generator.CreateAsync(baseline, CancellationToken.None);
|
||||
var variantDocument = await generator.CreateAsync(variant, CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(baselineDocument.Provenance.Nonce, variantDocument.Provenance.Nonce);
|
||||
Assert.NotEqual(baselineDocument.Provenance.ExpectedDsseSha256, variantDocument.Provenance.ExpectedDsseSha256);
|
||||
}
|
||||
|
||||
private static string ComputeSha256File(string path)
|
||||
{
|
||||
using var stream = File.OpenRead(path);
|
||||
var hash = SHA256.HashData(stream);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
private static string ComputeExpectedDsse(string imageDigest, string sbomDigest, string nonce)
|
||||
{
|
||||
var payload = $"{imageDigest}\n{sbomDigest}\n{nonce}";
|
||||
Span<byte> hash = stackalloc byte[32];
|
||||
SHA256.HashData(Encoding.UTF8.GetBytes(payload), hash);
|
||||
return $"sha256:{Convert.ToHexString(hash).ToLower(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Descriptor;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Descriptor;
|
||||
|
||||
public sealed class DescriptorGoldenTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task DescriptorMatchesBaselineFixture()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var sbomPath = Path.Combine(temp.Path, "sample.cdx.json");
|
||||
await File.WriteAllTextAsync(sbomPath, "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.5\"}");
|
||||
|
||||
var request = new DescriptorRequest
|
||||
{
|
||||
ImageDigest = "sha256:0123456789abcdef",
|
||||
SbomPath = sbomPath,
|
||||
SbomMediaType = "application/vnd.cyclonedx+json",
|
||||
SbomFormat = "cyclonedx-json",
|
||||
SbomKind = "inventory",
|
||||
SbomArtifactType = "application/vnd.stellaops.sbom.layer+json",
|
||||
SubjectMediaType = "application/vnd.oci.image.manifest.v1+json",
|
||||
GeneratorVersion = "1.2.3",
|
||||
GeneratorName = "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
LicenseId = "lic-123",
|
||||
SbomName = "sample.cdx.json",
|
||||
Repository = "git.stella-ops.org/stellaops",
|
||||
BuildRef = "refs/heads/main",
|
||||
AttestorUri = "https://attestor.local/api/v1/provenance"
|
||||
}.Validate();
|
||||
|
||||
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2025, 10, 18, 12, 0, 0, TimeSpan.Zero));
|
||||
var generator = new DescriptorGenerator(fakeTime);
|
||||
var document = await generator.CreateAsync(request, CancellationToken.None);
|
||||
var actualJson = JsonSerializer.Serialize(document, SerializerOptions);
|
||||
var normalizedJson = NormalizeDescriptorJson(actualJson, Path.GetFileName(sbomPath));
|
||||
|
||||
var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
var fixturePath = Path.Combine(projectRoot, "Fixtures", "descriptor.baseline.json");
|
||||
var updateRequested = string.Equals(Environment.GetEnvironmentVariable("UPDATE_BUILDX_FIXTURES"), "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (updateRequested)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fixturePath)!);
|
||||
await File.WriteAllTextAsync(fixturePath, normalizedJson);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(fixturePath))
|
||||
{
|
||||
throw new InvalidOperationException($"Baseline fixture '{fixturePath}' is missing. Set UPDATE_BUILDX_FIXTURES=1 and re-run the tests to generate it.");
|
||||
}
|
||||
|
||||
var baselineJson = await File.ReadAllTextAsync(fixturePath);
|
||||
|
||||
using var baselineDoc = JsonDocument.Parse(baselineJson);
|
||||
using var actualDoc = JsonDocument.Parse(normalizedJson);
|
||||
|
||||
AssertJsonEquivalent(baselineDoc.RootElement, actualDoc.RootElement);
|
||||
}
|
||||
|
||||
private static string NormalizeDescriptorJson(string json, string sbomFileName)
|
||||
{
|
||||
var node = JsonNode.Parse(json)?.AsObject()
|
||||
?? throw new InvalidOperationException("Failed to parse descriptor JSON for normalization.");
|
||||
|
||||
if (node["metadata"] is JsonObject metadata)
|
||||
{
|
||||
metadata["sbomPath"] = sbomFileName;
|
||||
}
|
||||
|
||||
return node.ToJsonString(SerializerOptions);
|
||||
}
|
||||
|
||||
private static void AssertJsonEquivalent(JsonElement expected, JsonElement actual)
|
||||
{
|
||||
if (expected.ValueKind != actual.ValueKind)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException($"Value kind mismatch. Expected '{expected.ValueKind}' but found '{actual.ValueKind}'.");
|
||||
}
|
||||
|
||||
switch (expected.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
var expectedProperties = expected.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal);
|
||||
var actualProperties = actual.EnumerateObject().ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal);
|
||||
|
||||
Assert.Equal(
|
||||
expectedProperties.Keys.OrderBy(static name => name).ToArray(),
|
||||
actualProperties.Keys.OrderBy(static name => name).ToArray());
|
||||
|
||||
foreach (var propertyName in expectedProperties.Keys)
|
||||
{
|
||||
AssertJsonEquivalent(expectedProperties[propertyName], actualProperties[propertyName]);
|
||||
}
|
||||
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
var expectedItems = expected.EnumerateArray().ToArray();
|
||||
var actualItems = actual.EnumerateArray().ToArray();
|
||||
|
||||
Assert.Equal(expectedItems.Length, actualItems.Length);
|
||||
for (var i = 0; i < expectedItems.Length; i++)
|
||||
{
|
||||
AssertJsonEquivalent(expectedItems[i], actualItems[i]);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
Assert.Equal(expected.ToString(), actual.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema": "stellaops.buildx.descriptor.v1",
|
||||
"generatedAt": "2025-10-18T12:00:00\u002B00:00",
|
||||
"generator": {
|
||||
"name": "StellaOps.Scanner.Sbomer.BuildXPlugin",
|
||||
"version": "1.2.3"
|
||||
},
|
||||
"subject": {
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"digest": "sha256:0123456789abcdef"
|
||||
},
|
||||
"artifact": {
|
||||
"mediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"digest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"size": 45,
|
||||
"annotations": {
|
||||
"org.opencontainers.artifact.type": "application/vnd.stellaops.sbom.layer\u002Bjson",
|
||||
"org.stellaops.scanner.version": "1.2.3",
|
||||
"org.stellaops.sbom.kind": "inventory",
|
||||
"org.stellaops.sbom.format": "cyclonedx-json",
|
||||
"org.stellaops.provenance.status": "pending",
|
||||
"org.stellaops.provenance.dsse.sha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
|
||||
"org.stellaops.provenance.nonce": "a608acf859cd58a8389816b8d9eb2a07",
|
||||
"org.stellaops.license.id": "lic-123",
|
||||
"org.opencontainers.image.title": "sample.cdx.json",
|
||||
"org.stellaops.repository": "git.stella-ops.org/stellaops"
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"status": "pending",
|
||||
"expectedDsseSha256": "sha256:1b364a6b888d580feb8565f7b6195b24535ca8201b4bcac58da063b32c47220d",
|
||||
"nonce": "a608acf859cd58a8389816b8d9eb2a07",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance",
|
||||
"predicateType": "https://slsa.dev/provenance/v1"
|
||||
},
|
||||
"metadata": {
|
||||
"sbomDigest": "sha256:d07d06ae82e1789a5b505731f3ec3add106e23a55395213c9a881c7e816c695c",
|
||||
"sbomPath": "sample.cdx.json",
|
||||
"sbomMediaType": "application/vnd.cyclonedx\u002Bjson",
|
||||
"subjectMediaType": "application/vnd.oci.image.manifest.v1\u002Bjson",
|
||||
"repository": "git.stella-ops.org/stellaops",
|
||||
"buildRef": "refs/heads/main",
|
||||
"attestorUri": "https://attestor.local/api/v1/provenance"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Manifest;
|
||||
using StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.Manifest;
|
||||
|
||||
public sealed class BuildxPluginManifestLoaderTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ReturnsManifestWithSourceInformation()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var manifestPath = System.IO.Path.Combine(temp.Path, "stellaops.manifest.json");
|
||||
await File.WriteAllTextAsync(manifestPath, BuildSampleManifestJson("stellaops.sbom-indexer"));
|
||||
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
var manifests = await loader.LoadAsync(CancellationToken.None);
|
||||
|
||||
var manifest = Assert.Single(manifests);
|
||||
Assert.Equal("stellaops.sbom-indexer", manifest.Id);
|
||||
Assert.Equal("0.1.0", manifest.Version);
|
||||
Assert.Equal(manifestPath, manifest.SourcePath);
|
||||
Assert.Equal(Path.GetDirectoryName(manifestPath), manifest.SourceDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadDefaultAsync_ThrowsWhenNoManifests()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => loader.LoadDefaultAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadAsync_ThrowsWhenRestartRequiredMissing()
|
||||
{
|
||||
await using var temp = new TempDirectory();
|
||||
var manifestPath = Path.Combine(temp.Path, "failure.manifest.json");
|
||||
await File.WriteAllTextAsync(manifestPath, BuildSampleManifestJson("stellaops.failure", requiresRestart: false));
|
||||
|
||||
var loader = new BuildxPluginManifestLoader(temp.Path);
|
||||
|
||||
await Assert.ThrowsAsync<BuildxPluginException>(() => loader.LoadAsync(CancellationToken.None));
|
||||
}
|
||||
|
||||
private static string BuildSampleManifestJson(string id, bool requiresRestart = true)
|
||||
{
|
||||
var manifest = new BuildxPluginManifest
|
||||
{
|
||||
SchemaVersion = BuildxPluginManifest.CurrentSchemaVersion,
|
||||
Id = id,
|
||||
DisplayName = "Sample",
|
||||
Version = "0.1.0",
|
||||
RequiresRestart = requiresRestart,
|
||||
EntryPoint = new BuildxPluginEntryPoint
|
||||
{
|
||||
Type = "dotnet",
|
||||
Executable = "StellaOps.Scanner.Sbomer.BuildXPlugin.dll"
|
||||
},
|
||||
Cas = new BuildxPluginCas
|
||||
{
|
||||
Protocol = "filesystem",
|
||||
DefaultRoot = "cas"
|
||||
}
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(manifest, SerializerOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Fixtures\descriptor.baseline.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StellaOps.Scanner.Sbomer.BuildXPlugin.Tests.TestUtilities;
|
||||
|
||||
internal sealed class TempDirectory : IDisposable, IAsyncDisposable
|
||||
{
|
||||
public string Path { get; }
|
||||
|
||||
public TempDirectory()
|
||||
{
|
||||
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"stellaops-buildx-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(Path);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cleanup();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
Cleanup();
|
||||
GC.SuppressFinalize(this);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(Path))
|
||||
{
|
||||
Directory.Delete(Path, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user