using EntKube.Web.Data; using EntKube.Web.Services; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace EntKube.Web.Tests; /// /// Tests for KubernetesOperationsService — the service that performs live /// cluster operations (pod logs, restart, redeploy) using stored kubeconfig. /// /// These tests verify the service's data-layer behavior (looking up clusters, /// building client configs). Actual K8s API calls are integration-level and /// require a live cluster. We test the plumbing, not the wire. /// public class KubernetesOperationsServiceTests : IDisposable { private readonly SqliteConnection connection; private readonly ApplicationDbContext db; private readonly TestDbContextFactory dbFactory; private readonly KubernetesOperationsService sut; public KubernetesOperationsServiceTests() { connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); DbContextOptions options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; db = new ApplicationDbContext(options); dbFactory = new TestDbContextFactory(connection); db.Database.EnsureCreated(); sut = new KubernetesOperationsService(dbFactory); } public void Dispose() { db.Dispose(); connection.Dispose(); } // ── Helpers ── private KubernetesCluster CreateCluster(string? kubeconfig = null) { Tenant tenant = new() { Id = Guid.NewGuid(), Name = "TestCo", Slug = "testco" }; db.Tenants.Add(tenant); Data.Environment env = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "production" }; db.Environments.Add(env); KubernetesCluster cluster = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, EnvironmentId = env.Id, Name = "prod-cluster", ApiServerUrl = "https://k8s.example.com", Kubeconfig = kubeconfig }; db.KubernetesClusters.Add(cluster); db.SaveChanges(); return cluster; } private (AppDeployment deployment, KubernetesCluster cluster) CreateDeploymentWithCluster() { KubernetesCluster cluster = CreateCluster(); Customer customer = new() { Id = Guid.NewGuid(), TenantId = cluster.TenantId, Name = "Contoso" }; db.Customers.Add(customer); App app = new() { Id = Guid.NewGuid(), CustomerId = customer.Id, Name = "billing-api" }; db.Apps.Add(app); AppDeployment deployment = new() { Id = Guid.NewGuid(), AppId = app.Id, Name = "billing-deploy", Type = DeploymentType.Manual, EnvironmentId = cluster.EnvironmentId, ClusterId = cluster.Id, Namespace = "billing-ns" }; db.AppDeployments.Add(deployment); db.SaveChanges(); return (deployment, cluster); } // ════════════════════════════════════════════════════════════════ // GetDeploymentWithClusterAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task GetDeploymentWithClusterAsync_ReturnsDeploymentAndCluster() { // Arrange (AppDeployment deployment, KubernetesCluster cluster) = CreateDeploymentWithCluster(); // Act — the service looks up the deployment with its cluster attached. AppDeployment? result = await sut.GetDeploymentWithClusterAsync(deployment.Id); // Assert result.Should().NotBeNull(); result!.Name.Should().Be("billing-deploy"); result.Cluster.Should().NotBeNull(); result.Cluster.Name.Should().Be("prod-cluster"); } [Fact] public async Task GetDeploymentWithClusterAsync_ReturnsNullForMissing() { // Act AppDeployment? result = await sut.GetDeploymentWithClusterAsync(Guid.NewGuid()); // Assert result.Should().BeNull(); } // ════════════════════════════════════════════════════════════════ // GetPodsAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task GetPodsAsync_WithoutKubeconfig_ReturnsClusterNotConfiguredError() { // Arrange — cluster has no kubeconfig stored. (AppDeployment deployment, _) = CreateDeploymentWithCluster(); // Act — trying to get pods without a kubeconfig should return an error. KubernetesOperationResult> result = await sut.GetPodsAsync(deployment.Id); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("kubeconfig"); } // ════════════════════════════════════════════════════════════════ // RestartDeploymentAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task RestartDeploymentAsync_WithoutKubeconfig_ReturnsError() { // Arrange (AppDeployment deployment, _) = CreateDeploymentWithCluster(); // Act KubernetesOperationResult result = await sut.RestartDeploymentAsync( deployment.Id, "billing-api"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("kubeconfig"); } // ════════════════════════════════════════════════════════════════ // DeletePodAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task DeletePodAsync_WithoutKubeconfig_ReturnsError() { // Arrange (AppDeployment deployment, _) = CreateDeploymentWithCluster(); // Act KubernetesOperationResult result = await sut.DeletePodAsync( deployment.Id, "billing-api-abc123"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("kubeconfig"); } // ════════════════════════════════════════════════════════════════ // GetPodLogsAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task GetPodLogsAsync_WithoutKubeconfig_ReturnsError() { // Arrange (AppDeployment deployment, _) = CreateDeploymentWithCluster(); // Act KubernetesOperationResult result = await sut.GetPodLogsAsync( deployment.Id, "billing-api-abc123"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("kubeconfig"); } // ════════════════════════════════════════════════════════════════ // ScaleDeploymentAsync // ════════════════════════════════════════════════════════════════ [Fact] public async Task ScaleDeploymentAsync_WithoutKubeconfig_ReturnsError() { // Arrange (AppDeployment deployment, _) = CreateDeploymentWithCluster(); // Act KubernetesOperationResult result = await sut.ScaleDeploymentAsync( deployment.Id, "billing-api", 3); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("kubeconfig"); } }