using EntKube.Provisioning.Domain; using EntKube.Provisioning.Features.RedisClusters; using EntKube.Provisioning.Features.RedisClusters.AdoptRedisCluster; using EntKube.Provisioning.Features.RedisClusters.CreateRedisCluster; using EntKube.Provisioning.Features.RedisClusters.UpgradeRedisCluster; using EntKube.Provisioning.Features.RedisClusters.ConfigureRedisCluster; using EntKube.Provisioning.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using Moq; namespace EntKube.Provisioning.Tests.Features; /// /// Tests for all Redis cluster management features. Each test verifies that /// the handler correctly orchestrates domain logic + K8s client interactions. /// public class RedisClusterFeatureTests { private readonly InMemoryRedisClusterRepository repository; private readonly Mock redisClient; public RedisClusterFeatureTests() { repository = new InMemoryRedisClusterRepository(); redisClient = new Mock(); } // ─── Create Cluster ─────────────────────────────────────────────────────── [Fact] public async Task CreateCluster_WithValidRequest_CreatesAndCallsK8sClient() { // Arrange — A platform admin wants a new 3-replica Redis instance // with sentinel enabled for HA. CreateRedisClusterHandler handler = new(repository, redisClient.Object); CreateRedisClusterRequest request = new( EnvironmentId: Guid.NewGuid(), ClusterId: Guid.NewGuid(), Name: "platform-cache", Namespace: "redis", RedisVersion: "7.2.5", Replicas: 3, StorageSize: "10Gi", SentinelEnabled: true, KubeConfig: "apiVersion: v1\nkind: Config", ContextName: "prod-ctx"); // Act Result result = await handler.HandleAsync(request); // Assert — Cluster created in repository and K8s client called. result.IsSuccess.Should().BeTrue(); result.Value.Should().NotBe(Guid.Empty); IReadOnlyList clusters = await repository.GetAllAsync(); clusters.Should().HaveCount(1); clusters[0].Name.Should().Be("platform-cache"); clusters[0].Status.Should().Be(RedisClusterStatus.Provisioning); redisClient.Verify(c => c.CreateClusterAsync( "apiVersion: v1\nkind: Config", "prod-ctx", It.Is(s => s.Name == "platform-cache" && s.RedisVersion == "7.2.5" && s.Replicas == 3 && s.SentinelEnabled == true), It.IsAny()), Times.Once); } [Fact] public async Task CreateCluster_WithMissingName_ReturnsFailure() { // Arrange CreateRedisClusterHandler handler = new(repository, redisClient.Object); CreateRedisClusterRequest request = new( EnvironmentId: Guid.NewGuid(), ClusterId: Guid.NewGuid(), Name: "", Namespace: "redis", RedisVersion: "7.2.5", Replicas: 3, StorageSize: "10Gi", SentinelEnabled: true, KubeConfig: "config", ContextName: "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert — Domain validation rejects empty names. result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("name"); } // ─── Adopt Clusters ─────────────────────────────────────────────────────── [Fact] public async Task AdoptClusters_DiscoversTwoInstances_CreatesBothInRunningState() { // Arrange — The K8s cluster has two existing Redis instances. redisClient.Setup(c => c.DiscoverClustersAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { new("session-cache", "redis", "7.2.5", 3, "10Gi", true), new("rate-limiter", "redis", "7.0.15", 1, "5Gi", false) }); AdoptRedisClustersHandler handler = new(repository, redisClient.Object); AdoptRedisClustersRequest request = new(Guid.NewGuid(), Guid.NewGuid(), "config", "ctx"); // Act Result> result = await handler.HandleAsync(request); // Assert — Both clusters adopted. result.IsSuccess.Should().BeTrue(); result.Value.Should().HaveCount(2); IReadOnlyList clusters = await repository.GetAllAsync(); clusters.Should().HaveCount(2); clusters.Should().OnlyContain(c => c.Status == RedisClusterStatus.Running); } [Fact] public async Task AdoptClusters_AlreadyAdopted_ReturnsExistingIdWithoutDuplicate() { // Arrange — One instance already adopted. Guid clusterId = Guid.NewGuid(); RedisCluster existing = RedisCluster.Adopt( Guid.NewGuid(), clusterId, "session-cache", "redis", "7.2.5", 3, "10Gi", true); await repository.AddAsync(existing); redisClient.Setup(c => c.DiscoverClustersAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { new("session-cache", "redis", "7.2.5", 3, "10Gi", true) }); AdoptRedisClustersHandler handler = new(repository, redisClient.Object); AdoptRedisClustersRequest request = new(Guid.NewGuid(), clusterId, "config", "ctx"); // Act Result> result = await handler.HandleAsync(request); // Assert — Returns existing ID without creating a duplicate. result.IsSuccess.Should().BeTrue(); result.Value.Should().HaveCount(1); result.Value![0].Should().Be(existing.Id); IReadOnlyList clusters = await repository.GetAllAsync(); clusters.Should().HaveCount(1); } // ─── Upgrade ────────────────────────────────────────────────────────────── [Fact] public async Task UpgradeCluster_WithValidVersion_TransitionsToUpgrading() { // Arrange — A running Redis instance at version 7.0.15. RedisCluster redisCluster = RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "redis", "7.0.15", 3, "10Gi", true); redisCluster.MarkRunning(); await repository.AddAsync(redisCluster); redisClient.Setup(c => c.GetAvailableVersionsAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { "7.0.15", "7.2.5" }); UpgradeRedisClusterHandler handler = new(repository, redisClient.Object); UpgradeRedisClusterRequest request = new(redisCluster.Id, "7.2.5", "config", "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert result.IsSuccess.Should().BeTrue(); redisCluster.Status.Should().Be(RedisClusterStatus.Upgrading); redisCluster.TargetVersion.Should().Be("7.2.5"); redisClient.Verify(c => c.UpgradeClusterAsync( "config", "ctx", "cache", "redis", "7.2.5", It.IsAny()), Times.Once); } [Fact] public async Task UpgradeCluster_UnavailableVersion_ReturnsFailure() { // Arrange — Target version not in the available list. RedisCluster redisCluster = RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "redis", "7.0.15", 3, "10Gi", true); redisCluster.MarkRunning(); await repository.AddAsync(redisCluster); redisClient.Setup(c => c.GetAvailableVersionsAsync( It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { "7.0.15" }); UpgradeRedisClusterHandler handler = new(repository, redisClient.Object); UpgradeRedisClusterRequest request = new(redisCluster.Id, "7.2.5", "config", "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not available"); } // ─── Configure ──────────────────────────────────────────────────────────── [Fact] public async Task ConfigureCluster_ScaleReplicas_UpdatesAndCallsK8s() { // Arrange — A running 3-replica cluster to be scaled to 5. RedisCluster redisCluster = RedisCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "cache", "redis", "7.2.5", 3, "10Gi", true); redisCluster.MarkRunning(); await repository.AddAsync(redisCluster); ConfigureRedisClusterHandler handler = new(repository, redisClient.Object); ConfigureRedisClusterRequest request = new( redisCluster.Id, Replicas: 5, StorageSize: null, SentinelEnabled: null, KubeConfig: "config", ContextName: "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert result.IsSuccess.Should().BeTrue(); redisCluster.Replicas.Should().Be(5); redisClient.Verify(c => c.UpdateClusterAsync( "config", "ctx", It.Is(s => s.Replicas == 5), It.IsAny()), Times.Once); } [Fact] public async Task ConfigureCluster_NonExistent_ReturnsFailure() { // Arrange — No cluster with this ID. ConfigureRedisClusterHandler handler = new(repository, redisClient.Object); ConfigureRedisClusterRequest request = new( Guid.NewGuid(), Replicas: 5, StorageSize: null, SentinelEnabled: null, KubeConfig: "config", ContextName: "ctx"); // Act Result result = await handler.HandleAsync(request); // Assert result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("not found"); } }