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

308 lines
8.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using EntKube.Provisioning.Domain;
using FluentAssertions;
namespace EntKube.Provisioning.Tests.Domain;
/// <summary>
/// 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.
/// </summary>
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<MinioTenantPool> 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<MinioTenantPool> 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<MinioTenantPool> pools = new() { new MinioTenantPool(4, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "", "ns", pools);
// Assert
act.Should().Throw<ArgumentException>().WithParameterName("name");
}
[Fact]
public void Create_WithEmptyNamespace_ThrowsArgumentException()
{
// Arrange & Act
List<MinioTenantPool> pools = new() { new MinioTenantPool(4, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "", pools);
// Assert
act.Should().Throw<ArgumentException>().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<MinioTenantPool>());
// Assert
act.Should().Throw<ArgumentException>().WithParameterName("pools");
}
[Fact]
public void Create_WithZeroServers_ThrowsArgumentException()
{
// Arrange & Act — Each pool needs at least one server.
List<MinioTenantPool> pools = new() { new MinioTenantPool(0, 4, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "ns", pools);
// Assert
act.Should().Throw<ArgumentException>();
}
[Fact]
public void Create_WithZeroVolumes_ThrowsArgumentException()
{
// Arrange & Act — Each pool needs at least one volume per server.
List<MinioTenantPool> pools = new() { new MinioTenantPool(4, 0, "100Gi", "standard") };
Action act = () => MinioTenant.Create(Guid.NewGuid(), "tenant", "ns", pools);
// Assert
act.Should().Throw<ArgumentException>();
}
[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<MinioTenantPool> 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<MinioTenantPool>());
act.Should().Throw<ArgumentException>();
}
[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 <name>-hl.<namespace>.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<MinioTenantPool> pools = new()
{
new MinioTenantPool(Servers: 4, VolumesPerServer: 4, StorageSize: "100Gi", StorageClass: "ceph-block")
};
return MinioTenant.Create(Guid.NewGuid(), "platform-storage", "minio-tenant-1", pools);
}
}