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

438 lines
15 KiB
C#

using EntKube.SharedKernel.Domain;
using FluentAssertions;
namespace EntKube.Provisioning.Tests.Domain;
/// <summary>
/// Tests for the PostgresCluster aggregate root. This rich domain model tracks
/// everything about a CloudNativePG PostgreSQL cluster: instances, storage,
/// WAL archiving, backup configuration, databases, and version lifecycle.
/// </summary>
public class PostgresClusterTests
{
[Fact]
public void Create_WithValidInputs_ReturnsClusterInProvisioningState()
{
// Arrange & Act — A platform admin provisions a new PostgreSQL cluster
// for shared use on a specific Kubernetes cluster.
Guid environmentId = Guid.NewGuid();
Guid clusterId = Guid.NewGuid();
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
environmentId: environmentId,
clusterId: clusterId,
name: "platform-db",
ns: "cnpg-system",
postgresVersion: "16.4",
instances: 3,
storageSize: "50Gi",
minioEndpoint: "minio.minio-system.svc:9000",
minioBucket: "cnpg-backups",
minioCredentialsSecret: "minio-cnpg-credentials");
// Assert — The cluster starts in Provisioning state, waiting for the
// reconciler to actually create the CNPG Cluster resource.
pgCluster.Id.Should().NotBe(Guid.Empty);
pgCluster.EnvironmentId.Should().Be(environmentId);
pgCluster.ClusterId.Should().Be(clusterId);
pgCluster.Name.Should().Be("platform-db");
pgCluster.Namespace.Should().Be("cnpg-system");
pgCluster.PostgresVersion.Should().Be("16.4");
pgCluster.Instances.Should().Be(3);
pgCluster.StorageSize.Should().Be("50Gi");
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Provisioning);
pgCluster.Databases.Should().BeEmpty();
}
[Fact]
public void Create_WithMinIOBackupConfig_SetsWalArchiving()
{
// Arrange & Act — WAL archiving to MinIO is configured at creation time
// since it's a hard dependency. The cluster won't be created without it.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
environmentId: Guid.NewGuid(),
clusterId: Guid.NewGuid(),
name: "prod-db",
ns: "cnpg-prod",
postgresVersion: "16.4",
instances: 3,
storageSize: "100Gi",
minioEndpoint: "minio.minio-system.svc:9000",
minioBucket: "pg-wal-archive",
minioCredentialsSecret: "minio-pg-creds");
// Assert — Backup configuration is stored as part of the aggregate.
pgCluster.BackupConfig.Should().NotBeNull();
pgCluster.BackupConfig!.MinioEndpoint.Should().Be("minio.minio-system.svc:9000");
pgCluster.BackupConfig.Bucket.Should().Be("pg-wal-archive");
pgCluster.BackupConfig.CredentialsSecret.Should().Be("minio-pg-creds");
pgCluster.BackupConfig.WalArchivingEnabled.Should().BeTrue();
}
[Fact]
public void Create_WithEmptyName_ThrowsArgumentException()
{
// Arrange & Act — The domain rejects invalid cluster names.
Action act = () => Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "", "ns", "16.4", 3, "50Gi",
"minio:9000", "bucket", "secret");
// Assert
act.Should().Throw<ArgumentException>().WithParameterName("name");
}
[Fact]
public void Create_WithZeroInstances_ThrowsArgumentException()
{
// Arrange & Act — Must have at least 1 instance.
Action act = () => Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "16.4", 0, "50Gi",
"minio:9000", "bucket", "secret");
// Assert
act.Should().Throw<ArgumentException>().WithParameterName("instances");
}
[Fact]
public void Create_WithEmptyMinioEndpoint_CreatesClusterWithoutBackup()
{
// Arrange & Act — Backup is optional. When MinIO endpoint is empty,
// the cluster should be created successfully without backup config.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "16.4", 3, "50Gi",
"", "bucket", "secret");
// Assert — cluster exists but has no backup configuration.
pgCluster.Should().NotBeNull();
pgCluster.BackupConfig.Should().BeNull();
}
[Fact]
public void Adopt_FromExistingCluster_CreatesRunningClusterWithDiscoveredConfig()
{
// Arrange & Act — When we adopt an existing CNPG cluster that's already
// running on the K8s cluster, we create it in Running state with the
// discovered configuration.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Adopt(
environmentId: Guid.NewGuid(),
clusterId: Guid.NewGuid(),
name: "existing-db",
ns: "cnpg-system",
postgresVersion: "15.6",
instances: 2,
storageSize: "20Gi",
minioEndpoint: "minio.minio-system.svc:9000",
minioBucket: "existing-backups",
minioCredentialsSecret: "existing-minio-creds",
databases: new List<string> { "app_db", "analytics_db" });
// Assert — Adopted clusters start in Running state and include
// any databases that were already present.
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Running);
pgCluster.Databases.Should().HaveCount(2);
pgCluster.Databases.Should().Contain("app_db");
pgCluster.Databases.Should().Contain("analytics_db");
}
[Fact]
public void MarkRunning_TransitionsFromProvisioning()
{
// Arrange — A freshly created cluster in Provisioning state.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
// Act — The reconciler confirmed the cluster is ready.
pgCluster.MarkRunning();
// Assert
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Running);
pgCluster.LastReconcileAt.Should().NotBeNull();
}
[Fact]
public void AddDatabase_WhenRunning_AddsDatabaseToList()
{
// Arrange — A running cluster can have databases added to it.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
// Act — An application requests a new database be provisioned.
Result<string> result = pgCluster.AddDatabase("my_app_db", "app_user");
// Assert
result.IsSuccess.Should().BeTrue();
pgCluster.Databases.Should().Contain("my_app_db");
}
[Fact]
public void AddDatabase_WhenNotRunning_ReturnsFailure()
{
// Arrange — A cluster that's still provisioning can't accept new databases.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
// Act
Result<string> result = pgCluster.AddDatabase("my_db", "user");
// Assert
result.IsSuccess.Should().BeFalse();
result.Error.Should().Contain("running");
}
[Fact]
public void AddDatabase_DuplicateName_ReturnsFailure()
{
// Arrange — A running cluster with one database already.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
pgCluster.AddDatabase("existing_db", "user1");
// Act — Try to add a database with the same name.
Result<string> result = pgCluster.AddDatabase("existing_db", "user2");
// Assert
result.IsSuccess.Should().BeFalse();
result.Error.Should().Contain("already exists");
}
[Fact]
public void RequestUpgrade_WhenRunning_SetsUpgradingStatusAndTargetVersion()
{
// Arrange — A running cluster at version 15.6.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "15.6", 3, "50Gi",
"minio:9000", "bucket", "secret");
pgCluster.MarkRunning();
// Act — Platform admin requests upgrade to 16.4.
Result<string> result = pgCluster.RequestUpgrade("16.4");
// Assert — The cluster transitions to Upgrading state with the target version recorded.
result.IsSuccess.Should().BeTrue();
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Upgrading);
pgCluster.TargetVersion.Should().Be("16.4");
}
[Fact]
public void RequestUpgrade_WhenNotRunning_ReturnsFailure()
{
// Arrange — A cluster still provisioning can't be upgraded.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
// Act
Result<string> result = pgCluster.RequestUpgrade("16.4");
// Assert
result.IsSuccess.Should().BeFalse();
result.Error.Should().Contain("running");
}
[Fact]
public void RequestUpgrade_ToSameVersion_ReturnsFailure()
{
// Arrange — A cluster already at version 16.4.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "16.4", 3, "50Gi",
"minio:9000", "bucket", "secret");
pgCluster.MarkRunning();
// Act — Try to upgrade to the same version.
Result<string> result = pgCluster.RequestUpgrade("16.4");
// Assert
result.IsSuccess.Should().BeFalse();
result.Error.Should().Contain("same version");
}
[Fact]
public void CompleteUpgrade_TransitionsToRunningWithNewVersion()
{
// Arrange — A cluster that's mid-upgrade.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "15.6", 3, "50Gi",
"minio:9000", "bucket", "secret");
pgCluster.MarkRunning();
pgCluster.RequestUpgrade("16.4");
// Act — The reconciler confirmed the upgrade completed.
pgCluster.CompleteUpgrade();
// Assert — Version updated, status back to Running.
pgCluster.PostgresVersion.Should().Be("16.4");
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Running);
pgCluster.TargetVersion.Should().BeNull();
}
[Fact]
public void ScaleInstances_UpdatesInstanceCount()
{
// Arrange — A running 3-instance cluster.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
// Act — Scale up to 5 instances for more read replicas.
Result<string> result = pgCluster.ScaleInstances(5);
// Assert
result.IsSuccess.Should().BeTrue();
pgCluster.Instances.Should().Be(5);
}
[Fact]
public void ScaleInstances_ToZero_ReturnsFailure()
{
// Arrange
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
// Act
Result<string> result = pgCluster.ScaleInstances(0);
// Assert
result.IsSuccess.Should().BeFalse();
result.Error.Should().Contain("at least 1");
}
[Fact]
public void ConfigureBackupSchedule_UpdatesRetentionPolicy()
{
// Arrange — A running cluster needs its backup schedule updated.
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
// Act — Set backup to run every 6 hours with 30-day retention.
pgCluster.ConfigureBackupSchedule("0 */6 * * *", retentionDays: 30);
// Assert
pgCluster.BackupConfig!.Schedule.Should().Be("0 */6 * * *");
pgCluster.BackupConfig.RetentionDays.Should().Be(30);
}
[Fact]
public void MarkDegraded_SetsStatusToDegraded()
{
// Arrange
Provisioning.Domain.PostgresCluster pgCluster = CreateTestCluster();
pgCluster.MarkRunning();
// Act — Reconciler detected an issue.
pgCluster.MarkDegraded("Primary pod not ready");
// Assert
pgCluster.Status.Should().Be(Provisioning.Domain.PostgresClusterStatus.Degraded);
pgCluster.StatusMessage.Should().Be("Primary pod not ready");
}
[Fact]
public void Create_WithS3Region_StoresRegionInBackupConfig()
{
// Arrange & Act — When creating a CNPG cluster backed by an external S3
// provider like Cleura, the region must be captured so barman-cloud can
// use it for AWS Signature V4 authentication.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
environmentId: Guid.NewGuid(),
clusterId: Guid.NewGuid(),
name: "cleura-db",
ns: "cnpg-prod",
postgresVersion: "16.4",
instances: 3,
storageSize: "100Gi",
minioEndpoint: "https://s3.cleura.cloud",
minioBucket: "cnpg-backups",
minioCredentialsSecret: "cnpg-s3-creds",
s3Region: "eu-north-1");
// Assert — The region is stored in the backup configuration so it can
// be included in the CNPG manifest's s3Credentials.region field.
pgCluster.BackupConfig.Should().NotBeNull();
pgCluster.BackupConfig!.S3Region.Should().Be("eu-north-1");
}
[Fact]
public void Create_WithoutS3Region_DefaultsToNull()
{
// Arrange & Act — MinIO endpoints don't need a region. When omitted,
// S3Region should be null and barman-cloud will skip region config.
Provisioning.Domain.PostgresCluster pgCluster = Provisioning.Domain.PostgresCluster.Create(
environmentId: Guid.NewGuid(),
clusterId: Guid.NewGuid(),
name: "minio-db",
ns: "cnpg-system",
postgresVersion: "16.4",
instances: 3,
storageSize: "50Gi",
minioEndpoint: "minio.minio-system.svc:9000",
minioBucket: "cnpg-backups",
minioCredentialsSecret: "minio-creds");
// Assert
pgCluster.BackupConfig.Should().NotBeNull();
pgCluster.BackupConfig!.S3Region.Should().BeNull();
}
private static Provisioning.Domain.PostgresCluster CreateTestCluster()
{
return Provisioning.Domain.PostgresCluster.Create(
Guid.NewGuid(),
Guid.NewGuid(),
"test-db",
"cnpg-system",
"16.4",
3,
"50Gi",
"minio.minio-system.svc:9000",
"cnpg-backups",
"minio-cnpg-credentials");
}
}