using EntKube.Provisioning.Domain; using EntKube.Provisioning.Features.MinioInstances; using EntKube.Provisioning.Features.MinioInstances.AdoptMinioInstance; using EntKube.Provisioning.Features.PostgresClusters; using EntKube.Provisioning.Features.PostgresClusters.CreatePostgresCluster; using EntKube.Provisioning.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Moq; namespace EntKube.Provisioning.Tests.Features; /// /// Tests for MinIO adoption and its integration with CNPG cluster provisioning. /// MinIO must be adopted before CNPG clusters can be created — the create flow /// resolves the MinIO details by reference (cluster ID) rather than raw values. /// public class MinioAdoptionTests { private readonly InMemoryMinioInstanceRepository minioRepository; private readonly InMemoryPostgresClusterRepository pgRepository; private readonly Mock minioClient; private readonly Mock cnpgClient; public MinioAdoptionTests() { minioRepository = new InMemoryMinioInstanceRepository(); pgRepository = new InMemoryPostgresClusterRepository(); minioClient = new Mock(); cnpgClient = new Mock(); } // ─── Adopt MinIO ────────────────────────────────────────────────────────── [Fact] public async Task AdoptMinio_WithDiscoveredTenant_CreatesRunningInstance() { // Arrange — The K8s cluster has an existing MinIO tenant running. minioClient.Setup(c => c.DiscoverMinioAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new DiscoveredMinioInstance( Name: "platform-minio", Namespace: "minio-system", Endpoint: "minio.minio-system.svc:9000", ConsoleEndpoint: "minio-console.minio-system.svc:9001", CredentialsSecret: "minio-root-credentials", TotalCapacity: "1Ti", UsedCapacity: "250Gi", Buckets: new List { "loki-logs", "velero-backups" })); AdoptMinioInstanceHandler handler = new(minioRepository, minioClient.Object); AdoptMinioInstanceRequest request = new( ClusterId: Guid.NewGuid(), KubeConfig: "config", ContextName: "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert — MinIO adopted and stored with all discovered details. result.IsSuccess.Should().BeTrue(); result.Value.Should().NotBe(Guid.Empty); IReadOnlyList all = await minioRepository.GetAllAsync(); all.Should().HaveCount(1); all[0].Name.Should().Be("platform-minio"); all[0].Endpoint.Should().Be("minio.minio-system.svc:9000"); all[0].Status.Should().Be(MinioInstanceStatus.Running); all[0].Buckets.Should().Contain("loki-logs"); all[0].Buckets.Should().Contain("velero-backups"); } [Fact] public async Task AdoptMinio_NoTenantFound_ReturnsFailure() { // Arrange — No MinIO tenant on this cluster. minioClient.Setup(c => c.DiscoverMinioAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((DiscoveredMinioInstance?)null); AdoptMinioInstanceHandler handler = new(minioRepository, minioClient.Object); AdoptMinioInstanceRequest request = new(Guid.NewGuid(), "config", "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("No MinIO"); } [Fact] public async Task AdoptMinio_AlreadyAdopted_ReturnsExistingId() { // Arrange — MinIO was already adopted for this cluster. Guid clusterId = Guid.NewGuid(); MinioInstance existing = MinioInstance.Adopt( clusterId, "platform-minio", "minio-system", "minio:9000", null, "secret", "500Gi", null, null); await minioRepository.AddAsync(existing); minioClient.Setup(c => c.DiscoverMinioAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new DiscoveredMinioInstance( "platform-minio", "minio-system", "minio:9000", null, "secret", "500Gi", null, null)); AdoptMinioInstanceHandler handler = new(minioRepository, minioClient.Object); AdoptMinioInstanceRequest request = new(clusterId, "config", "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert — Returns the existing ID without creating a duplicate. result.IsSuccess.Should().BeTrue(); result.Value.Should().Be(existing.Id); IReadOnlyList all = await minioRepository.GetAllAsync(); all.Should().HaveCount(1); } // ─── CNPG Create with MinIO Reference ───────────────────────────────────── [Fact] public async Task CreatePostgresCluster_WithMinioInstanceId_ResolvesMinioConfig() { // Arrange — An adopted MinIO instance exists. When creating a CNPG cluster, // we reference it by the cluster ID instead of providing raw details. Guid clusterId = Guid.NewGuid(); MinioInstance minio = MinioInstance.Adopt( clusterId, "platform-minio", "minio-system", "minio.minio-system.svc:9000", null, "minio-root-credentials", "1Ti", null, new List { "cnpg-backups" }); await minioRepository.AddAsync(minio); CreatePostgresClusterHandler handler = new(pgRepository, cnpgClient.Object, minioRepository); CreatePostgresClusterRequest request = new( EnvironmentId: Guid.NewGuid(), ClusterId: clusterId, Name: "prod-db", Namespace: "cnpg-system", PostgresVersion: "16.4", Instances: 3, StorageSize: "50Gi", KubeConfig: "config", ContextName: "ctx", MinioEndpoint: null, MinioBucket: "cnpg-backups", MinioCredentialsSecret: null); // Act Result result = await handler.HandleAsync(request); // Assert — MinIO details resolved from the adopted instance. result.IsSuccess.Should().BeTrue(); IReadOnlyList clusters = await pgRepository.GetAllAsync(); clusters.Should().HaveCount(1); clusters[0].BackupConfig!.MinioEndpoint.Should().Be("minio.minio-system.svc:9000"); clusters[0].BackupConfig.CredentialsSecret.Should().Be("minio-root-credentials"); clusters[0].BackupConfig.Bucket.Should().Be("cnpg-backups"); cnpgClient.Verify(c => c.CreateClusterAsync( "config", "ctx", It.Is(s => s.MinioEndpoint == "minio.minio-system.svc:9000" && s.MinioCredentialsSecret == "minio-root-credentials"), It.IsAny()), Times.Once); } [Fact] public async Task CreatePostgresCluster_NoMinioAdopted_CreatesWithoutBackup() { // Arrange — No MinIO adopted for this cluster, and no explicit endpoint provided. // The cluster should still be created, just without backup configuration. CreatePostgresClusterHandler handler = new(pgRepository, cnpgClient.Object, minioRepository); CreatePostgresClusterRequest request = new( EnvironmentId: Guid.NewGuid(), ClusterId: Guid.NewGuid(), Name: "prod-db", Namespace: "cnpg-system", PostgresVersion: "16.4", Instances: 3, StorageSize: "50Gi", KubeConfig: "config", ContextName: "ctx", MinioEndpoint: null, MinioBucket: "backups", MinioCredentialsSecret: null); // Act Result result = await handler.HandleAsync(request); // Assert — Cluster is created successfully without backup. result.IsSuccess.Should().BeTrue(); } [Fact] public async Task CreatePostgresCluster_WithExplicitMinioDetails_StillWorks() { // Arrange — Explicit MinIO details provided (bypass adoption lookup). CreatePostgresClusterHandler handler = new(pgRepository, cnpgClient.Object, minioRepository); CreatePostgresClusterRequest request = new( EnvironmentId: Guid.NewGuid(), ClusterId: Guid.NewGuid(), Name: "prod-db", Namespace: "cnpg-system", PostgresVersion: "16.4", Instances: 3, StorageSize: "50Gi", KubeConfig: "config", ContextName: "ctx", MinioEndpoint: "custom-minio:9000", MinioBucket: "custom-bucket", MinioCredentialsSecret: "custom-secret"); // Act Result result = await handler.HandleAsync(request); // Assert — Explicit values used directly. result.IsSuccess.Should().BeTrue(); IReadOnlyList clusters = await pgRepository.GetAllAsync(); clusters[0].BackupConfig!.MinioEndpoint.Should().Be("custom-minio:9000"); clusters[0].BackupConfig.Bucket.Should().Be("custom-bucket"); clusters[0].BackupConfig.CredentialsSecret.Should().Be("custom-secret"); } }