too much for one commit
This commit is contained in:
360
tests/EntKube.Provisioning.Tests/Domain/GrafanaInstanceTests.cs
Normal file
360
tests/EntKube.Provisioning.Tests/Domain/GrafanaInstanceTests.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using EntKube.Provisioning.Domain;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Provisioning.Tests.Domain;
|
||||
|
||||
public class GrafanaInstanceTests
|
||||
{
|
||||
private readonly Guid environmentId = Guid.NewGuid();
|
||||
private readonly Guid clusterId = Guid.NewGuid();
|
||||
|
||||
// ─── Creation ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Create_WithValidInputs_CreatesInstanceInPendingState()
|
||||
{
|
||||
// Arrange & Act — An operator provisions a Grafana instance for an environment.
|
||||
// Grafana is per-environment: one centralized visualization for all clusters
|
||||
// in that environment, deployed to a specific cluster.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(
|
||||
environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Assert — The instance should be in Pending state with sensible defaults.
|
||||
|
||||
instance.Id.Should().NotBe(Guid.Empty);
|
||||
instance.EnvironmentId.Should().Be(environmentId);
|
||||
instance.ClusterId.Should().Be(clusterId);
|
||||
instance.Name.Should().Be("prod-grafana");
|
||||
instance.Namespace.Should().Be("monitoring");
|
||||
instance.Status.Should().Be(GrafanaInstanceStatus.Pending);
|
||||
instance.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
|
||||
// Grafana defaults
|
||||
|
||||
instance.Persistence.Should().BeFalse();
|
||||
instance.PersistenceSize.Should().Be("10Gi");
|
||||
instance.Oidc.Should().BeNull();
|
||||
instance.Ingress.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyEnvironmentId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act — Grafana must belong to an environment.
|
||||
|
||||
Action act = () => GrafanaInstance.Create(Guid.Empty, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("environmentId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyClusterId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act — Grafana must be deployed to a specific cluster.
|
||||
|
||||
Action act = () => GrafanaInstance.Create(environmentId, Guid.Empty, "prod-grafana", "monitoring");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("clusterId");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
Action act = () => GrafanaInstance.Create(environmentId, clusterId, "", "monitoring");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyNamespace_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
Action act = () => GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("ns");
|
||||
}
|
||||
|
||||
// ─── Persistence Configuration ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConfigurePersistence_UpdatesSettings()
|
||||
{
|
||||
// Arrange — An operator wants Grafana to keep dashboards across restarts.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act
|
||||
|
||||
instance.ConfigurePersistence(enabled: true, size: "20Gi");
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Persistence.Should().BeTrue();
|
||||
instance.PersistenceSize.Should().Be("20Gi");
|
||||
}
|
||||
|
||||
// ─── OIDC Configuration ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConfigureOidc_SetsOidcProvider()
|
||||
{
|
||||
// Arrange — Integrate Grafana with an OIDC provider (e.g., Entra ID)
|
||||
// so users authenticate with corporate credentials.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act
|
||||
|
||||
OidcConfiguration oidc = new(
|
||||
Provider: "generic_oauth",
|
||||
ClientId: "grafana-client-id",
|
||||
ClientSecretRef: "vault://monitoring/grafana-oidc-client-secret",
|
||||
AuthUrl: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/authorize",
|
||||
TokenUrl: "https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token",
|
||||
ApiUrl: "https://graph.microsoft.com/oidc/userinfo",
|
||||
Scopes: "openid profile email",
|
||||
RoleAttributePath: "contains(groups[*], 'admin-group-id') && 'Admin' || 'Viewer'",
|
||||
AutoLogin: true);
|
||||
|
||||
instance.ConfigureOidc(oidc);
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Oidc.Should().NotBeNull();
|
||||
instance.Oidc!.Provider.Should().Be("generic_oauth");
|
||||
instance.Oidc.ClientId.Should().Be("grafana-client-id");
|
||||
instance.Oidc.ClientSecretRef.Should().Be("vault://monitoring/grafana-oidc-client-secret");
|
||||
instance.Oidc.AutoLogin.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureOidc_WithEmptyClientId_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act & Assert
|
||||
|
||||
OidcConfiguration oidc = new(
|
||||
Provider: "generic_oauth",
|
||||
ClientId: "",
|
||||
ClientSecretRef: "vault://secret",
|
||||
AuthUrl: "https://example.com/auth",
|
||||
TokenUrl: "https://example.com/token",
|
||||
ApiUrl: "https://example.com/userinfo",
|
||||
Scopes: "openid",
|
||||
RoleAttributePath: null,
|
||||
AutoLogin: false);
|
||||
|
||||
Action act = () => instance.ConfigureOidc(oidc);
|
||||
|
||||
act.Should().Throw<ArgumentException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveOidc_ClearsOidcConfiguration()
|
||||
{
|
||||
// Arrange — A Grafana instance with OIDC configured.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
OidcConfiguration oidc = new(
|
||||
Provider: "generic_oauth",
|
||||
ClientId: "client-id",
|
||||
ClientSecretRef: "vault://secret",
|
||||
AuthUrl: "https://example.com/auth",
|
||||
TokenUrl: "https://example.com/token",
|
||||
ApiUrl: "https://example.com/userinfo",
|
||||
Scopes: "openid",
|
||||
RoleAttributePath: null,
|
||||
AutoLogin: false);
|
||||
|
||||
instance.ConfigureOidc(oidc);
|
||||
|
||||
// Act — Remove OIDC, fall back to local auth.
|
||||
|
||||
instance.RemoveOidc();
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Oidc.Should().BeNull();
|
||||
}
|
||||
|
||||
// ─── Ingress ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConfigureIngress_SetsGrafanaRouting()
|
||||
{
|
||||
// Arrange — Publish Grafana externally with its own hostname.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act
|
||||
|
||||
GrafanaIngressConfiguration ingress = new(
|
||||
Enabled: true,
|
||||
Provider: IngressProvider.GatewayApi,
|
||||
Hostname: "grafana.prod01.example.com",
|
||||
TlsEnabled: true,
|
||||
TlsCertificateMode: TlsCertificateMode.CertManager,
|
||||
TlsSecretName: null,
|
||||
ClusterIssuer: "letsencrypt-prod");
|
||||
|
||||
instance.ConfigureIngress(ingress);
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Ingress.Should().NotBeNull();
|
||||
instance.Ingress!.Enabled.Should().BeTrue();
|
||||
instance.Ingress.Provider.Should().Be(IngressProvider.GatewayApi);
|
||||
instance.Ingress.Hostname.Should().Be("grafana.prod01.example.com");
|
||||
instance.Ingress.TlsCertificateMode.Should().Be(TlsCertificateMode.CertManager);
|
||||
instance.Ingress.ClusterIssuer.Should().Be("letsencrypt-prod");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisableIngress_ClearsIngressConfig()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
GrafanaIngressConfiguration ingress = new(
|
||||
Enabled: true,
|
||||
Provider: IngressProvider.GatewayApi,
|
||||
Hostname: "grafana.example.com",
|
||||
TlsEnabled: true,
|
||||
TlsCertificateMode: TlsCertificateMode.Manual,
|
||||
TlsSecretName: "grafana-tls");
|
||||
|
||||
instance.ConfigureIngress(ingress);
|
||||
|
||||
// Act
|
||||
|
||||
instance.DisableIngress();
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Ingress.Should().BeNull();
|
||||
}
|
||||
|
||||
// ─── Dashboards ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AddDashboard_StoresDashboardDefinition()
|
||||
{
|
||||
// Arrange — Dashboards are JSON definitions deployed as ConfigMaps
|
||||
// with the grafana_dashboard=1 label so the sidecar picks them up.
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act — Add a Kubernetes cluster dashboard.
|
||||
|
||||
GrafanaDashboard dashboard = new(
|
||||
Name: "k8s-cluster",
|
||||
DisplayName: "Kubernetes Cluster",
|
||||
Category: DashboardCategory.Kubernetes,
|
||||
JsonContent: """{"dashboard": {"title": "K8s Cluster"}}""");
|
||||
|
||||
instance.AddDashboard(dashboard);
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Dashboards.Should().HaveCount(1);
|
||||
instance.Dashboards[0].Name.Should().Be("k8s-cluster");
|
||||
instance.Dashboards[0].Category.Should().Be(DashboardCategory.Kubernetes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddDashboard_WithDuplicateName_ThrowsInvalidOperationException()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
GrafanaDashboard dashboard = new(
|
||||
Name: "k8s-cluster",
|
||||
DisplayName: "Kubernetes Cluster",
|
||||
Category: DashboardCategory.Kubernetes,
|
||||
JsonContent: """{"dashboard": {}}""");
|
||||
|
||||
instance.AddDashboard(dashboard);
|
||||
|
||||
// Act & Assert — Can't add two dashboards with the same name.
|
||||
|
||||
Action act = () => instance.AddDashboard(dashboard);
|
||||
|
||||
act.Should().Throw<InvalidOperationException>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveDashboard_DeletesByName()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
instance.AddDashboard(new GrafanaDashboard("k8s-cluster", "Kubernetes Cluster", DashboardCategory.Kubernetes, "{}"));
|
||||
instance.AddDashboard(new GrafanaDashboard("istio-mesh", "Istio Mesh", DashboardCategory.Istio, "{}"));
|
||||
|
||||
// Act
|
||||
|
||||
instance.RemoveDashboard("k8s-cluster");
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Dashboards.Should().HaveCount(1);
|
||||
instance.Dashboards[0].Name.Should().Be("istio-mesh");
|
||||
}
|
||||
|
||||
// ─── Lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MarkRunning_SetsStatusToRunning()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act
|
||||
|
||||
instance.MarkRunning();
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Status.Should().Be(GrafanaInstanceStatus.Running);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkDegraded_SetsStatusToDegraded()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
GrafanaInstance instance = GrafanaInstance.Create(environmentId, clusterId, "prod-grafana", "monitoring");
|
||||
|
||||
// Act
|
||||
|
||||
instance.MarkDegraded();
|
||||
|
||||
// Assert
|
||||
|
||||
instance.Status.Should().Be(GrafanaInstanceStatus.Degraded);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user