using EntKube.Provisioning.Domain;
using FluentAssertions;
namespace EntKube.Provisioning.Tests.Domain;
///
/// Tests for the MinioTenant aggregate. A MinIO Tenant represents an actual storage
/// cluster managed by the MinIO Operator on Kubernetes. Each tenant has one or more
/// pools, each pool defining how many servers and disks to use.
///
/// One MinIO Operator installation can host multiple tenants — each tenant is a
/// separate storage cluster with its own credentials, namespace, and capacity.
///
public class MinioTenantTests
{
[Fact]
public void Create_WithValidInputs_CreatesTenantInProvisioningState()
{
// Arrange — A platform admin wants to provision a new MinIO storage tenant
// on a cluster. They specify how much capacity they need: 4 servers, each
// with 4 disks of 100Gi.
Guid clusterId = Guid.NewGuid();
List pools = new()
{
new MinioTenantPool(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
};
// Act
MinioTenant tenant = MinioTenant.Create(
clusterId: clusterId,
name: "platform-storage",
ns: "minio-tenant-1",
pools: pools);
// Assert — New tenants start in Provisioning state until the operator confirms.
tenant.Id.Should().NotBe(Guid.Empty);
tenant.ClusterId.Should().Be(clusterId);
tenant.Name.Should().Be("platform-storage");
tenant.Namespace.Should().Be("minio-tenant-1");
tenant.Status.Should().Be(MinioTenantStatus.Provisioning);
tenant.Pools.Should().HaveCount(1);
tenant.Pools[0].Servers.Should().Be(4);
tenant.Pools[0].VolumesPerServer.Should().Be(4);
tenant.Pools[0].StorageSize.Should().Be("100Gi");
tenant.Pools[0].StorageClass.Should().Be("ceph-block");
tenant.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
}
[Fact]
public void Create_WithMultiplePools_TracksAllPools()
{
// Arrange — Some deployments need heterogeneous pools:
// one for hot data (fast NVMe) and one for warm data (spinning disk).
List pools = new()
{
new MinioTenantPool(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "nvme-fast"),
new MinioTenantPool(Servers: 2, VolumesPerServer: 8, StorageSize: "500Gi", StorageClass: "hdd-bulk")
};
// Act
MinioTenant tenant = MinioTenant.Create(Guid.NewGuid(), "multi-pool", "minio-mp", pools);
// Assert
tenant.Pools.Should().HaveCount(2);
tenant.Pools[0].StorageClass.Should().Be("nvme-fast");
tenant.Pools[1].StorageClass.Should().Be("hdd-bulk");
}
[Fact]
public void Create_WithEmptyName_ThrowsArgumentException()
{
// Arrange & Act
List pools = new() { new MinioTenantPool(4, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "", "ns", pools);
// Assert
act.Should().Throw().WithParameterName("name");
}
[Fact]
public void Create_WithEmptyNamespace_ThrowsArgumentException()
{
// Arrange & Act
List pools = new() { new MinioTenantPool(4, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "", pools);
// Assert
act.Should().Throw().WithParameterName("ns");
}
[Fact]
public void Create_WithNoPools_ThrowsArgumentException()
{
// Arrange & Act — A tenant must have at least one pool to be meaningful.
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "ns", new List());
// Assert
act.Should().Throw().WithParameterName("pools");
}
[Fact]
public void Create_WithZeroServers_ThrowsArgumentException()
{
// Arrange & Act — Each pool needs at least one server.
List pools = new() { new MinioTenantPool(0, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "ns", pools);
// Assert
act.Should().Throw();
}
[Fact]
public void Create_WithZeroVolumes_ThrowsArgumentException()
{
// Arrange & Act — Each pool needs at least one volume per server.
List pools = new() { new MinioTenantPool(4, 0, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "ns", pools);
// Assert
act.Should().Throw();
}
[Fact]
public void MarkRunning_TransitionsFromProvisioning()
{
// Arrange — After the operator creates the tenant, the reconciler confirms it's running.
MinioTenant tenant = CreateTestTenant();
// Act
tenant.MarkRunning();
// Assert
tenant.Status.Should().Be(MinioTenantStatus.Running);
tenant.StatusMessage.Should().BeNull();
}
[Fact]
public void MarkDegraded_SetsStatusAndReason()
{
// Arrange
MinioTenant tenant = CreateTestTenant();
tenant.MarkRunning();
// Act
tenant.MarkDegraded("1 drive offline");
// Assert
tenant.Status.Should().Be(MinioTenantStatus.Degraded);
tenant.StatusMessage.Should().Be("1 drive offline");
}
[Fact]
public void UpdatePools_ReplacesPoolConfiguration()
{
// Arrange — The admin wants to scale out: add more servers to the pool.
MinioTenant tenant = CreateTestTenant();
tenant.MarkRunning();
List newPools = new()
{
new MinioTenantPool(Servers: 8, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
};
// Act
tenant.UpdatePools(newPools);
// Assert — Status goes back to Provisioning while the operator reconciles.
tenant.Pools.Should().HaveCount(1);
tenant.Pools[0].Servers.Should().Be(8);
tenant.Status.Should().Be(MinioTenantStatus.Provisioning);
}
[Fact]
public void UpdatePools_WithEmptyPools_ThrowsArgumentException()
{
// Arrange
MinioTenant tenant = CreateTestTenant();
tenant.MarkRunning();
// Act & Assert
Action act = () => tenant.UpdatePools(new List());
act.Should().Throw();
}
[Fact]
public void Decommission_MarksAsDecommissioned()
{
// Arrange — The admin tears down a tenant. The operator will delete resources.
MinioTenant tenant = CreateTestTenant();
tenant.MarkRunning();
// Act
tenant.Decommission();
// Assert
tenant.Status.Should().Be(MinioTenantStatus.Decommissioned);
}
[Fact]
public void GetEndpoint_ReturnsConventionalServiceUrl()
{
// Arrange — MinIO tenants expose storage at -hl..svc:9000
MinioTenant tenant = CreateTestTenant();
// Act
string endpoint = tenant.GetEndpoint();
// Assert
endpoint.Should().Be("platform-storage-hl.minio-tenant-1.svc:9000");
}
[Fact]
public void GetConsoleEndpoint_ReturnsConventionalConsoleUrl()
{
// Arrange
MinioTenant tenant = CreateTestTenant();
// Act
string consoleEndpoint = tenant.GetConsoleEndpoint();
// Assert
consoleEndpoint.Should().Be("platform-storage-console.minio-tenant-1.svc:9443");
}
[Fact]
public void GetCredentialsSecretName_ReturnsConventionalSecretName()
{
// Arrange
MinioTenant tenant = CreateTestTenant();
// Act
string secret = tenant.GetCredentialsSecretName();
// Assert
secret.Should().Be("platform-storage-secret");
}
[Fact]
public void TotalCapacity_CalculatesFromPoolsAsString()
{
// Arrange — Capacity = servers * volumesPerServer * storageSize (per pool).
// With 4 servers × 4 volumes × 100Gi = 1600Gi total.
MinioTenant tenant = CreateTestTenant();
// Act
string capacity = tenant.CalculateTotalCapacity();
// Assert — 4 × 4 × 100 = 1600Gi
capacity.Should().Be("1600Gi");
}
private static MinioTenant CreateTestTenant()
{
List pools = new()
{
new MinioTenantPool(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
};
return MinioTenant.Create(Guid.NewGuid(), "platform-storage", "minio-tenant-1", pools);
}
}