Files
git.stella-ops.org/src/Policy/__Tests/StellaOps.Policy.Engine.Tests/Attestation/PolicyDecisionAttestationServiceTests.cs
StellaOps Bot 43882078a4 save work
2025-12-19 09:40:41 +02:00

327 lines
10 KiB
C#

// -----------------------------------------------------------------------------
// PolicyDecisionAttestationServiceTests.cs
// Sprint: SPRINT_3801_0001_0001_policy_decision_attestation
// Description: Unit tests for PolicyDecisionAttestationService.
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using StellaOps.Policy.Engine.Attestation;
using StellaOps.Policy.Engine.Vex;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.Attestation;
public class PolicyDecisionAttestationServiceTests
{
private readonly Mock<IOptionsMonitor<PolicyDecisionAttestationOptions>> _optionsMock;
private readonly Mock<IVexSignerClient> _signerClientMock;
private readonly Mock<IVexRekorClient> _rekorClientMock;
private readonly PolicyDecisionAttestationService _service;
public PolicyDecisionAttestationServiceTests()
{
_optionsMock = new Mock<IOptionsMonitor<PolicyDecisionAttestationOptions>>();
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = true,
UseSignerService = true,
DefaultTtlHours = 24
});
_signerClientMock = new Mock<IVexSignerClient>();
_rekorClientMock = new Mock<IVexRekorClient>();
_service = new PolicyDecisionAttestationService(
_signerClientMock.Object,
_rekorClientMock.Object,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
}
[Fact]
public async Task CreateAttestationAsync_WhenDisabled_ReturnsFailure()
{
// Arrange
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = false
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.False(result.Success);
Assert.Contains("disabled", result.Error, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task CreateAttestationAsync_WithSignerClient_CallsSigner()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = "AQID",
KeyId = "key-1"
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.NotNull(result.AttestationDigest);
Assert.Matches("^sha256:[a-f0-9]{64}$", result.AttestationDigest);
Assert.Equal("key-1", result.KeyId);
_signerClientMock.Verify(x => x.SignAsync(
It.Is<VexSignerRequest>(r => r.PayloadType == "stella.ops/policy-decision@v1"),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task CreateAttestationAsync_WhenSigningFails_ReturnsFailure()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = false,
Error = "Key not found"
});
var request = CreateTestRequest();
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.False(result.Success);
Assert.Contains("Key not found", result.Error);
}
[Fact]
public async Task CreateAttestationAsync_WithRekorSubmission_SubmitsToRekor()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = "AQID",
KeyId = "key-1"
});
_rekorClientMock.Setup(x => x.SubmitAsync(
It.IsAny<VexRekorSubmitRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexRekorSubmitResult
{
Success = true,
Metadata = new VexRekorMetadata
{
Uuid = "rekor-uuid-123",
Index = 12345,
LogUrl = "https://rekor.local/api/v1/log/entries/rekor-uuid-123",
IntegratedAt = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero)
}
});
var request = CreateTestRequest() with { SubmitToRekor = true };
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.NotNull(result.RekorResult);
Assert.True(result.RekorResult.Success);
Assert.Equal(12345, result.RekorResult.LogIndex);
Assert.Equal("rekor-uuid-123", result.RekorResult.Uuid);
var envelopeDigestHex = result.AttestationDigest!.Substring("sha256:".Length);
_rekorClientMock.Verify(x => x.SubmitAsync(
It.Is<VexRekorSubmitRequest>(r =>
r.ArtifactKind == "policy-decision" &&
r.Envelope.PayloadType == PredicateTypes.StellaOpsPolicyDecision &&
r.EnvelopeDigest == envelopeDigestHex &&
r.SubjectUris!.Contains("example.com/image:v1@sha256:abc123")),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task CreateAttestationAsync_WithoutSignerClient_CreatesUnsignedAttestation()
{
// Arrange
var serviceWithoutSigner = new PolicyDecisionAttestationService(
signerClient: null,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
var request = CreateTestRequest();
// Act
var result = await serviceWithoutSigner.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
Assert.StartsWith("sha256:", result.AttestationDigest);
Assert.Null(result.KeyId);
}
[Fact]
public async Task CreateAttestationAsync_IncludesAllSubjects()
{
// Arrange
_signerClientMock.Setup(x => x.SignAsync(
It.IsAny<VexSignerRequest>(),
It.IsAny<CancellationToken>()))
.ReturnsAsync(new VexSignerResult
{
Success = true,
Signature = "AQID"
});
var request = CreateTestRequest() with
{
Subjects = new[]
{
new AttestationSubject
{
Name = "example.com/image:v1",
Digest = new Dictionary<string, string> { ["sha256"] = "abc123" }
},
new AttestationSubject
{
Name = "example.com/image:v2",
Digest = new Dictionary<string, string> { ["sha256"] = "def456" }
}
}
};
// Act
var result = await _service.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
}
[Fact]
public async Task CreateAttestationAsync_SetsExpirationFromOptions()
{
// Arrange
_optionsMock.Setup(x => x.CurrentValue).Returns(new PolicyDecisionAttestationOptions
{
Enabled = true,
UseSignerService = false,
DefaultTtlHours = 48
});
var serviceWithOptions = new PolicyDecisionAttestationService(
signerClient: null,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
var request = CreateTestRequest();
// Act
var result = await serviceWithOptions.CreateAttestationAsync(request);
// Assert
Assert.True(result.Success);
}
[Fact]
public async Task SubmitToRekorAsync_WhenNoClient_ReturnsFailure()
{
// Arrange
var serviceWithoutRekor = new PolicyDecisionAttestationService(
_signerClientMock.Object,
rekorClient: null,
_optionsMock.Object,
TimeProvider.System,
NullLogger<PolicyDecisionAttestationService>.Instance);
// Act
var result = await serviceWithoutRekor.SubmitToRekorAsync("sha256:test");
// Assert
Assert.False(result.Success);
Assert.Contains("not available", result.Error);
}
[Fact]
public async Task VerifyAsync_ReturnsNotImplemented()
{
// Act
var result = await _service.VerifyAsync("sha256:test");
// Assert
Assert.False(result.Valid);
Assert.Contains("not yet implemented", result.Issues![0], StringComparison.OrdinalIgnoreCase);
}
private static PolicyDecisionAttestationRequest CreateTestRequest()
{
return new PolicyDecisionAttestationRequest
{
Predicate = new PolicyDecisionPredicate
{
Policy = new PolicyReference
{
Id = "test-policy",
Version = "1.0.0",
Name = "Test Policy"
},
Inputs = new PolicyDecisionInputs
{
Subjects = new[]
{
new SubjectReference
{
Name = "example.com/image:v1",
Digest = "sha256:abc123"
}
}
},
Result = new PolicyDecisionResult
{
Decision = PolicyDecision.Allow,
Summary = "All gates passed"
}
},
Subjects = new[]
{
new AttestationSubject
{
Name = "example.com/image:v1",
Digest = new Dictionary<string, string> { ["sha256"] = "abc123" }
}
},
TenantId = "tenant-1"
};
}
}