using EntKube.SharedKernel.Domain; using FluentAssertions; namespace EntKube.Provisioning.Tests.Domain; /// /// Tests for the MongoCluster aggregate root. This rich domain model tracks /// everything about a MongoDB Community Operator cluster: replica set members, /// storage, backup configuration, databases, users, and version lifecycle. /// /// MongoDB Community Operator manages MongoDBCommunity CRDs on Kubernetes. /// Each cluster is a replica set with configurable members, storage, and /// authentication. Backup is handled via scheduled mongodump jobs to MinIO. /// public class MongoClusterTests { // ─── Creation ────────────────────────────────────────────────────────────── [Fact] public void Create_WithValidInputs_ReturnsClusterInProvisioningState() { // Arrange & Act — A platform admin provisions a new MongoDB cluster // for shared use on a specific Kubernetes cluster. Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( environmentId: environmentId, clusterId: clusterId, name: "platform-mongo", ns: "mongodb-system", mongoVersion: "7.0.12", members: 3, storageSize: "50Gi", minioEndpoint: "minio.minio-system.svc:9000", minioBucket: "mongo-backups", minioCredentialsSecret: "minio-mongo-credentials"); // Assert — The cluster starts in Provisioning state, waiting for the // reconciler to create the MongoDBCommunity resource. mongoCluster.Id.Should().NotBe(Guid.Empty); mongoCluster.EnvironmentId.Should().Be(environmentId); mongoCluster.ClusterId.Should().Be(clusterId); mongoCluster.Name.Should().Be("platform-mongo"); mongoCluster.Namespace.Should().Be("mongodb-system"); mongoCluster.MongoVersion.Should().Be("7.0.12"); mongoCluster.Members.Should().Be(3); mongoCluster.StorageSize.Should().Be("50Gi"); mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Provisioning); mongoCluster.Databases.Should().BeEmpty(); } [Fact] public void Create_WithMinIOBackupConfig_SetsBackupConfiguration() { // Arrange & Act — Backup to MinIO is configured at creation time // for mongodump-based backups. Unlike CNPG WAL archiving, MongoDB // uses periodic full dumps. Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( environmentId: Guid.NewGuid(), clusterId: Guid.NewGuid(), name: "prod-mongo", ns: "mongodb-prod", mongoVersion: "7.0.12", members: 3, storageSize: "100Gi", minioEndpoint: "minio.minio-system.svc:9000", minioBucket: "mongo-dump-archive", minioCredentialsSecret: "minio-mongo-creds"); // Assert — Backup configuration is stored as part of the aggregate. mongoCluster.BackupConfig.Should().NotBeNull(); mongoCluster.BackupConfig!.MinioEndpoint.Should().Be("minio.minio-system.svc:9000"); mongoCluster.BackupConfig.Bucket.Should().Be("mongo-dump-archive"); mongoCluster.BackupConfig.CredentialsSecret.Should().Be("minio-mongo-creds"); } [Fact] public void Create_WithEmptyName_ThrowsArgumentException() { // Arrange & Act — The domain rejects invalid cluster names. Action act = () => Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "", "ns", "7.0.12", 3, "50Gi", "minio:9000", "bucket", "secret"); // Assert act.Should().Throw().WithParameterName("name"); } [Fact] public void Create_WithZeroMembers_ThrowsArgumentException() { // Arrange & Act — Must have at least 1 replica set member. Action act = () => Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "7.0.12", 0, "50Gi", "minio:9000", "bucket", "secret"); // Assert act.Should().Throw().WithParameterName("members"); } [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.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "7.0.12", 3, "50Gi", "", "bucket", "secret"); // Assert — cluster exists but has no backup configuration. mongoCluster.Should().NotBeNull(); mongoCluster.BackupConfig.Should().BeNull(); } // ─── Adoption ────────────────────────────────────────────────────────────── [Fact] public void Adopt_FromExistingCluster_CreatesRunningClusterWithDiscoveredConfig() { // Arrange & Act — When we adopt an existing MongoDBCommunity cluster // that's already running on K8s, we create it in Running state. Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Adopt( environmentId: Guid.NewGuid(), clusterId: Guid.NewGuid(), name: "existing-mongo", ns: "mongodb-system", mongoVersion: "6.0.16", members: 2, storageSize: "20Gi", minioEndpoint: "minio.minio-system.svc:9000", minioBucket: "existing-backups", minioCredentialsSecret: "existing-minio-creds", databases: new List { "app_db", "analytics_db" }); // Assert — Adopted clusters start in Running state and include // any databases that were already present. mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Running); mongoCluster.Databases.Should().HaveCount(2); mongoCluster.Databases.Should().Contain("app_db"); mongoCluster.Databases.Should().Contain("analytics_db"); } // ─── Status transitions ──────────────────────────────────────────────────── [Fact] public void MarkRunning_TransitionsFromProvisioning() { // Arrange — A freshly created cluster in Provisioning state. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); // Act — The reconciler confirmed the cluster is ready. mongoCluster.MarkRunning(); // Assert mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Running); mongoCluster.LastReconcileAt.Should().NotBeNull(); } [Fact] public void MarkDegraded_SetsStatusToDegraded() { // Arrange Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); // Act — Reconciler detected an issue. mongoCluster.MarkDegraded("Replica set member not ready"); // Assert mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Degraded); mongoCluster.StatusMessage.Should().Be("Replica set member not ready"); } // ─── Database management ─────────────────────────────────────────────────── [Fact] public void AddDatabase_WhenRunning_AddsDatabaseToList() { // Arrange — A running cluster can have databases added. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); // Act — An application requests a new database. Result result = mongoCluster.AddDatabase("my_app_db", "app_user"); // Assert result.IsSuccess.Should().BeTrue(); mongoCluster.Databases.Should().Contain("my_app_db"); } [Fact] public void AddDatabase_WhenNotRunning_ReturnsFailure() { // Arrange — A cluster still provisioning can't accept new databases. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); // Act Result result = mongoCluster.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.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); mongoCluster.AddDatabase("existing_db", "user1"); // Act — Try to add a database with the same name. Result result = mongoCluster.AddDatabase("existing_db", "user2"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("already exists"); } // ─── Version upgrades ────────────────────────────────────────────────────── [Fact] public void RequestUpgrade_WhenRunning_SetsUpgradingStatusAndTargetVersion() { // Arrange — A running cluster at version 6.0.16. Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "6.0.16", 3, "50Gi", "minio:9000", "bucket", "secret"); mongoCluster.MarkRunning(); // Act — Platform admin requests upgrade to 7.0.12. Result result = mongoCluster.RequestUpgrade("7.0.12"); // Assert — The cluster transitions to Upgrading state. result.IsSuccess.Should().BeTrue(); mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Upgrading); mongoCluster.TargetVersion.Should().Be("7.0.12"); } [Fact] public void RequestUpgrade_WhenNotRunning_ReturnsFailure() { // Arrange — A cluster still provisioning can't be upgraded. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); // Act Result result = mongoCluster.RequestUpgrade("7.0.12"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("running"); } [Fact] public void RequestUpgrade_ToSameVersion_ReturnsFailure() { // Arrange — A cluster already at version 7.0.12. Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "7.0.12", 3, "50Gi", "minio:9000", "bucket", "secret"); mongoCluster.MarkRunning(); // Act — Try to upgrade to the same version. Result result = mongoCluster.RequestUpgrade("7.0.12"); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("same version"); } [Fact] public void CompleteUpgrade_TransitionsToRunningWithNewVersion() { // Arrange — A cluster mid-upgrade. Provisioning.Domain.MongoCluster mongoCluster = Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "db", "ns", "6.0.16", 3, "50Gi", "minio:9000", "bucket", "secret"); mongoCluster.MarkRunning(); mongoCluster.RequestUpgrade("7.0.12"); // Act — Reconciler confirmed upgrade completed. mongoCluster.CompleteUpgrade(); // Assert — Version updated, status back to Running. mongoCluster.MongoVersion.Should().Be("7.0.12"); mongoCluster.Status.Should().Be(Provisioning.Domain.MongoClusterStatus.Running); mongoCluster.TargetVersion.Should().BeNull(); } // ─── Scaling ─────────────────────────────────────────────────────────────── [Fact] public void ScaleMembers_UpdatesMemberCount() { // Arrange — A running 3-member cluster. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); // Act — Scale up to 5 members for more redundancy. Result result = mongoCluster.ScaleMembers(5); // Assert result.IsSuccess.Should().BeTrue(); mongoCluster.Members.Should().Be(5); } [Fact] public void ScaleMembers_ToZero_ReturnsFailure() { // Arrange Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); // Act Result result = mongoCluster.ScaleMembers(0); // Assert result.IsSuccess.Should().BeFalse(); result.Error.Should().Contain("at least 1"); } // ─── Backup configuration ────────────────────────────────────────────────── [Fact] public void ConfigureBackupSchedule_UpdatesRetentionPolicy() { // Arrange — A running cluster needs its backup schedule updated. Provisioning.Domain.MongoCluster mongoCluster = CreateTestCluster(); mongoCluster.MarkRunning(); // Act — Set backup to run every 6 hours with 30-day retention. mongoCluster.ConfigureBackupSchedule("0 */6 * * *", retentionDays: 30); // Assert mongoCluster.BackupConfig!.Schedule.Should().Be("0 */6 * * *"); mongoCluster.BackupConfig.RetentionDays.Should().Be(30); } // ─── Helper ──────────────────────────────────────────────────────────────── private static Provisioning.Domain.MongoCluster CreateTestCluster() { return Provisioning.Domain.MongoCluster.Create( Guid.NewGuid(), Guid.NewGuid(), "test-mongo", "mongodb-system", "7.0.12", 3, "50Gi", "minio.minio-system.svc:9000", "mongo-backups", "minio-mongo-credentials"); } }