using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using FluentAssertions; namespace EntKube.Clusters.Tests.Domain; public class KubernetesClusterTests { [Fact] public void Register_WithValidInputs_CreatesClusterInPendingState() { // Arrange & Act — A tenant admin registers a new cluster with its name, API URL, // kubeconfig, tenant, and environment it belongs to. Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); KubernetesCluster cluster = KubernetesCluster.Register( "production-eu", "https://k8s.example.com:6443", "apiVersion: v1\nkind: Config", tenantId, "prod-context", environmentId); // Assert — The cluster should be created with a unique ID, linked to the tenant // and environment, and start in Pending state because we haven't verified connectivity yet. cluster.Id.Should().NotBe(Guid.Empty); cluster.Name.Should().Be("production-eu"); cluster.ApiServerUrl.Should().Be("https://k8s.example.com:6443"); cluster.KubeConfig.Should().Contain("apiVersion"); cluster.TenantId.Should().Be(tenantId); cluster.EnvironmentId.Should().Be(environmentId); cluster.ContextName.Should().Be("prod-context"); cluster.Status.Should().Be(ClusterStatus.Pending); cluster.RegisteredAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public void Register_WithEmptyName_ThrowsArgumentException() { // Arrange & Act — Attempting to register a cluster without a name should fail // because every cluster needs a human-readable identifier. Action act = () => KubernetesCluster.Register("", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); // Assert act.Should().Throw() .WithParameterName("name"); } [Fact] public void Register_WithEmptyApiServerUrl_ThrowsArgumentException() { // Arrange & Act — Without an API URL, we can't communicate with the cluster. Action act = () => KubernetesCluster.Register("my-cluster", "", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); // Assert act.Should().Throw() .WithParameterName("apiServerUrl"); } [Fact] public void MarkConnected_SetsStatusToConnected() { // Arrange — A freshly registered cluster in Pending state. KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); // Act — The health check confirms the cluster is reachable. cluster.MarkConnected(); // Assert cluster.Status.Should().Be(ClusterStatus.Connected); cluster.LastHealthCheckAt.Should().NotBeNull(); } [Fact] public void MarkUnreachable_SetsStatusToUnreachable() { // Arrange KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); // Act — The health check fails. cluster.MarkUnreachable(); // Assert cluster.Status.Should().Be(ClusterStatus.Unreachable); cluster.LastHealthCheckAt.Should().NotBeNull(); } [Fact] public void Register_WithEnvironmentId_StoresEnvironmentId() { // Arrange & Act — A cluster is registered and immediately assigned // to an environment (e.g. dev01 → "dev" environment). Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); KubernetesCluster cluster = KubernetesCluster.Register( "dev01", "https://api.dev", "kubeconfig", tenantId, "dev-ctx", environmentId); // Assert — The environment should be stored on the cluster. cluster.EnvironmentId.Should().Be(environmentId); } [Fact] public void Register_WithoutEnvironmentId_ThrowsArgumentException() { // Arrange & Act — Attempting to register a cluster without an environment // should fail because every cluster must belong to an environment. Action act = () => KubernetesCluster.Register( "orphan", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.Empty); // Assert act.Should().Throw() .WithParameterName("environmentId"); } [Fact] public void AssignToEnvironment_ReassignsToNewEnvironment() { // Arrange — A cluster registered in one environment can be reassigned. Guid originalEnvId = Guid.NewGuid(); Guid newEnvId = Guid.NewGuid(); KubernetesCluster cluster = KubernetesCluster.Register( "test01", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", originalEnvId); // Act — Admin reassigns it to a different environment. cluster.AssignToEnvironment(newEnvId); // Assert cluster.EnvironmentId.Should().Be(newEnvId); } [Fact] public void AssignToEnvironment_WithEmptyGuid_ThrowsArgumentException() { // Arrange KubernetesCluster cluster = KubernetesCluster.Register( "test01", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); // Act Action act = () => cluster.AssignToEnvironment(Guid.Empty); // Assert act.Should().Throw() .WithParameterName("environmentId"); } [Fact] public void Register_CreatesClusterWithEmptyComponents() { // Arrange & Act — A freshly registered cluster has no known components yet. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); // Assert cluster.Components.Should().BeEmpty(); } [Fact] public void UpdateComponents_WithCheckResults_PersistsComponents() { // Arrange — After an adoption scan, we record the discovered components // on the cluster so they survive without needing to re-scan. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List results = new() { new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Pod running" }, new List(), new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary { ["replicas"] = "3" })), new ComponentCheckResult("cert-manager", ComponentStatus.NotInstalled, new List(), new List { "No pods found" }) }; // Act cluster.UpdateComponents(results); // Assert — Both components stored with their status and configuration. cluster.Components.Should().HaveCount(2); ClusterComponent kyverno = cluster.Components.First(c => c.ComponentName == "kyverno"); kyverno.Status.Should().Be(ComponentStatus.Installed); kyverno.Version.Should().Be("3.3.4"); kyverno.Namespace.Should().Be("kyverno"); kyverno.Configuration["replicas"].Should().Be("3"); kyverno.LastCheckedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); ClusterComponent certManager = cluster.Components.First(c => c.ComponentName == "cert-manager"); certManager.Status.Should().Be(ComponentStatus.NotInstalled); certManager.Configuration.Should().BeEmpty(); } [Fact] public void UpdateComponents_ReplacesExistingComponents() { // Arrange — A second scan should replace the first scan's results entirely. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List firstScan = new() { new ComponentCheckResult("kyverno", ComponentStatus.NotInstalled, new List(), new List { "Missing" }) }; List secondScan = new() { new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Running" }, new List(), new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary())) }; // Act cluster.UpdateComponents(firstScan); cluster.UpdateComponents(secondScan); // Assert — Only the latest scan results should remain. cluster.Components.Should().HaveCount(1); cluster.Components[0].Status.Should().Be(ComponentStatus.Installed); } [Fact] public void UpdateComponentConfiguration_UpdatesExistingComponentValues() { // Arrange — After a configuration change is deployed, we update the // stored configuration so it reflects the current state. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List results = new() { new ComponentCheckResult("monitoring", ComponentStatus.Installed, new List(), new List(), new DiscoveredConfiguration("45.0.0", "monitoring", "kube-prometheus-stack", new Dictionary { ["retention"] = "30d", ["replicas"] = "2" })) }; cluster.UpdateComponents(results); // Act — Change retention to 90d. Dictionary newValues = new() { ["retention"] = "90d" }; cluster.UpdateComponentConfiguration("monitoring", newValues); // Assert — Retention updated, replicas unchanged. ClusterComponent monitoring = cluster.Components.First(c => c.ComponentName == "monitoring"); monitoring.Configuration["retention"].Should().Be("90d"); monitoring.Configuration["replicas"].Should().Be("2"); } [Fact] public void UpdateComponentConfiguration_ForUnknownComponent_DoesNotThrow() { // Arrange — If a component hasn't been scanned yet, updating its config // is a no-op. This shouldn't break anything. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); // Act — No components recorded yet. Action act = () => cluster.UpdateComponentConfiguration("unknown", new Dictionary { ["k"] = "v" }); // Assert act.Should().NotThrow(); cluster.Components.Should().BeEmpty(); } [Fact] public void UpdateComponentVersion_ForInstalledComponent_UpdatesVersion() { // Arrange — A cluster has Harbor installed at version 1.16.0. After a // successful upgrade, we need to track the new version. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List results = new() { new ComponentCheckResult("harbor", ComponentStatus.Installed, new List { "Running" }, new List(), new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary())) }; cluster.UpdateComponents(results); // Act — Upgrade to 1.17.0. cluster.UpdateComponentVersion("harbor", "1.17.0"); // Assert — The tracked version now reflects the upgrade. ClusterComponent harbor = cluster.Components.First(c => c.ComponentName == "harbor"); harbor.Version.Should().Be("1.17.0"); harbor.LastCheckedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); } [Fact] public void UpdateComponentVersion_ForUnknownComponent_DoesNotThrow() { // Arrange — If the component hasn't been scanned, updating its version // is a safe no-op — no exception, no side effects. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); // Act Action act = () => cluster.UpdateComponentVersion("nonexistent", "1.0.0"); // Assert act.Should().NotThrow(); cluster.Components.Should().BeEmpty(); } [Fact] public void UpdateComponentVersion_IsCaseInsensitive() { // Arrange — Component names should match case-insensitively, just like // MarkComponentInstalled and UpdateComponentConfiguration. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://api.test", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.MarkConnected(); List results = new() { new ComponentCheckResult("Harbor", ComponentStatus.Installed, new List(), new List(), new DiscoveredConfiguration("1.16.0", "harbor", "harbor", new Dictionary())) }; cluster.UpdateComponents(results); // Act — Use different casing. cluster.UpdateComponentVersion("harbor", "1.17.0"); // Assert cluster.Components.First().Version.Should().Be("1.17.0"); } [Fact] public void Register_DefaultsToNoneProvider() { // When a cluster is first registered, no cloud provider is set. // The operator can assign one later when they know where it's hosted. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.Provider.Should().Be(CloudProvider.None); cluster.ProviderCredentials.Should().BeNull(); } [Fact] public void SetProvider_WithCleura_StoresProviderAndCredentials() { // A cluster admin links their cluster to Cleura, providing their // Cleura Cloud username and password so EntKube can call the REST API. KubernetesCluster cluster = KubernetesCluster.Register( "cleura-cluster", "https://k8s.cleura.cloud", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); ProviderCredentials creds = new("my-user", "my-password", "Sto2"); // Act cluster.SetProvider(CloudProvider.Cleura, creds); // Assert cluster.Provider.Should().Be(CloudProvider.Cleura); cluster.ProviderCredentials.Should().NotBeNull(); cluster.ProviderCredentials!.Username.Should().Be("my-user"); cluster.ProviderCredentials!.Region.Should().Be("Sto2"); } [Fact] public void SetProvider_ToNone_ClearsCredentials() { // Unlinking a provider removes the stored credentials. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); cluster.SetProvider(CloudProvider.Cleura, new ProviderCredentials("u", "p", "Sto2")); // Act cluster.SetProvider(CloudProvider.None, null); // Assert cluster.Provider.Should().Be(CloudProvider.None); cluster.ProviderCredentials.Should().BeNull(); } [Fact] public void SetProvider_CleuraWithoutCredentials_Throws() { // Setting Cleura as the provider requires credentials — you can't // call the Cleura API without a username and password. KubernetesCluster cluster = KubernetesCluster.Register( "test", "https://k8s.example.com", "kubeconfig", Guid.NewGuid(), "ctx", Guid.NewGuid()); Action act = () => cluster.SetProvider(CloudProvider.Cleura, null); act.Should().Throw(); } }