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); } }