803 lines
24 KiB
C#
803 lines
24 KiB
C#
// -----------------------------------------------------------------------------
|
|
// K8sBoundaryExtractorTests.cs
|
|
// Sprint: SPRINT_3800_0002_0002_boundary_k8s
|
|
// Description: Unit tests for K8sBoundaryExtractor.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using StellaOps.Scanner.Reachability.Boundary;
|
|
using StellaOps.Scanner.Reachability.Gates;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Scanner.Reachability.Tests;
|
|
|
|
public class K8sBoundaryExtractorTests
|
|
{
|
|
private readonly K8sBoundaryExtractor _extractor;
|
|
|
|
public K8sBoundaryExtractorTests()
|
|
{
|
|
_extractor = new K8sBoundaryExtractor(
|
|
NullLogger<K8sBoundaryExtractor>.Instance);
|
|
}
|
|
|
|
#region Priority and CanHandle
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Priority_Returns200_HigherThanRichGraphExtractor()
|
|
{
|
|
Assert.Equal(200, _extractor.Priority);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Theory]
|
|
[InlineData("k8s", true)]
|
|
[InlineData("K8S", true)]
|
|
[InlineData("kubernetes", true)]
|
|
[InlineData("Kubernetes", true)]
|
|
[InlineData("static", false)]
|
|
[InlineData("runtime", false)]
|
|
public void CanHandle_WithSource_ReturnsExpected(string source, bool expected)
|
|
{
|
|
var context = BoundaryExtractionContext.Empty with { Source = source };
|
|
Assert.Equal(expected, _extractor.CanHandle(context));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void CanHandle_WithK8sAnnotations_ReturnsTrue()
|
|
{
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["kubernetes.io/ingress.class"] = "nginx"
|
|
}
|
|
};
|
|
|
|
Assert.True(_extractor.CanHandle(context));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void CanHandle_WithIngressAnnotation_ReturnsTrue()
|
|
{
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
|
|
}
|
|
};
|
|
|
|
Assert.True(_extractor.CanHandle(context));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void CanHandle_WithEmptyAnnotations_ReturnsFalse()
|
|
{
|
|
var context = BoundaryExtractionContext.Empty;
|
|
Assert.False(_extractor.CanHandle(context));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extract - Exposure Detection
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithInternetFacing_ReturnsPublicExposure()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
IsInternetFacing = true
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Exposure);
|
|
Assert.Equal("public", result.Exposure.Level);
|
|
Assert.True(result.Exposure.InternetFacing);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithIngressClass_ReturnsInternetFacing()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["kubernetes.io/ingress.class"] = "nginx"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Exposure);
|
|
Assert.True(result.Exposure.InternetFacing);
|
|
Assert.True(result.Exposure.BehindProxy);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Theory]
|
|
[InlineData("LoadBalancer", "public", true)]
|
|
[InlineData("NodePort", "internal", false)]
|
|
[InlineData("ClusterIP", "private", false)]
|
|
public void Extract_WithServiceType_ReturnsExpectedExposure(
|
|
string serviceType, string expectedLevel, bool expectedInternetFacing)
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["service.type"] = serviceType
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Exposure);
|
|
Assert.Equal(expectedLevel, result.Exposure.Level);
|
|
Assert.Equal(expectedInternetFacing, result.Exposure.InternetFacing);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithExternalPorts_ReturnsInternalLevel()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
PortBindings = new Dictionary<int, string> { [443] = "https" }
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Exposure);
|
|
Assert.Equal("internal", result.Exposure.Level);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithDmzZone_ReturnsInternalLevel()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
NetworkZone = "dmz"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Exposure);
|
|
Assert.Equal("internal", result.Exposure.Level);
|
|
Assert.Equal("dmz", result.Exposure.Zone);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extract - Surface Detection
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithServicePath_ReturnsSurfaceWithPath()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["service.path"] = "/api/v1"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("/api/v1", result.Surface.Path);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithRewriteTarget_ReturnsSurfaceWithPath()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/rewrite-target"] = "/backend"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("/backend", result.Surface.Path);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithNamespace_ReturnsSurfaceWithNamespacePath()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Namespace = "production"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("/production", result.Surface.Path);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithTlsAnnotation_ReturnsHttpsProtocol()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["cert-manager.io/cluster-issuer"] = "letsencrypt"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("https", result.Surface.Protocol);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithGrpcAnnotation_ReturnsGrpcProtocol()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["grpc.service"] = "UserService"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("grpc", result.Surface.Protocol);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithPortBinding_ReturnsSurfaceWithPort()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
PortBindings = new Dictionary<int, string> { [8080] = "http" }
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal(8080, result.Surface.Port);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithIngressHost_ReturnsSurfaceWithHost()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["ingress.host"] = "api.example.com"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Surface);
|
|
Assert.Equal("api.example.com", result.Surface.Host);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extract - Auth Detection
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithBasicAuth_ReturnsBasicAuthType()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/auth-secret"] = "basic-auth-secret"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Auth);
|
|
Assert.True(result.Auth.Required);
|
|
Assert.Equal("basic", result.Auth.Type);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithOAuth_ReturnsOAuth2Type()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/oauth2-signin"] = "https://auth.example.com"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Auth);
|
|
Assert.True(result.Auth.Required);
|
|
Assert.Equal("oauth2", result.Auth.Type);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithMtls_ReturnsMtlsType()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/auth-tls-secret"] = "client-certs"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Auth);
|
|
Assert.True(result.Auth.Required);
|
|
Assert.Equal("mtls", result.Auth.Type);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithExplicitAuthType_ReturnsSpecifiedType()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/auth-type"] = "jwt"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Auth);
|
|
Assert.True(result.Auth.Required);
|
|
Assert.Equal("jwt", result.Auth.Type);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithAuthRoles_ReturnsRolesList()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/auth-type"] = "oauth2",
|
|
["nginx.ingress.kubernetes.io/auth-roles"] = "admin,editor,viewer"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Auth);
|
|
Assert.NotNull(result.Auth.Roles);
|
|
Assert.Equal(3, result.Auth.Roles.Count);
|
|
Assert.Contains("admin", result.Auth.Roles);
|
|
Assert.Contains("editor", result.Auth.Roles);
|
|
Assert.Contains("viewer", result.Auth.Roles);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithNoAuth_ReturnsNullAuth()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Null(result.Auth);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extract - Controls Detection
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithNetworkPolicy_ReturnsNetworkPolicyControl()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Namespace = "production",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["network.policy.enabled"] = "true"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Controls);
|
|
var control = Assert.Single(result.Controls);
|
|
Assert.Equal("network_policy", control.Type);
|
|
Assert.True(control.Active);
|
|
Assert.Equal("production", control.Config);
|
|
Assert.Equal("high", control.Effectiveness);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithRateLimit_ReturnsRateLimitControl()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/rate-limit"] = "100"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Controls);
|
|
var control = Assert.Single(result.Controls);
|
|
Assert.Equal("rate_limit", control.Type);
|
|
Assert.True(control.Active);
|
|
Assert.Equal("medium", control.Effectiveness);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithIpAllowlist_ReturnsIpAllowlistControl()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/whitelist-source-range"] = "10.0.0.0/8"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Controls);
|
|
var control = Assert.Single(result.Controls);
|
|
Assert.Equal("ip_allowlist", control.Type);
|
|
Assert.True(control.Active);
|
|
Assert.Equal("high", control.Effectiveness);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithWaf_ReturnsWafControl()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/enable-modsecurity"] = "true"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Controls);
|
|
var control = Assert.Single(result.Controls);
|
|
Assert.Equal("waf", control.Type);
|
|
Assert.True(control.Active);
|
|
Assert.Equal("high", control.Effectiveness);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithMultipleControls_ReturnsAllControls()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["network.policy.enabled"] = "true",
|
|
["nginx.ingress.kubernetes.io/rate-limit"] = "100",
|
|
["nginx.ingress.kubernetes.io/enable-modsecurity"] = "true"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result.Controls);
|
|
Assert.Equal(3, result.Controls.Count);
|
|
Assert.Contains(result.Controls, c => c.Type == "network_policy");
|
|
Assert.Contains(result.Controls, c => c.Type == "rate_limit");
|
|
Assert.Contains(result.Controls, c => c.Type == "waf");
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithNoControls_ReturnsNullControls()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Null(result.Controls);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Extract - Confidence and Metadata
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_BaseConfidence_Returns0Point7()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(0.7, result.Confidence, precision: 2);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithIngressAnnotation_IncreasesConfidence()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(0.85, result.Confidence, precision: 2);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithServiceType_IncreasesConfidence()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["service.type"] = "ClusterIP"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal(0.8, result.Confidence, precision: 2);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_MaxConfidence_CapsAt0Point95()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["kubernetes.io/ingress.class"] = "nginx",
|
|
["service.type"] = "LoadBalancer"
|
|
}
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.True(result.Confidence <= 0.95);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_ReturnsK8sSource()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("k8s", result.Source);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_BuildsEvidenceRef_WithNamespaceAndEnvironment()
|
|
{
|
|
var root = new RichGraphRoot("root-123", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Namespace = "production",
|
|
EnvironmentId = "env-456"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("k8s/production/env-456/root-123", result.EvidenceRef);
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_ReturnsNetworkKind()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s"
|
|
};
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.NotNull(result);
|
|
Assert.Equal("network", result.Kind);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region ExtractAsync
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExtractAsync_ReturnsSameResultAsExtract()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "k8s", null);
|
|
var context = BoundaryExtractionContext.Empty with
|
|
{
|
|
Source = "k8s",
|
|
Namespace = "production",
|
|
Annotations = new Dictionary<string, string>
|
|
{
|
|
["kubernetes.io/ingress.class"] = "nginx"
|
|
}
|
|
};
|
|
|
|
var syncResult = _extractor.Extract(root, null, context);
|
|
var asyncResult = await _extractor.ExtractAsync(root, null, context);
|
|
|
|
Assert.NotNull(syncResult);
|
|
Assert.NotNull(asyncResult);
|
|
Assert.Equal(syncResult.Kind, asyncResult.Kind);
|
|
Assert.Equal(syncResult.Source, asyncResult.Source);
|
|
Assert.Equal(syncResult.Confidence, asyncResult.Confidence);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Edge Cases
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WithNullRoot_ThrowsArgumentNullException()
|
|
{
|
|
var context = BoundaryExtractionContext.Empty with { Source = "k8s" };
|
|
Assert.Throws<ArgumentNullException>(() => _extractor.Extract(null!, null, context));
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Extract_WhenCannotHandle_ReturnsNull()
|
|
{
|
|
var root = new RichGraphRoot("root-1", "static", null);
|
|
var context = BoundaryExtractionContext.Empty with { Source = "static" };
|
|
|
|
var result = _extractor.Extract(root, null, context);
|
|
|
|
Assert.Null(result);
|
|
}
|
|
|
|
#endregion
|
|
}
|