using EntKube.Provisioning.Domain;
using EntKube.Provisioning.Features.MinioTenants;
using EntKube.Provisioning.Features.MinioTenants.CreateMinioTenant;
using EntKube.Provisioning.Features.MinioTenants.ConfigureMinioTenant;
using EntKube.Provisioning.Features.MinioTenants.DeleteMinioTenant;
using EntKube.Provisioning.Infrastructure;
using EntKube.SharedKernel.Domain;
using FluentAssertions;
using Moq;
namespace EntKube.Provisioning.Tests.Features;
///
/// Tests for MinIO Tenant provisioning — the full lifecycle of creating, configuring,
/// and deleting MinIO tenants on a Kubernetes cluster. Unlike MinIO adoption (which
/// discovers existing tenants), this provisions new tenants from scratch via the
/// MinIO Operator's Tenant CRD.
///
public class MinioTenantProvisioningTests
{
private readonly InMemoryMinioTenantRepository tenantRepository;
private readonly Mock tenantClient;
public MinioTenantProvisioningTests()
{
tenantRepository = new InMemoryMinioTenantRepository();
tenantClient = new Mock();
}
// ─── Create Tenant ──────────────────────────────────────────────────────────
[Fact]
public async Task CreateTenant_WithValidConfig_ProvisionsTenantOnCluster()
{
// Arrange — A platform admin wants a new MinIO tenant with 4 servers,
// each with 4 disks of 100Gi, using the ceph-block storage class.
tenantClient.Setup(c => c.ApplyTenantAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
CreateMinioTenantRequest request = new(
ClusterId: Guid.NewGuid(),
KubeConfig: "kubeconfig-content",
ContextName: "prod-context",
Name: "platform-storage",
Namespace: "minio-tenant-1",
Pools: new List
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
});
// Act
Result result = await handler.HandleAsync(request);
// Assert — Tenant created in domain and applied to K8s cluster.
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBe(Guid.Empty);
IReadOnlyList all = await tenantRepository.GetAllAsync();
all.Should().HaveCount(1);
all[0].Name.Should().Be("platform-storage");
all[0].Namespace.Should().Be("minio-tenant-1");
all[0].Status.Should().Be(MinioTenantStatus.Provisioning);
all[0].Pools.Should().HaveCount(1);
all[0].Pools[0].Servers.Should().Be(4);
all[0].Pools[0].VolumesPerServer.Should().Be(4);
// Verify the K8s client was called to apply the Tenant CRD.
tenantClient.Verify(c => c.ApplyTenantAsync(
"kubeconfig-content", "prod-context",
It.Is(t => t.Name == "platform-storage"),
It.IsAny()), Times.Once);
}
[Fact]
public async Task CreateTenant_WithMultiplePools_AllPoolsApplied()
{
// Arrange — Heterogeneous pools: fast NVMe for hot data, HDD for warm.
tenantClient.Setup(c => c.ApplyTenantAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
CreateMinioTenantRequest request = new(
ClusterId: Guid.NewGuid(),
KubeConfig: "config",
ContextName: "ctx",
Name: "multi-tier",
Namespace: "minio-multi",
Pools: new List
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "nvme-fast"),
new(Servers: 2, VolumesPerServer: 8, StorageSize: "500Gi", StorageClass: "hdd-bulk")
});
// Act
Result result = await handler.HandleAsync(request);
// Assert
result.IsSuccess.Should().BeTrue();
IReadOnlyList all = await tenantRepository.GetAllAsync();
all[0].Pools.Should().HaveCount(2);
}
[Fact]
public async Task CreateTenant_WithInvalidConfig_ReturnsFailure()
{
// Arrange — Zero servers is not valid.
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
CreateMinioTenantRequest request = new(
ClusterId: Guid.NewGuid(),
KubeConfig: "config",
ContextName: "ctx",
Name: "bad-tenant",
Namespace: "minio-bad",
Pools: new List
{
new(Servers: 0, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
});
// Act
Result result = await handler.HandleAsync(request);
// Assert — Domain validation catches the bad config.
result.IsFailure.Should().BeTrue();
result.Error.Should().Contain("server");
}
[Fact]
public async Task CreateTenant_KubernetesClientFails_ReturnsFailure()
{
// Arrange — Network issue or RBAC problem prevents applying the CRD.
tenantClient.Setup(c => c.ApplyTenantAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.ThrowsAsync(new InvalidOperationException("Forbidden: RBAC denied"));
CreateMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
CreateMinioTenantRequest request = new(
ClusterId: Guid.NewGuid(),
KubeConfig: "config",
ContextName: "ctx",
Name: "tenant",
Namespace: "minio-ns",
Pools: new List
{
new(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "standard")
});
// Act
Result result = await handler.HandleAsync(request);
// Assert — Failure reported without tenant being persisted.
result.IsFailure.Should().BeTrue();
result.Error.Should().Contain("RBAC");
IReadOnlyList all = await tenantRepository.GetAllAsync();
all.Should().BeEmpty();
}
// ─── Configure Tenant ──────────────────────────────────────────────────────
[Fact]
public async Task ConfigureTenant_UpdatesPools_AndReapplies()
{
// Arrange — Existing tenant needs scale-out: from 4 to 8 servers.
MinioTenant existing = MinioTenant.Create(
Guid.NewGuid(), "platform-storage", "minio-tenant-1",
new List { new(4, 4, "100Gi", "ceph-block") });
existing.MarkRunning();
await tenantRepository.AddAsync(existing);
tenantClient.Setup(c => c.ApplyTenantAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
ConfigureMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
ConfigureMinioTenantRequest request = new(
TenantId: existing.Id,
KubeConfig: "config",
ContextName: "ctx",
Pools: new List
{
new(Servers: 8, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
});
// Act
Result result = await handler.HandleAsync(request);
// Assert — Pools updated, status back to Provisioning, K8s re-applied.
result.IsSuccess.Should().BeTrue();
MinioTenant? updated = await tenantRepository.GetByIdAsync(existing.Id);
updated.Should().NotBeNull();
updated!.Pools[0].Servers.Should().Be(8);
updated.Status.Should().Be(MinioTenantStatus.Provisioning);
tenantClient.Verify(c => c.ApplyTenantAsync(
"config", "ctx",
It.Is(t => t.Pools[0].Servers == 8),
It.IsAny()), Times.Once);
}
[Fact]
public async Task ConfigureTenant_NotFound_ReturnsFailure()
{
// Arrange
ConfigureMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
ConfigureMinioTenantRequest request = new(
TenantId: Guid.NewGuid(),
KubeConfig: "config",
ContextName: "ctx",
Pools: new List { new(4, 4, "100Gi", "standard") });
// Act
Result result = await handler.HandleAsync(request);
// Assert
result.IsFailure.Should().BeTrue();
result.Error.Should().Contain("not found");
}
// ─── Delete Tenant ──────────────────────────────────────────────────────────
[Fact]
public async Task DeleteTenant_RemovesFromClusterAndMarksDecommissioned()
{
// Arrange — Platform admin tears down a tenant.
MinioTenant existing = MinioTenant.Create(
Guid.NewGuid(), "old-tenant", "minio-old",
new List { new(4, 4, "100Gi", "standard") });
existing.MarkRunning();
await tenantRepository.AddAsync(existing);
tenantClient.Setup(c => c.DeleteTenantAsync(
It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(Task.CompletedTask);
DeleteMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
DeleteMinioTenantRequest request = new(
TenantId: existing.Id,
KubeConfig: "config",
ContextName: "ctx");
// Act
Result result = await handler.HandleAsync(request);
// Assert — Marked decommissioned and deleted from K8s.
result.IsSuccess.Should().BeTrue();
MinioTenant? tenant = await tenantRepository.GetByIdAsync(existing.Id);
tenant.Should().NotBeNull();
tenant!.Status.Should().Be(MinioTenantStatus.Decommissioned);
tenantClient.Verify(c => c.DeleteTenantAsync(
"config", "ctx", "old-tenant", "minio-old",
It.IsAny()), Times.Once);
}
[Fact]
public async Task DeleteTenant_NotFound_ReturnsFailure()
{
// Arrange
DeleteMinioTenantHandler handler = new(tenantRepository, tenantClient.Object);
DeleteMinioTenantRequest request = new(Guid.NewGuid(), "config", "ctx");
// Act
Result result = await handler.HandleAsync(request);
// Assert
result.IsFailure.Should().BeTrue();
result.Error.Should().Contain("not found");
}
}