save progress
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Scanner.EntryTrace.Mesh;
|
||||
using StellaOps.Scanner.EntryTrace.Semantic;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.EntryTrace.Tests.Mesh;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for MeshEntrypointGraph and related types.
|
||||
/// Part of Sprint 0412 - Task TEST-002.
|
||||
/// </summary>
|
||||
public sealed class MeshEntrypointGraphTests
|
||||
{
|
||||
[Fact]
|
||||
public void MeshEntrypointGraph_Creation_SetsProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
var graph = new MeshEntrypointGraph
|
||||
{
|
||||
MeshId = "test-mesh",
|
||||
Type = MeshType.Kubernetes,
|
||||
Namespace = "default",
|
||||
Services = CreateServiceNodes(3),
|
||||
Edges = [],
|
||||
IngressPaths = [],
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-mesh", graph.MeshId);
|
||||
Assert.Equal(MeshType.Kubernetes, graph.Type);
|
||||
Assert.Equal("default", graph.Namespace);
|
||||
Assert.Equal(3, graph.Services.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshEntrypointGraph_FindPathsToService_FindsDirectPath()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServiceNodes(3);
|
||||
var edges = new[]
|
||||
{
|
||||
new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "a->b",
|
||||
SourceServiceId = "svc-0",
|
||||
TargetServiceId = "svc-1",
|
||||
TargetPort = 8080
|
||||
},
|
||||
new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "b->c",
|
||||
SourceServiceId = "svc-1",
|
||||
TargetServiceId = "svc-2",
|
||||
TargetPort = 8080
|
||||
}
|
||||
}.ToImmutableArray();
|
||||
|
||||
var ingressPaths = new[]
|
||||
{
|
||||
new IngressPath
|
||||
{
|
||||
IngressName = "main-ingress",
|
||||
Host = "example.com",
|
||||
Path = "/",
|
||||
TargetServiceId = "svc-0",
|
||||
TargetPort = 8080
|
||||
}
|
||||
}.ToImmutableArray();
|
||||
|
||||
var graph = new MeshEntrypointGraph
|
||||
{
|
||||
MeshId = "test",
|
||||
Type = MeshType.Kubernetes,
|
||||
Services = services,
|
||||
Edges = edges,
|
||||
IngressPaths = ingressPaths,
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
// Act
|
||||
var paths = graph.FindPathsToService("svc-2", maxDepth: 5);
|
||||
|
||||
// Assert
|
||||
Assert.Single(paths);
|
||||
Assert.Equal(2, paths[0].Hops.Length);
|
||||
Assert.True(paths[0].IsExternallyExposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshEntrypointGraph_FindPathsToService_RespectsMaxDepth()
|
||||
{
|
||||
// Arrange - Long chain of services
|
||||
var services = CreateServiceNodes(10);
|
||||
var edges = new List<CrossContainerEdge>();
|
||||
for (var i = 0; i < 9; i++)
|
||||
{
|
||||
edges.Add(new CrossContainerEdge
|
||||
{
|
||||
EdgeId = $"svc-{i}->svc-{i + 1}",
|
||||
SourceServiceId = $"svc-{i}",
|
||||
TargetServiceId = $"svc-{i + 1}",
|
||||
TargetPort = 8080
|
||||
});
|
||||
}
|
||||
|
||||
var graph = new MeshEntrypointGraph
|
||||
{
|
||||
MeshId = "test",
|
||||
Type = MeshType.Kubernetes,
|
||||
Services = services,
|
||||
Edges = edges.ToImmutableArray(),
|
||||
IngressPaths = [],
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
// Act - Limit depth to 3
|
||||
var paths = graph.FindPathsToService("svc-9", maxDepth: 3);
|
||||
|
||||
// Assert - Should not find path since it requires 9 hops
|
||||
Assert.Empty(paths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshEntrypointGraph_FindPathsToService_NoPathExists()
|
||||
{
|
||||
// Arrange - Disconnected services
|
||||
var services = CreateServiceNodes(2);
|
||||
var graph = new MeshEntrypointGraph
|
||||
{
|
||||
MeshId = "test",
|
||||
Type = MeshType.Kubernetes,
|
||||
Services = services,
|
||||
Edges = [],
|
||||
IngressPaths = [],
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
// Act
|
||||
var paths = graph.FindPathsToService("svc-1", maxDepth: 5);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(paths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceNode_Creation_SetsProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
var node = new ServiceNode
|
||||
{
|
||||
ServiceId = "my-service",
|
||||
ContainerName = "app",
|
||||
ImageDigest = "sha256:abc123",
|
||||
ImageReference = "myapp:v1.0.0",
|
||||
Entrypoints = [],
|
||||
ExposedPorts = [8080, 8443],
|
||||
InternalDns = ["my-service.default.svc.cluster.local"],
|
||||
Labels = new Dictionary<string, string> { ["app"] = "my-app" }.ToImmutableDictionary(),
|
||||
Replicas = 3
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("my-service", node.ServiceId);
|
||||
Assert.Equal("app", node.ContainerName);
|
||||
Assert.Equal(2, node.ExposedPorts.Length);
|
||||
Assert.Equal(3, node.Replicas);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossContainerEdge_Creation_SetsProperties()
|
||||
{
|
||||
// Arrange & Act
|
||||
var edge = new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "frontend->backend",
|
||||
SourceServiceId = "frontend",
|
||||
TargetServiceId = "backend",
|
||||
SourcePort = 0,
|
||||
TargetPort = 8080,
|
||||
Protocol = "http",
|
||||
IsExplicit = true
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("frontend->backend", edge.EdgeId);
|
||||
Assert.Equal("frontend", edge.SourceServiceId);
|
||||
Assert.Equal("backend", edge.TargetServiceId);
|
||||
Assert.Equal(8080, edge.TargetPort);
|
||||
Assert.True(edge.IsExplicit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CrossContainerPath_TracksHops()
|
||||
{
|
||||
// Arrange
|
||||
var hops = new[]
|
||||
{
|
||||
new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "a->b",
|
||||
SourceServiceId = "a",
|
||||
TargetServiceId = "b",
|
||||
TargetPort = 8080
|
||||
},
|
||||
new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "b->c",
|
||||
SourceServiceId = "b",
|
||||
TargetServiceId = "c",
|
||||
TargetPort = 9090
|
||||
}
|
||||
}.ToImmutableArray();
|
||||
|
||||
// Act
|
||||
var path = new CrossContainerPath
|
||||
{
|
||||
PathId = "path-1",
|
||||
SourceServiceId = "a",
|
||||
TargetServiceId = "c",
|
||||
Hops = hops,
|
||||
IsExternallyExposed = true,
|
||||
VulnerableComponents = ["pkg:npm/lodash@4.17.20"],
|
||||
TotalLatencyEstimateMs = 10
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, path.Hops.Length);
|
||||
Assert.True(path.IsExternallyExposed);
|
||||
Assert.Single(path.VulnerableComponents);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IngressPath_TracksExternalExposure()
|
||||
{
|
||||
// Arrange & Act
|
||||
var ingress = new IngressPath
|
||||
{
|
||||
IngressName = "main-ingress",
|
||||
Host = "api.example.com",
|
||||
Path = "/v1",
|
||||
TargetServiceId = "api-gateway",
|
||||
TargetPort = 8080,
|
||||
TlsEnabled = true,
|
||||
TlsSecretName = "api-tls-secret",
|
||||
Annotations = new Dictionary<string, string>
|
||||
{
|
||||
["nginx.ingress.kubernetes.io/rewrite-target"] = "/"
|
||||
}.ToImmutableDictionary()
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("main-ingress", ingress.IngressName);
|
||||
Assert.Equal("api.example.com", ingress.Host);
|
||||
Assert.True(ingress.TlsEnabled);
|
||||
Assert.NotNull(ingress.TlsSecretName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshEntrypointGraphBuilder_BuildsGraph()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new MeshEntrypointGraphBuilder("test-mesh", MeshType.DockerCompose);
|
||||
|
||||
// Act
|
||||
var graph = builder
|
||||
.WithNamespace("my-project")
|
||||
.WithService(new ServiceNode
|
||||
{
|
||||
ServiceId = "web",
|
||||
ContainerName = "web",
|
||||
ImageDigest = "sha256:abc",
|
||||
Entrypoints = [],
|
||||
ExposedPorts = [80]
|
||||
})
|
||||
.WithService(new ServiceNode
|
||||
{
|
||||
ServiceId = "db",
|
||||
ContainerName = "db",
|
||||
ImageDigest = "sha256:def",
|
||||
Entrypoints = [],
|
||||
ExposedPorts = [5432]
|
||||
})
|
||||
.WithEdge(new CrossContainerEdge
|
||||
{
|
||||
EdgeId = "web->db",
|
||||
SourceServiceId = "web",
|
||||
TargetServiceId = "db",
|
||||
TargetPort = 5432
|
||||
})
|
||||
.Build();
|
||||
|
||||
// Assert
|
||||
Assert.Equal("test-mesh", graph.MeshId);
|
||||
Assert.Equal(MeshType.DockerCompose, graph.Type);
|
||||
Assert.Equal(2, graph.Services.Length);
|
||||
Assert.Single(graph.Edges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshType_AllValuesAreDistinct()
|
||||
{
|
||||
// Assert
|
||||
var values = Enum.GetValues<MeshType>();
|
||||
var distinctCount = values.Distinct().Count();
|
||||
Assert.Equal(values.Length, distinctCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshEntrypointGraph_MultiplePaths_FindsAll()
|
||||
{
|
||||
// Arrange - Diamond pattern: A -> B -> D, A -> C -> D
|
||||
var services = new[]
|
||||
{
|
||||
CreateServiceNode("A"),
|
||||
CreateServiceNode("B"),
|
||||
CreateServiceNode("C"),
|
||||
CreateServiceNode("D")
|
||||
}.ToImmutableArray();
|
||||
|
||||
var edges = new[]
|
||||
{
|
||||
CreateEdge("A", "B"),
|
||||
CreateEdge("A", "C"),
|
||||
CreateEdge("B", "D"),
|
||||
CreateEdge("C", "D")
|
||||
}.ToImmutableArray();
|
||||
|
||||
var ingress = new[]
|
||||
{
|
||||
new IngressPath
|
||||
{
|
||||
IngressName = "main",
|
||||
Host = "test.com",
|
||||
Path = "/",
|
||||
TargetServiceId = "A",
|
||||
TargetPort = 80
|
||||
}
|
||||
}.ToImmutableArray();
|
||||
|
||||
var graph = new MeshEntrypointGraph
|
||||
{
|
||||
MeshId = "diamond",
|
||||
Type = MeshType.Kubernetes,
|
||||
Services = services,
|
||||
Edges = edges,
|
||||
IngressPaths = ingress,
|
||||
AnalyzedAt = DateTime.UtcNow.ToString("O")
|
||||
};
|
||||
|
||||
// Act
|
||||
var paths = graph.FindPathsToService("D", maxDepth: 5);
|
||||
|
||||
// Assert - Should find both paths: A->B->D and A->C->D
|
||||
Assert.Equal(2, paths.Length);
|
||||
Assert.All(paths, p => Assert.True(p.IsExternallyExposed));
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private static ImmutableArray<ServiceNode> CreateServiceNodes(int count)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<ServiceNode>(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
builder.Add(CreateServiceNode($"svc-{i}"));
|
||||
}
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static ServiceNode CreateServiceNode(string serviceId)
|
||||
{
|
||||
return new ServiceNode
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
ContainerName = serviceId,
|
||||
ImageDigest = $"sha256:{serviceId}",
|
||||
ImageReference = $"{serviceId}:latest",
|
||||
Entrypoints = [],
|
||||
ExposedPorts = [8080]
|
||||
};
|
||||
}
|
||||
|
||||
private static CrossContainerEdge CreateEdge(string from, string to)
|
||||
{
|
||||
return new CrossContainerEdge
|
||||
{
|
||||
EdgeId = $"{from}->{to}",
|
||||
SourceServiceId = from,
|
||||
TargetServiceId = to,
|
||||
TargetPort = 8080
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user