Files
Entkube/tests/EntKube.Provisioning.Tests/Features/MinioTenantAdoptionTests.cs
2026-05-13 14:01:32 +02:00

257 lines
9.7 KiB
C#

using EntKube.Provisioning.Domain;
using EntKube.Provisioning.Features.MinioTenants;
using EntKube.Provisioning.Features.MinioTenants.AdoptMinioTenants;
using EntKube.Provisioning.Infrastructure;
using EntKube.SharedKernel.Domain;
using FluentAssertions;
using Moq;
namespace EntKube.Provisioning.Tests.Features;
/// <summary>
/// Tests for MinIO Tenant adoption — discovering existing MinIO Tenant CRDs on a
/// Kubernetes cluster and importing them into the platform's domain so they can be
/// viewed, managed, and monitored. This follows the same pattern as CNPG cluster
/// adoption: the Provisioning service scans the cluster, finds what's already running,
/// and creates domain aggregates for each tenant.
/// </summary>
public class MinioTenantAdoptionTests
{
private readonly InMemoryMinioTenantRepository tenantRepository;
private readonly Mock<IMinioTenantClient> tenantClient;
public MinioTenantAdoptionTests()
{
tenantRepository = new InMemoryMinioTenantRepository();
tenantClient = new Mock<IMinioTenantClient>();
}
// ─── Adopt Tenants ──────────────────────────────────────────────────────────
[Fact]
public async Task AdoptTenants_WithExistingTenants_ImportsThemAsRunning()
{
// Arrange — The Kubernetes cluster has two MinIO tenants already running:
// one for platform storage and one for application data. Neither of them
// exists in our repository yet, so adoption should discover and import both.
Guid clusterId = Guid.NewGuid();
tenantClient.Setup(c => c.DiscoverTenantsAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<DiscoveredMinioTenant>
{
new(
Name: "platform-storage",
Namespace: "minio-platform",
Pools: new List<DiscoveredMinioPool>
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
},
CurrentState: "Initialized",
Buckets: new List<string> { "cnpg-backups", "loki-logs" }),
new(
Name: "app-storage",
Namespace: "minio-apps",
Pools: new List<DiscoveredMinioPool>
{
new(Servers: 2, VolumesPerServer: 4, StorageSize: "50Gi", StorageClass: "standard")
},
CurrentState: "Initialized",
Buckets: new List<string> { "user-uploads" })
});
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
AdoptMinioTenantsRequest request = new(
ClusterId: clusterId,
KubeConfig: "kubeconfig-content",
ContextName: "prod-context");
// Act
Result<List<Guid>> result = await handler.HandleAsync(request);
// Assert — Both tenants adopted with correct details.
result.IsSuccess.Should().BeTrue();
result.Value.Should().HaveCount(2);
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
all.Should().HaveCount(2);
MinioTenant platformTenant = all.First(t => t.Name == "platform-storage");
platformTenant.ClusterId.Should().Be(clusterId);
platformTenant.Namespace.Should().Be("minio-platform");
platformTenant.Status.Should().Be(MinioTenantStatus.Running);
platformTenant.Pools.Should().HaveCount(1);
platformTenant.Pools[0].Servers.Should().Be(4);
platformTenant.Pools[0].StorageSize.Should().Be("100Gi");
MinioTenant appTenant = all.First(t => t.Name == "app-storage");
appTenant.Status.Should().Be(MinioTenantStatus.Running);
}
[Fact]
public async Task AdoptTenants_NoTenantsOnCluster_ReturnsEmptyList()
{
// Arrange — The MinIO operator is installed but no tenants are deployed yet.
tenantClient.Setup(c => c.DiscoverTenantsAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<DiscoveredMinioTenant>());
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
AdoptMinioTenantsRequest request = new(Guid.NewGuid(), "config", "ctx");
// Act
Result<List<Guid>> result = await handler.HandleAsync(request);
// Assert — Success with empty list, not a failure.
result.IsSuccess.Should().BeTrue();
result.Value.Should().BeEmpty();
}
[Fact]
public async Task AdoptTenants_AlreadyAdopted_DoesNotDuplicate()
{
// Arrange — One tenant was already adopted. Running adoption again should
// skip it and only import new tenants, not create duplicates.
Guid clusterId = Guid.NewGuid();
MinioTenant existing = MinioTenant.Adopt(
clusterId: clusterId,
name: "platform-storage",
ns: "minio-platform",
pools: new List<MinioTenantPool>
{
new(4, 4, "100Gi", "ceph-block")
});
await tenantRepository.AddAsync(existing);
tenantClient.Setup(c => c.DiscoverTenantsAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<DiscoveredMinioTenant>
{
new(
Name: "platform-storage",
Namespace: "minio-platform",
Pools: new List<DiscoveredMinioPool>
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
},
CurrentState: "Initialized",
Buckets: new List<string> { "cnpg-backups" }),
new(
Name: "new-tenant",
Namespace: "minio-new",
Pools: new List<DiscoveredMinioPool>
{
new(Servers: 2, VolumesPerServer: 2, StorageSize: "50Gi", StorageClass: "standard")
},
CurrentState: "Initialized",
Buckets: new List<string>())
});
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
AdoptMinioTenantsRequest request = new(clusterId, "config", "ctx");
// Act
Result<List<Guid>> result = await handler.HandleAsync(request);
// Assert — Only one new tenant adopted, the existing one was skipped.
result.IsSuccess.Should().BeTrue();
result.Value.Should().HaveCount(2);
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
all.Should().HaveCount(2);
}
[Fact]
public async Task AdoptTenants_DegradedTenant_MarkedDegraded()
{
// Arrange — A tenant with state "Decommissioning" or some non-healthy
// state should be imported but marked as Degraded, not Running.
tenantClient.Setup(c => c.DiscoverTenantsAsync(
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<DiscoveredMinioTenant>
{
new(
Name: "sick-tenant",
Namespace: "minio-sick",
Pools: new List<DiscoveredMinioPool>
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
},
CurrentState: "Decommissioning",
Buckets: new List<string>())
});
AdoptMinioTenantsHandler handler = new(tenantRepository, tenantClient.Object);
AdoptMinioTenantsRequest request = new(Guid.NewGuid(), "config", "ctx");
// Act
Result<List<Guid>> result = await handler.HandleAsync(request);
// Assert
result.IsSuccess.Should().BeTrue();
IReadOnlyList<MinioTenant> all = await tenantRepository.GetAllAsync();
all[0].Status.Should().Be(MinioTenantStatus.Degraded);
}
// ─── Domain: MinioTenant.Adopt ──────────────────────────────────────────────
[Fact]
public void Adopt_WithValidConfig_CreatesRunningTenant()
{
// Arrange — We discover a healthy tenant with known pools.
List<MinioTenantPool> pools = new()
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
};
// Act
MinioTenant tenant = MinioTenant.Adopt(
clusterId: Guid.NewGuid(),
name: "discovered-tenant",
ns: "minio-discovered",
pools: pools);
// Assert — Adopted tenants start as Running since they're already serving traffic.
tenant.Id.Should().NotBe(Guid.Empty);
tenant.Name.Should().Be("discovered-tenant");
tenant.Namespace.Should().Be("minio-discovered");
tenant.Status.Should().Be(MinioTenantStatus.Running);
tenant.Pools.Should().HaveCount(1);
tenant.Pools[0].Servers.Should().Be(4);
}
[Fact]
public void Adopt_WithEmptyName_Throws()
{
// A tenant with no name is not valid — discovery must provide a name.
Action act = () => MinioTenant.Adopt(
Guid.NewGuid(), "", "ns",
new List<MinioTenantPool> { new(1, 1, "10Gi", "standard") });
act.Should().Throw<ArgumentException>();
}
}