using EntKube.SharedKernel.Domain; using FluentAssertions; namespace EntKube.Provisioning.Tests.Domain; /// /// Tests for the RedisCluster aggregate root. This rich domain model tracks /// everything about a Redis instance managed by the OT-OPERATORS Redis Operator: /// replica count, storage, sentinel mode, persistence, and version lifecycle. /// /// The OT-OPERATORS Redis Operator manages Redis, RedisCluster, RedisReplication, /// and RedisSentinel CRDs on Kubernetes. Each Redis instance can be standalone, /// replicated, or run with Sentinel for HA. /// public class RedisClusterTests { // ─── Creation ────────────────────────────────────────────────────────────── [Fact] public void Create_WithValidInputs_ReturnsClusterInProvisioningState() { // Arrange & Act — A platform admin provisions a new Redis instance // for shared caching on a specific Kubernetes cluster. Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Create( environmentId: environmentId, clusterId: clusterId, name: "platform-redis", ns: "redis", redisVersion: "7.2.5", replicas: 3, storageSize: "10Gi", sentinelEnabled: true); // Assert — The cluster starts in Provisioning state, waiting for the // reconciler to create the Redis CR. redisCluster.Id.Should().NotBe(Guid.Empty); redisCluster.EnvironmentId.Should().Be(environmentId); redisCluster.ClusterId.Should().Be(clusterId); redisCluster.Name.Should().Be("platform-redis"); redisCluster.Namespace.Should().Be("redis"); redisCluster.RedisVersion.Should().Be("7.2.5"); redisCluster.Replicas.Should().Be(3); redisCluster.StorageSize.Should().Be("10Gi"); redisCluster.SentinelEnabled.Should().BeTrue(); redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Provisioning); } [Fact] public void Create_WithEmptyName_ThrowsArgumentException() { // Arrange & Act — The domain rejects invalid cluster names. Action act = () => Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "", "ns", "7.2.5", 3, "10Gi", true); // Assert act.Should().Throw().WithParameterName("name"); } [Fact] public void Create_WithZeroReplicas_ThrowsArgumentException() { // Arrange & Act — Must have at least 1 replica. Action act = () => Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "ns", "7.2.5", 0, "10Gi", true); // Assert act.Should().Throw().WithParameterName("replicas"); } [Fact] public void Create_WithEmptyNamespace_ThrowsArgumentException() { // Arrange & Act Action act = () => Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "", "7.2.5", 3, "10Gi", true); // Assert act.Should().Throw().WithParameterName("ns"); } // ─── Adoption ────────────────────────────────────────────────────────────── [Fact] public void Adopt_FromExistingInstance_CreatesRunningCluster() { // Arrange & Act — When we adopt an existing Redis instance that's // already running on the K8s cluster, we create it in Running state. Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Adopt( environmentId: Guid.NewGuid(), clusterId: Guid.NewGuid(), name: "existing-redis", ns: "redis", redisVersion: "7.0.15", replicas: 3, storageSize: "5Gi", sentinelEnabled: true); // Assert — Adopted clusters start in Running state. redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Running); redisCluster.Name.Should().Be("existing-redis"); redisCluster.LastReconcileAt.Should().NotBeNull(); } // ─── Status transitions ──────────────────────────────────────────────────── [Fact] public void MarkRunning_TransitionsFromProvisioning() { // Arrange — A freshly created cluster in Provisioning state. Provisioning.Domain.RedisCluster redisCluster = CreateTestCluster(); // Act — The reconciler confirmed the Redis instance is ready. redisCluster.MarkRunning(); // Assert redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Running); redisCluster.LastReconcileAt.Should().NotBeNull(); } [Fact] public void MarkDegraded_SetsStatusToDegraded() { // Arrange Provisioning.Domain.RedisCluster redisCluster = CreateTestCluster(); redisCluster.MarkRunning(); // Act — Reconciler detected an issue. redisCluster.MarkDegraded("Replica pod not ready"); // Assert redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Degraded); redisCluster.StatusMessage.Should().Be("Replica pod not ready"); } // ─── Version upgrades ────────────────────────────────────────────────────── [Fact] public void RequestUpgrade_WhenRunning_SetsUpgradingStatusAndTargetVersion() { // Arrange — A running cluster at version 7.0.15. Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "ns", "7.0.15", 3, "10Gi", true); redisCluster.MarkRunning(); // Act — Platform admin requests upgrade to 7.2.5. Result result = redisCluster.RequestUpgrade("7.2.5"); // Assert — The cluster transitions to Upgrading state. result.IsSuccess.Should().BeTrue(); redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Upgrading); redisCluster.TargetVersion.Should().Be("7.2.5"); } [Fact] public void RequestUpgrade_WhenNotRunning_ReturnsFailure() { // Arrange — A cluster still provisioning can't be upgraded. Provisioning.Domain.RedisCluster redisCluster = CreateTestCluster(); // Act Result result = redisCluster.RequestUpgrade("7.2.5"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("running"); } [Fact] public void RequestUpgrade_ToSameVersion_ReturnsFailure() { // Arrange — A cluster already at version 7.2.5. Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "ns", "7.2.5", 3, "10Gi", true); redisCluster.MarkRunning(); // Act — Try to upgrade to the same version. Result result = redisCluster.RequestUpgrade("7.2.5"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("same version"); } [Fact] public void CompleteUpgrade_TransitionsToRunningWithNewVersion() { // Arrange — A cluster mid-upgrade. Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "ns", "7.0.15", 3, "10Gi", true); redisCluster.MarkRunning(); redisCluster.RequestUpgrade("7.2.5"); // Act — Reconciler confirmed upgrade completed. redisCluster.CompleteUpgrade(); // Assert — Version updated, status back to Running. redisCluster.RedisVersion.Should().Be("7.2.5"); redisCluster.Status.Should().Be(Provisioning.Domain.RedisClusterStatus.Running); redisCluster.TargetVersion.Should().BeNull(); } // ─── Scaling ─────────────────────────────────────────────────────────────── [Fact] public void ScaleReplicas_UpdatesReplicaCount() { // Arrange — A running 3-replica cluster. Provisioning.Domain.RedisCluster redisCluster = CreateTestCluster(); redisCluster.MarkRunning(); // Act — Scale up to 5 replicas for more read capacity. Result result = redisCluster.ScaleReplicas(5); // Assert result.IsSuccess.Should().BeTrue(); redisCluster.Replicas.Should().Be(5); } [Fact] public void ScaleReplicas_ToZero_ReturnsFailure() { // Arrange Provisioning.Domain.RedisCluster redisCluster = CreateTestCluster(); redisCluster.MarkRunning(); // Act Result result = redisCluster.ScaleReplicas(0); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("at least 1"); } // ─── Configuration ───────────────────────────────────────────────────────── [Fact] public void ConfigureSentinel_EnablesOrDisablesSentinel() { // Arrange — A cluster without sentinel. Provisioning.Domain.RedisCluster redisCluster = Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "ns", "7.2.5", 3, "10Gi", false); redisCluster.MarkRunning(); // Act — Enable sentinel for HA. redisCluster.ConfigureSentinel(true); // Assert redisCluster.SentinelEnabled.Should().BeTrue(); } // ─── Helper ──────────────────────────────────────────────────────────────── private static Provisioning.Domain.RedisCluster CreateTestCluster() { return Provisioning.Domain.RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "test-redis", "redis", "7.2.5", 3, "10Gi", true); } }