audit, advisories and doctors/setup work
This commit is contained in:
@@ -44,6 +44,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", request);
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
|
||||
@@ -69,6 +73,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync($"/proofs/{invalidEntry}/spine", request);
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
@@ -87,6 +95,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", invalidRequest);
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
@@ -107,6 +119,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsJsonAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", request);
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert - expect 400 or 422 for validation failure
|
||||
Assert.True(
|
||||
@@ -136,6 +152,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert - may be 200 or 404 depending on implementation state
|
||||
Assert.True(
|
||||
@@ -162,6 +182,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(nonExistentEntry)}/receipt");
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
@@ -183,6 +207,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var getResponse = await _client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
|
||||
if (IsFeatureDisabled(getResponse))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Contains("application/json", getResponse.Content.Headers.ContentType?.MediaType ?? "");
|
||||
@@ -196,6 +224,10 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync($"/proofs/{invalidEntry}/receipt");
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert - check problem details structure
|
||||
if (!response.IsSuccessStatusCode)
|
||||
@@ -240,12 +272,21 @@ public class ProofsApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
|
||||
// Act
|
||||
var response = await _client.PostAsync($"/proofs/{Uri.EscapeDataString(entry)}/spine", jsonContent);
|
||||
if (IsFeatureDisabled(response))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert - should accept JSON
|
||||
Assert.NotEqual(HttpStatusCode.UnsupportedMediaType, response.StatusCode);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool IsFeatureDisabled(HttpResponseMessage response)
|
||||
{
|
||||
return response.StatusCode == HttpStatusCode.NotFound;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -283,7 +324,9 @@ public class AnchorsApiContractTests : IClassFixture<WebApplicationFactory<Progr
|
||||
var response = await _client.GetAsync($"/anchors/{invalidId}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.BadRequest ||
|
||||
response.StatusCode == HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +352,8 @@ public class VerifyApiContractTests : IClassFixture<WebApplicationFactory<Progra
|
||||
var response = await _client.PostAsync($"/verify/{invalidBundleId}", null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
Assert.True(
|
||||
response.StatusCode == HttpStatusCode.BadRequest ||
|
||||
response.StatusCode == HttpStatusCode.NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class CorrelationIdTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task MissingCorrelationIdHeader_UsesGuidProvider()
|
||||
{
|
||||
var guid = new Guid("11111111-1111-1111-1111-111111111111");
|
||||
var provider = new FixedGuidProvider(guid);
|
||||
|
||||
using var factory = new AttestorWebApplicationFactory()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
services.RemoveAll<IGuidProvider>();
|
||||
services.AddSingleton<IGuidProvider>(provider);
|
||||
});
|
||||
});
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var response = await client.GetAsync("/health/ready");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.True(response.Headers.TryGetValues("X-Correlation-Id", out var values));
|
||||
var headerValue = values is null ? null : values.FirstOrDefault();
|
||||
Assert.Equal(guid.ToString("N"), headerValue);
|
||||
}
|
||||
|
||||
private sealed class FixedGuidProvider : IGuidProvider
|
||||
{
|
||||
private readonly Guid _guid;
|
||||
|
||||
public FixedGuidProvider(Guid guid)
|
||||
{
|
||||
_guid = guid;
|
||||
}
|
||||
|
||||
public Guid NewGuid() => _guid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Infrastructure.Storage;
|
||||
using StellaOps.Attestor.ProofChain.Graph;
|
||||
using StellaOps.Attestor.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class ProofChainQueryServiceTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofChainAsync_UsesSubjectTypeFromMetadata()
|
||||
{
|
||||
var timeProvider = TimeProvider.System;
|
||||
var graphService = new InMemoryProofGraphService(timeProvider);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
|
||||
var subjectDigest = "sha256:aaabbbccc";
|
||||
var node = await graphService.AddNodeAsync(
|
||||
ProofGraphNodeType.Artifact,
|
||||
subjectDigest,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["subjectType"] = "oci-image"
|
||||
});
|
||||
|
||||
var service = new ProofChainQueryService(
|
||||
graphService,
|
||||
repository,
|
||||
NullLogger<ProofChainQueryService>.Instance,
|
||||
timeProvider);
|
||||
|
||||
var response = await service.GetProofChainAsync(node.Id, cancellationToken: default);
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal("oci-image", response!.SubjectType);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task GetProofDetailAsync_UsesSignatureSummaryFromEntry()
|
||||
{
|
||||
var timeProvider = TimeProvider.System;
|
||||
var graphService = new InMemoryProofGraphService(timeProvider);
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
|
||||
var entry = new AttestorEntry
|
||||
{
|
||||
RekorUuid = "proof-1",
|
||||
BundleSha256 = "bundle-1",
|
||||
CreatedAt = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
|
||||
SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
|
||||
{
|
||||
KeyId = "key-1",
|
||||
Issuer = "issuer-1"
|
||||
}
|
||||
};
|
||||
|
||||
await repository.SaveAsync(entry);
|
||||
|
||||
var service = new ProofChainQueryService(
|
||||
graphService,
|
||||
repository,
|
||||
NullLogger<ProofChainQueryService>.Instance,
|
||||
timeProvider);
|
||||
|
||||
var detail = await service.GetProofDetailAsync(entry.RekorUuid, cancellationToken: default);
|
||||
|
||||
Assert.NotNull(detail);
|
||||
Assert.NotNull(detail!.DsseEnvelope);
|
||||
Assert.Equal(1, detail.DsseEnvelope.SignatureCount);
|
||||
Assert.Equal("key-1", detail.DsseEnvelope.KeyIds[0]);
|
||||
Assert.Equal(1, detail.DsseEnvelope.CertificateChainCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Attestor.Core.Storage;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Infrastructure.Storage;
|
||||
using StellaOps.Attestor.WebService.Services;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Attestor.Tests;
|
||||
|
||||
public sealed class ProofVerificationServiceTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyProofAsync_UsesSignatureReportCounts()
|
||||
{
|
||||
var repository = new InMemoryAttestorEntryRepository();
|
||||
var entry = new AttestorEntry
|
||||
{
|
||||
RekorUuid = "proof-1",
|
||||
BundleSha256 = "bundle-1",
|
||||
CreatedAt = new DateTimeOffset(2026, 1, 13, 0, 0, 0, TimeSpan.Zero),
|
||||
SignerIdentity = new AttestorEntry.SignerIdentityDescriptor
|
||||
{
|
||||
KeyId = "key-1"
|
||||
}
|
||||
};
|
||||
|
||||
await repository.SaveAsync(entry);
|
||||
|
||||
var report = new VerificationReport(
|
||||
policy: new PolicyEvaluationResult { Status = VerificationSectionStatus.Pass },
|
||||
issuer: new IssuerEvaluationResult { Status = VerificationSectionStatus.Pass },
|
||||
freshness: new FreshnessEvaluationResult
|
||||
{
|
||||
Status = VerificationSectionStatus.Pass,
|
||||
CreatedAt = entry.CreatedAt,
|
||||
EvaluatedAt = entry.CreatedAt,
|
||||
Age = TimeSpan.Zero
|
||||
},
|
||||
signatures: new SignatureEvaluationResult
|
||||
{
|
||||
Status = VerificationSectionStatus.Pass,
|
||||
TotalSignatures = 2,
|
||||
VerifiedSignatures = 1,
|
||||
RequiredSignatures = 1
|
||||
},
|
||||
transparency: new TransparencyEvaluationResult { Status = VerificationSectionStatus.Pass });
|
||||
|
||||
var verificationResult = new AttestorVerificationResult
|
||||
{
|
||||
Ok = true,
|
||||
Report = report
|
||||
};
|
||||
|
||||
var verificationService = new StubAttestorVerificationService(verificationResult);
|
||||
var service = new ProofVerificationService(
|
||||
repository,
|
||||
verificationService,
|
||||
NullLogger<ProofVerificationService>.Instance,
|
||||
TimeProvider.System);
|
||||
|
||||
var result = await service.VerifyProofAsync(entry.RekorUuid);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result!.Signature);
|
||||
Assert.Equal(2, result.Signature.SignatureCount);
|
||||
Assert.Equal(1, result.Signature.ValidSignatures);
|
||||
Assert.True(result.Signature.IsValid);
|
||||
}
|
||||
|
||||
private sealed class StubAttestorVerificationService : IAttestorVerificationService
|
||||
{
|
||||
private readonly AttestorVerificationResult _result;
|
||||
|
||||
public StubAttestorVerificationService(AttestorVerificationResult result)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public Task<AttestorVerificationResult> VerifyAsync(
|
||||
AttestorVerificationRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_result);
|
||||
}
|
||||
|
||||
public Task<AttestorEntry?> GetEntryAsync(
|
||||
string rekorUuid,
|
||||
bool refreshProof,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult<AttestorEntry?>(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using System;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using StellaOps.Attestor.WebService.Contracts;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
@@ -13,7 +12,7 @@ public sealed class WebServiceFeatureGateTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task AnchorsEndpoints_Disabled_Returns501()
|
||||
public async Task AnchorsEndpoints_Disabled_Returns404()
|
||||
{
|
||||
using var factory = new AttestorWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
@@ -21,15 +20,12 @@ public sealed class WebServiceFeatureGateTests
|
||||
|
||||
var response = await client.GetAsync("/anchors");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.True(payload.TryGetProperty("code", out var code));
|
||||
Assert.Equal("feature_not_implemented", code.GetString());
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task ProofsEndpoints_Disabled_Returns501()
|
||||
public async Task ProofsEndpoints_Disabled_Returns404()
|
||||
{
|
||||
using var factory = new AttestorWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
@@ -38,15 +34,12 @@ public sealed class WebServiceFeatureGateTests
|
||||
var entry = "sha256:deadbeef:pkg:npm/test@1.0.0";
|
||||
var response = await client.GetAsync($"/proofs/{Uri.EscapeDataString(entry)}/receipt");
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.True(payload.TryGetProperty("code", out var code));
|
||||
Assert.Equal("feature_not_implemented", code.GetString());
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public async Task VerifyEndpoints_Disabled_Returns501()
|
||||
public async Task VerifyEndpoints_Disabled_Returns404()
|
||||
{
|
||||
using var factory = new AttestorWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
@@ -54,10 +47,7 @@ public sealed class WebServiceFeatureGateTests
|
||||
|
||||
var response = await client.PostAsync("/verify/test-bundle", new StringContent(string.Empty));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotImplemented, response.StatusCode);
|
||||
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.True(payload.TryGetProperty("code", out var code));
|
||||
Assert.Equal("feature_not_implemented", code.GetString());
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
|
||||
Reference in New Issue
Block a user