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,412 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// RichGraphBoundaryExtractorTests.cs
|
||||
// Sprint: SPRINT_3800_0002_0001_boundary_richgraph
|
||||
// Description: Unit tests for RichGraphBoundaryExtractor.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Scanner.Reachability.Boundary;
|
||||
using StellaOps.Scanner.Reachability.Gates;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Reachability.Tests;
|
||||
|
||||
public class RichGraphBoundaryExtractorTests
|
||||
{
|
||||
private readonly RichGraphBoundaryExtractor _extractor;
|
||||
|
||||
public RichGraphBoundaryExtractorTests()
|
||||
{
|
||||
_extractor = new RichGraphBoundaryExtractor(
|
||||
NullLogger<RichGraphBoundaryExtractor>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_HttpRoot_ReturnsBoundaryWithApiSurface()
|
||||
{
|
||||
var root = new RichGraphRoot("root-http", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "com.example.Controller.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: "POST /api/users",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("network", result.Kind);
|
||||
Assert.NotNull(result.Surface);
|
||||
Assert.Equal("api", result.Surface.Type);
|
||||
Assert.Equal("https", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_GrpcRoot_ReturnsBoundaryWithGrpcProtocol()
|
||||
{
|
||||
var root = new RichGraphRoot("root-grpc", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "com.example.UserService.getUser",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "grpc_method",
|
||||
Display: "UserService.GetUser",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Surface);
|
||||
Assert.Equal("grpc", result.Surface.Protocol);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_CliRoot_ReturnsProcessBoundary()
|
||||
{
|
||||
var root = new RichGraphRoot("root-cli", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Main",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "csharp",
|
||||
Kind: "cli_command",
|
||||
Display: "stella scan",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("process", result.Kind);
|
||||
Assert.NotNull(result.Surface);
|
||||
Assert.Equal("cli", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_LibraryPhase_ReturnsLibraryBoundary()
|
||||
{
|
||||
var root = new RichGraphRoot("root-lib", "library", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Utils.parseJson",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "javascript",
|
||||
Kind: "function",
|
||||
Display: "parseJson",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("library", result.Kind);
|
||||
Assert.NotNull(result.Surface);
|
||||
Assert.Equal("library", result.Surface.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithAuthGate_SetsAuthRequired()
|
||||
{
|
||||
var root = new RichGraphRoot("root-auth", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Controller.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var context = BoundaryExtractionContext.FromGates(new[]
|
||||
{
|
||||
new DetectedGate
|
||||
{
|
||||
Type = GateType.AuthRequired,
|
||||
Detail = "JWT token required",
|
||||
GuardSymbol = "AuthFilter.doFilter",
|
||||
Confidence = 0.9,
|
||||
DetectionMethod = "pattern_match"
|
||||
}
|
||||
});
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, context);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Auth);
|
||||
Assert.True(result.Auth.Required);
|
||||
Assert.Equal("jwt", result.Auth.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithAdminGate_SetsAdminRole()
|
||||
{
|
||||
var root = new RichGraphRoot("root-admin", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "AdminController.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var context = BoundaryExtractionContext.FromGates(new[]
|
||||
{
|
||||
new DetectedGate
|
||||
{
|
||||
Type = GateType.AdminOnly,
|
||||
Detail = "Requires admin role",
|
||||
GuardSymbol = "RoleFilter.check",
|
||||
Confidence = 0.85,
|
||||
DetectionMethod = "annotation"
|
||||
}
|
||||
});
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, context);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Auth);
|
||||
Assert.True(result.Auth.Required);
|
||||
Assert.NotNull(result.Auth.Roles);
|
||||
Assert.Contains("admin", result.Auth.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithFeatureFlagGate_AddsControl()
|
||||
{
|
||||
var root = new RichGraphRoot("root-ff", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "BetaFeature.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var context = BoundaryExtractionContext.FromGates(new[]
|
||||
{
|
||||
new DetectedGate
|
||||
{
|
||||
Type = GateType.FeatureFlag,
|
||||
Detail = "beta_users_only",
|
||||
GuardSymbol = "FeatureFlags.isEnabled",
|
||||
Confidence = 0.95,
|
||||
DetectionMethod = "call_analysis"
|
||||
}
|
||||
});
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, context);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Controls);
|
||||
Assert.Single(result.Controls);
|
||||
Assert.Equal("feature_flag", result.Controls[0].Type);
|
||||
Assert.True(result.Controls[0].Active);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_WithInternetFacingContext_SetsExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-public", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "PublicApi.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var context = BoundaryExtractionContext.ForEnvironment(
|
||||
"production",
|
||||
isInternetFacing: true,
|
||||
networkZone: "dmz");
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, context);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Exposure);
|
||||
Assert.True(result.Exposure.InternetFacing);
|
||||
Assert.Equal("dmz", result.Exposure.Zone);
|
||||
Assert.Equal("public", result.Exposure.Level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_InternalService_SetsInternalExposure()
|
||||
{
|
||||
var root = new RichGraphRoot("root-internal", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "InternalService.process",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "internal_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.Exposure);
|
||||
Assert.False(result.Exposure.InternetFacing);
|
||||
Assert.Equal("internal", result.Exposure.Level);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_SetsConfidenceBasedOnContext()
|
||||
{
|
||||
var root = new RichGraphRoot("root-1", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Api.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
// Empty context should have lower confidence
|
||||
var emptyResult = _extractor.Extract(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
// Rich context should have higher confidence
|
||||
var richContext = new BoundaryExtractionContext
|
||||
{
|
||||
IsInternetFacing = true,
|
||||
NetworkZone = "dmz",
|
||||
DetectedGates = new[]
|
||||
{
|
||||
new DetectedGate
|
||||
{
|
||||
Type = GateType.AuthRequired,
|
||||
Detail = "auth",
|
||||
GuardSymbol = "auth",
|
||||
Confidence = 0.9,
|
||||
DetectionMethod = "test"
|
||||
}
|
||||
}
|
||||
};
|
||||
var richResult = _extractor.Extract(root, rootNode, richContext);
|
||||
|
||||
Assert.NotNull(emptyResult);
|
||||
Assert.NotNull(richResult);
|
||||
Assert.True(richResult.Confidence > emptyResult.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extract_IsDeterministic()
|
||||
{
|
||||
var root = new RichGraphRoot("root-det", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Api.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: "GET /api/test",
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var context = BoundaryExtractionContext.FromGates(new[]
|
||||
{
|
||||
new DetectedGate
|
||||
{
|
||||
Type = GateType.AuthRequired,
|
||||
Detail = "JWT",
|
||||
GuardSymbol = "Auth",
|
||||
Confidence = 0.9,
|
||||
DetectionMethod = "test"
|
||||
}
|
||||
});
|
||||
|
||||
var result1 = _extractor.Extract(root, rootNode, context);
|
||||
var result2 = _extractor.Extract(root, rootNode, context);
|
||||
|
||||
Assert.NotNull(result1);
|
||||
Assert.NotNull(result2);
|
||||
Assert.Equal(result1.Kind, result2.Kind);
|
||||
Assert.Equal(result1.Surface?.Type, result2.Surface?.Type);
|
||||
Assert.Equal(result1.Auth?.Required, result2.Auth?.Required);
|
||||
Assert.Equal(result1.Confidence, result2.Confidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanHandle_AlwaysReturnsTrue()
|
||||
{
|
||||
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.Empty));
|
||||
Assert.True(_extractor.CanHandle(BoundaryExtractionContext.ForEnvironment("test")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Priority_ReturnsBaseValue()
|
||||
{
|
||||
Assert.Equal(100, _extractor.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExtractAsync_ReturnsResult()
|
||||
{
|
||||
var root = new RichGraphRoot("root-async", "runtime", null);
|
||||
var rootNode = new RichGraphNode(
|
||||
Id: "node-1",
|
||||
SymbolId: "Api.handle",
|
||||
CodeId: null,
|
||||
Purl: null,
|
||||
Lang: "java",
|
||||
Kind: "http_handler",
|
||||
Display: null,
|
||||
BuildId: null,
|
||||
Evidence: null,
|
||||
Attributes: null,
|
||||
SymbolDigest: null);
|
||||
|
||||
var result = await _extractor.ExtractAsync(root, rootNode, BoundaryExtractionContext.Empty);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("network", result.Kind);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user