too much for one commit
This commit is contained in:
437
tests/EntKube.Provisioning.Tests/Domain/PostgresClusterTests.cs
Normal file
437
tests/EntKube.Provisioning.Tests/Domain/PostgresClusterTests.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user