feat(telemetry): add telemetry client and services for tracking events
- Implemented TelemetryClient to handle event queuing and flushing to the telemetry endpoint. - Created TtfsTelemetryService for emitting specific telemetry events related to TTFS. - Added tests for TelemetryClient to ensure event queuing and flushing functionality. - Introduced models for reachability drift detection, including DriftResult and DriftedSink. - Developed DriftApiService for interacting with the drift detection API. - Updated FirstSignalCardComponent to emit telemetry events on signal appearance. - Enhanced localization support for first signal component with i18n strings.
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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 VexSignerResponse
|
||||
{
|
||||
Success = true,
|
||||
AttestationDigest = "sha256:abc123",
|
||||
KeyId = "key-1"
|
||||
});
|
||||
|
||||
var request = CreateTestRequest();
|
||||
|
||||
// Act
|
||||
var result = await _service.CreateAttestationAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("sha256:abc123", 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 VexSignerResponse
|
||||
{
|
||||
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 VexSignerResponse
|
||||
{
|
||||
Success = true,
|
||||
AttestationDigest = "sha256:abc123",
|
||||
KeyId = "key-1"
|
||||
});
|
||||
|
||||
_rekorClientMock.Setup(x => x.SubmitAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new VexRekorResponse
|
||||
{
|
||||
Success = true,
|
||||
LogIndex = 12345,
|
||||
Uuid = "rekor-uuid-123"
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
_rekorClientMock.Verify(x => x.SubmitAsync(
|
||||
"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 VexSignerResponse
|
||||
{
|
||||
Success = true,
|
||||
AttestationDigest = "sha256:abc123"
|
||||
});
|
||||
|
||||
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" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user