using EntKube.Web.Data; using EntKube.Web.Services; using FluentAssertions; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Moq; namespace EntKube.Web.Tests; /// /// Tests for MongoService — the orchestration layer for managed MongoDB Community Operator clusters. /// Covers cluster creation, deletion, upgrade, backup, restore, and database management. /// /// The Kubernetes API interactions are mocked via IKubernetesClientFactory. /// Database state management and vault secret creation are tested against /// a real SQLite in-memory database to verify the full orchestration flow. /// public class MongoServiceTests : IDisposable { private static readonly byte[] TestRootKey = Convert.FromBase64String( "dGhpcyBpcyBhIDMyIGJ5dGUga2V5ISEhMTIzNDU2Nzg="); private readonly SqliteConnection connection; private readonly ApplicationDbContext db; private readonly TestDbContextFactory dbFactory; private readonly VaultService vaultService; private readonly Mock k8sFactory; private readonly MongoService sut; public MongoServiceTests() { 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(); VaultEncryptionService encryption = new(TestRootKey); vaultService = new VaultService(dbFactory, encryption); k8sFactory = new Mock(); sut = new MongoService(dbFactory, vaultService, k8sFactory.Object); } public void Dispose() { db.Dispose(); connection.Dispose(); } // ──────── Helpers ──────── private async Task<(Tenant tenant, KubernetesCluster cluster, StorageLink storageLink)> SeedTenantWithClusterAsync() { 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.Set().Add(env); KubernetesCluster cluster = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, EnvironmentId = env.Id, Name = "prod-cluster", ApiServerUrl = "https://k8s.example.com", Kubeconfig = "apiVersion: v1\nkind: Config\nclusters: []" }; db.KubernetesClusters.Add(cluster); // Install the MongoDB Community Operator on this cluster. ClusterComponent mongoOperator = new() { Id = Guid.NewGuid(), ClusterId = cluster.Id, Name = "mongodb-community-operator", ComponentType = "helm", Status = ComponentStatus.Installed, Namespace = "mongodb-operator" }; db.ClusterComponents.Add(mongoOperator); // Create a storage link for backups. StorageLink storageLink = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, EnvironmentId = env.Id, Provider = StorageProvider.CleuraS3, Name = "Backup Bucket", Endpoint = "https://s3-kna1.citycloud.com", BucketName = "mongo-backups", Region = "Kna1" }; db.StorageLinks.Add(storageLink); await db.SaveChangesAsync(); // Initialize vault and store S3 credentials. await vaultService.InitializeVaultAsync(tenant.Id); await vaultService.SetStorageLinkSecretAsync(tenant.Id, storageLink.Id, "ACCESS_KEY", "test-access-key"); await vaultService.SetStorageLinkSecretAsync(tenant.Id, storageLink.Id, "SECRET_KEY", "test-secret-key"); return (tenant, cluster, storageLink); } // ──────── CreateClusterAsync ──────── [Fact] public async Task CreateClusterAsync_ValidInput_CreatesDbRecordAndReturnsCluster() { // Arrange — a tenant with a cluster and MongoDB Community Operator installed. (Tenant tenant, KubernetesCluster cluster, StorageLink storageLink) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act — create a new managed MongoDB cluster. MongoCluster result = await sut.CreateClusterAsync( tenant.Id, cluster.Id, "my-mongo", "databases", 3, "10Gi", storageLink.Id, "0 2 * * *"); // Assert — the cluster record should exist in the database. result.Should().NotBeNull(); result.Name.Should().Be("my-mongo"); result.Namespace.Should().Be("databases"); result.Members.Should().Be(3); result.StorageSize.Should().Be("10Gi"); result.StorageLinkId.Should().Be(storageLink.Id); result.BackupSchedule.Should().Be("0 2 * * *"); result.Status.Should().Be(MongoClusterStatus.Creating); MongoCluster? persisted = await db.MongoClusters.FindAsync(result.Id); persisted.Should().NotBeNull(); } [Fact] public async Task CreateClusterAsync_WithoutBackup_CreatesClusterWithNoStorageLink() { // Arrange (Tenant tenant, KubernetesCluster cluster, _) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); // Act — create without backup configuration. MongoCluster result = await sut.CreateClusterAsync( tenant.Id, cluster.Id, "standalone-mongo", "default", 1, "5Gi", storageLinkId: null, backupSchedule: null); // Assert result.StorageLinkId.Should().BeNull(); result.BackupSchedule.Should().BeNull(); } [Fact] public async Task CreateClusterAsync_NoMongoOperator_ThrowsInvalidOperation() { // Arrange — tenant with a cluster but NO MongoDB Community Operator. Tenant tenant = new() { Id = Guid.NewGuid(), Name = "NoPercona", Slug = "no-percona" }; db.Tenants.Add(tenant); Data.Environment env = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Dev" }; db.Set().Add(env); KubernetesCluster cluster = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, EnvironmentId = env.Id, Name = "dev-cluster", ApiServerUrl = "https://k8s.dev.example.com", Kubeconfig = "fake" }; db.KubernetesClusters.Add(cluster); await db.SaveChangesAsync(); // Act & Assert Func act = () => sut.CreateClusterAsync( tenant.Id, cluster.Id, "mongo", "default", 1, "5Gi", null, null); await act.Should().ThrowAsync() .WithMessage("*MongoDB Community Operator*not installed*"); } [Fact] public async Task CreateClusterAsync_AppliesManifestToKubernetes() { // Arrange (Tenant tenant, KubernetesCluster cluster, StorageLink storageLink) = await SeedTenantWithClusterAsync(); List appliedManifests = []; string? appliedKubeconfig = null; k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Callback((manifest, kubeconfig, _) => { appliedManifests.Add(manifest); appliedKubeconfig = kubeconfig; }) .Returns(Task.CompletedTask); // Act await sut.CreateClusterAsync( tenant.Id, cluster.Id, "backup-mongo", "databases", 3, "20Gi", storageLink.Id, "0 2 * * *"); // Assert — verify the manifests contain expected Community Operator configuration. string allManifests = string.Join("\n", appliedManifests); allManifests.Should().Contain("kind: MongoDBCommunity"); allManifests.Should().Contain("mongodbcommunity.mongodb.com/v1"); allManifests.Should().Contain("name: backup-mongo"); allManifests.Should().Contain("namespace: databases"); appliedKubeconfig.Should().NotBeNull(); } // ──────── DeleteClusterAsync ──────── [Fact] public async Task DeleteClusterAsync_RemovesFromDbAndKubernetes() { // Arrange — create a cluster first. (Tenant tenant, KubernetesCluster cluster, StorageLink storageLink) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); k8sFactory.Setup(f => f.DeleteManifestAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); MongoCluster created = await sut.CreateClusterAsync( tenant.Id, cluster.Id, "delete-me", "databases", 1, "5Gi", null, null); // Act await sut.DeleteClusterAsync(tenant.Id, created.Id); // Assert — should be gone from the database. MongoCluster? deleted = await db.MongoClusters.FindAsync(created.Id); deleted.Should().BeNull(); } // ──────── GetClustersAsync ──────── [Fact] public async Task GetClustersAsync_ReturnsOnlyTenantClusters() { // Arrange (Tenant tenant, KubernetesCluster cluster, _) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); await sut.CreateClusterAsync(tenant.Id, cluster.Id, "mongo-1", "ns1", 3, "10Gi", null, null); await sut.CreateClusterAsync(tenant.Id, cluster.Id, "mongo-2", "ns2", 1, "5Gi", null, null); // Act List result = await sut.GetClustersAsync(tenant.Id); // Assert result.Should().HaveCount(2); result.Select(c => c.Name).Should().Contain("mongo-1").And.Contain("mongo-2"); } // ──────── CreateDatabaseAsync ──────── [Fact] public async Task CreateDatabaseAsync_CreatesDbRecordAndExecutesMongo() { // Arrange (Tenant tenant, KubernetesCluster cluster, _) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); k8sFactory.Setup(f => f.ExecuteMongoAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); k8sFactory.Setup(f => f.GetSecretValueAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync("test-admin-password"); MongoCluster mongoCluster = await sut.CreateClusterAsync( tenant.Id, cluster.Id, "db-test", "databases", 3, "10Gi", null, null); // Act MongoDatabase result = await sut.CreateDatabaseAsync(tenant.Id, mongoCluster.Id, "myapp"); // Assert result.Should().NotBeNull(); result.Name.Should().Be("myapp"); result.Status.Should().Be(MongoDatabaseStatus.Ready); k8sFactory.Verify(f => f.ExecuteMongoAsync( "db-test", "databases", It.Is(s => s.Contains("myapp")), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } // ──────── BackupAsync ──────── [Fact] public async Task BackupAsync_CreatesBackupRecordAndAppliesManifest() { // Arrange (Tenant tenant, KubernetesCluster cluster, StorageLink storageLink) = await SeedTenantWithClusterAsync(); k8sFactory.Setup(f => f.ApplyManifestAsync( It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask); MongoCluster mongoCluster = await sut.CreateClusterAsync( tenant.Id, cluster.Id, "backup-test", "databases", 3, "10Gi", storageLink.Id, "0 2 * * *"); // Act MongoBackup result = await sut.BackupAsync(tenant.Id, mongoCluster.Id); // Assert result.Should().NotBeNull(); result.Type.Should().Be(MongoBackupType.OnDemand); result.Status.Should().Be(MongoBackupStatus.Running); // Verify a backup Job manifest was applied. k8sFactory.Verify(f => f.ApplyManifestAsync( It.Is(m => m.Contains("kind: Job") && m.Contains("mongodump")), It.IsAny(), It.IsAny()), Times.AtLeastOnce); } }