too much for one commit
This commit is contained in:
@@ -0,0 +1,646 @@
|
||||
using EntKube.Provisioning.Domain;
|
||||
using EntKube.Provisioning.Features.PostgresClusters;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.AdoptPostgresCluster;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.CreatePostgresCluster;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.AddDatabase;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.UpgradePostgresCluster;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.ConfigurePostgresCluster;
|
||||
using EntKube.Provisioning.Features.PostgresClusters.RestoreBackup;
|
||||
using EntKube.Provisioning.Infrastructure;
|
||||
using EntKube.SharedKernel.Domain;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace EntKube.Provisioning.Tests.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for all PostgreSQL cluster management features. Each test verifies that
|
||||
/// the handler correctly orchestrates domain logic + K8s client interactions.
|
||||
/// </summary>
|
||||
public class PostgresClusterFeatureTests
|
||||
{
|
||||
private readonly InMemoryPostgresClusterRepository repository;
|
||||
private readonly InMemoryMinioInstanceRepository minioRepository;
|
||||
private readonly Mock<ICnpgClusterClient> cnpgClient;
|
||||
|
||||
public PostgresClusterFeatureTests()
|
||||
{
|
||||
repository = new InMemoryPostgresClusterRepository();
|
||||
minioRepository = new InMemoryMinioInstanceRepository();
|
||||
cnpgClient = new Mock<ICnpgClusterClient>();
|
||||
}
|
||||
|
||||
// ─── Create Cluster ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCluster_WithValidRequest_CreatesAndCallsK8sClient()
|
||||
{
|
||||
// Arrange — A platform admin wants a new 3-instance PostgreSQL cluster
|
||||
// with WAL archiving to MinIO.
|
||||
|
||||
CreatePostgresClusterHandler handler = new(repository, cnpgClient.Object, minioRepository);
|
||||
|
||||
CreatePostgresClusterRequest request = new(
|
||||
EnvironmentId: Guid.NewGuid(),
|
||||
ClusterId: Guid.NewGuid(),
|
||||
Name: "platform-db",
|
||||
Namespace: "cnpg-system",
|
||||
PostgresVersion: "16.4",
|
||||
Instances: 3,
|
||||
StorageSize: "50Gi",
|
||||
KubeConfig: "apiVersion: v1\nkind: Config",
|
||||
ContextName: "prod-ctx",
|
||||
MinioEndpoint: "minio.minio-system.svc:9000",
|
||||
MinioBucket: "cnpg-backups",
|
||||
MinioCredentialsSecret: "minio-cnpg-creds");
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> 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<PostgresCluster> clusters = await repository.GetAllAsync();
|
||||
clusters.Should().HaveCount(1);
|
||||
clusters[0].Name.Should().Be("platform-db");
|
||||
clusters[0].Status.Should().Be(PostgresClusterStatus.Provisioning);
|
||||
|
||||
cnpgClient.Verify(c => c.CreateClusterAsync(
|
||||
"apiVersion: v1\nkind: Config",
|
||||
"prod-ctx",
|
||||
It.Is<CnpgClusterSpec>(s =>
|
||||
s.Name == "platform-db" &&
|
||||
s.PostgresVersion == "16.4" &&
|
||||
s.Instances == 3 &&
|
||||
s.MinioEndpoint == "minio.minio-system.svc:9000"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCluster_WithMissingName_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
CreatePostgresClusterHandler handler = new(repository, cnpgClient.Object, minioRepository);
|
||||
|
||||
CreatePostgresClusterRequest request = new(
|
||||
EnvironmentId: Guid.NewGuid(),
|
||||
ClusterId: Guid.NewGuid(),
|
||||
Name: "",
|
||||
Namespace: "ns",
|
||||
PostgresVersion: "16.4",
|
||||
Instances: 3,
|
||||
StorageSize: "50Gi",
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
MinioEndpoint: "minio:9000",
|
||||
MinioBucket: "bucket",
|
||||
MinioCredentialsSecret: "secret");
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("name");
|
||||
}
|
||||
|
||||
// ─── Adopt Cluster ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AdoptCluster_WithDiscoveredClusters_ImportsAll()
|
||||
{
|
||||
// Arrange — The K8s cluster has 2 existing CNPG clusters. We adopt them
|
||||
// so the platform is aware of what's already running.
|
||||
|
||||
cnpgClient.Setup(c => c.DiscoverClustersAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<DiscoveredPostgresCluster>
|
||||
{
|
||||
new("prod-db", "cnpg-system", "16.4", 3, "100Gi",
|
||||
new List<string> { "app_db", "analytics_db" },
|
||||
"minio.minio-system.svc:9000", "pg-backups", "minio-creds", true),
|
||||
new("staging-db", "cnpg-staging", "15.6", 1, "20Gi",
|
||||
new List<string> { "staging_db" },
|
||||
"minio.minio-system.svc:9000", "pg-backups-staging", "minio-creds", true)
|
||||
});
|
||||
|
||||
AdoptPostgresClustersHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AdoptPostgresClustersRequest request = new(
|
||||
EnvironmentId: Guid.NewGuid(),
|
||||
ClusterId: Guid.NewGuid(),
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<List<Guid>> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Both clusters adopted and stored in Running state.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().HaveCount(2);
|
||||
|
||||
IReadOnlyList<PostgresCluster> all = await repository.GetAllAsync();
|
||||
all.Should().HaveCount(2);
|
||||
all[0].Status.Should().Be(PostgresClusterStatus.Running);
|
||||
all[0].Databases.Should().Contain("app_db");
|
||||
all[1].Databases.Should().Contain("staging_db");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdoptCluster_WithNoClusters_ReturnsEmptyList()
|
||||
{
|
||||
// Arrange — No CNPG clusters found on the K8s cluster.
|
||||
|
||||
cnpgClient.Setup(c => c.DiscoverClustersAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<DiscoveredPostgresCluster>());
|
||||
|
||||
AdoptPostgresClustersHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AdoptPostgresClustersRequest request = new(Guid.NewGuid(), Guid.NewGuid(), "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<List<Guid>> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ─── Add Database ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AddDatabase_ToRunningCluster_CreatesDatabaseOnK8s()
|
||||
{
|
||||
// Arrange — A running PostgreSQL cluster exists.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Create(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
pgCluster.MarkRunning();
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
AddDatabaseHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AddDatabaseRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
DatabaseName: "new_app_db",
|
||||
OwnerRole: "app_user",
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
PostgresCluster? updated = await repository.GetByIdAsync(pgCluster.Id);
|
||||
updated!.Databases.Should().Contain("new_app_db");
|
||||
|
||||
cnpgClient.Verify(c => c.CreateDatabaseAsync(
|
||||
"config", "ctx", "prod-db", "cnpg-system", "new_app_db", "app_user",
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDatabase_ToNonExistentCluster_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
AddDatabaseHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AddDatabaseRequest request = new(Guid.NewGuid(), "db", "user", "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
// ─── Upgrade Cluster ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task UpgradeCluster_WithValidVersion_InitiatesUpgradeOnK8s()
|
||||
{
|
||||
// Arrange — A running cluster at version 15.6.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Create(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "15.6", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
pgCluster.MarkRunning();
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
cnpgClient.Setup(c => c.GetAvailableVersionsAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<string> { "15.6", "15.7", "16.4", "16.5" });
|
||||
|
||||
UpgradePostgresClusterHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
UpgradePostgresClusterRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
TargetVersion: "16.4",
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Domain transitions to Upgrading, K8s client called.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
PostgresCluster? updated = await repository.GetByIdAsync(pgCluster.Id);
|
||||
updated!.Status.Should().Be(PostgresClusterStatus.Upgrading);
|
||||
updated.TargetVersion.Should().Be("16.4");
|
||||
|
||||
cnpgClient.Verify(c => c.UpgradeClusterAsync(
|
||||
"config", "ctx", "prod-db", "cnpg-system", "16.4",
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpgradeCluster_ToUnavailableVersion_ReturnsFailure()
|
||||
{
|
||||
// Arrange — Version 99.0 doesn't exist.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Create(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
pgCluster.MarkRunning();
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
cnpgClient.Setup(c => c.GetAvailableVersionsAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<string> { "16.4", "16.5" });
|
||||
|
||||
UpgradePostgresClusterHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
UpgradePostgresClusterRequest request = new(pgCluster.Id, "99.0", "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not available");
|
||||
}
|
||||
|
||||
// ─── Configure Cluster ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureCluster_ScaleInstances_UpdatesK8sAndDomain()
|
||||
{
|
||||
// Arrange — Scale a running cluster from 3 to 5 instances.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Create(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
pgCluster.MarkRunning();
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
ConfigurePostgresClusterHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
ConfigurePostgresClusterRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
Instances: 5,
|
||||
StorageSize: null,
|
||||
BackupSchedule: null,
|
||||
BackupRetentionDays: null,
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
PostgresCluster? updated = await repository.GetByIdAsync(pgCluster.Id);
|
||||
updated!.Instances.Should().Be(5);
|
||||
|
||||
cnpgClient.Verify(c => c.UpdateClusterAsync(
|
||||
"config", "ctx",
|
||||
It.Is<CnpgClusterSpec>(s => s.Instances == 5),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureCluster_UpdateBackupSchedule_UpdatesScheduledBackup()
|
||||
{
|
||||
// Arrange — Change backup schedule to every 6 hours with 30-day retention.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Create(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
pgCluster.MarkRunning();
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
ConfigurePostgresClusterHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
ConfigurePostgresClusterRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
Instances: null,
|
||||
StorageSize: null,
|
||||
BackupSchedule: "0 */6 * * *",
|
||||
BackupRetentionDays: 30,
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
PostgresCluster? updated = await repository.GetByIdAsync(pgCluster.Id);
|
||||
updated!.BackupConfig!.Schedule.Should().Be("0 */6 * * *");
|
||||
updated.BackupConfig.RetentionDays.Should().Be(30);
|
||||
|
||||
cnpgClient.Verify(c => c.ConfigureScheduledBackupAsync(
|
||||
"config", "ctx", "prod-db", "cnpg-system", "0 */6 * * *", 30,
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConfigureCluster_NonExistent_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
ConfigurePostgresClusterHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
ConfigurePostgresClusterRequest request = new(
|
||||
Guid.NewGuid(), Instances: 5, StorageSize: null,
|
||||
BackupSchedule: null, BackupRetentionDays: null,
|
||||
KubeConfig: "config", ContextName: "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<string> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
// ─── Adopt De-duplication ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AdoptCluster_WhenClusterAlreadyAdopted_SkipsDuplicate()
|
||||
{
|
||||
// Arrange — The platform already has a cluster with the same name and
|
||||
// namespace adopted. Re-running discovery should not create a duplicate.
|
||||
|
||||
Guid clusterId = Guid.NewGuid();
|
||||
|
||||
PostgresCluster existing = PostgresCluster.Adopt(
|
||||
Guid.NewGuid(), clusterId, "prod-db", "cnpg-system", "16.4", 3, "100Gi",
|
||||
"minio:9000", "pg-backups", "minio-creds",
|
||||
databases: new List<string> { "app_db" });
|
||||
|
||||
await repository.AddAsync(existing);
|
||||
|
||||
cnpgClient.Setup(c => c.DiscoverClustersAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<DiscoveredPostgresCluster>
|
||||
{
|
||||
new("prod-db", "cnpg-system", "16.4", 3, "100Gi",
|
||||
new List<string> { "app_db", "analytics_db" },
|
||||
"minio:9000", "pg-backups", "minio-creds", true),
|
||||
new("new-db", "cnpg-staging", "15.8", 1, "20Gi",
|
||||
new List<string> { "staging_db" },
|
||||
"minio:9000", "pg-backups-staging", "minio-creds", true)
|
||||
});
|
||||
|
||||
AdoptPostgresClustersHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AdoptPostgresClustersRequest request = new(Guid.NewGuid(), clusterId, "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<List<Guid>> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Only the new cluster is adopted, the existing one is updated.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
IReadOnlyList<PostgresCluster> all = await repository.GetByClusterIdAsync(clusterId);
|
||||
all.Should().HaveCount(2);
|
||||
all.Should().ContainSingle(c => c.Name == "prod-db");
|
||||
all.Should().ContainSingle(c => c.Name == "new-db");
|
||||
|
||||
// The existing cluster's databases should be updated with newly discovered ones.
|
||||
|
||||
PostgresCluster? updated = all.First(c => c.Name == "prod-db");
|
||||
updated.Databases.Should().Contain("analytics_db");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AdoptCluster_WithClusterWithoutBackup_AdoptsWithDefaults()
|
||||
{
|
||||
// Arrange — A CNPG cluster that has no MinIO backup configured.
|
||||
// The platform should still adopt it with default backup settings.
|
||||
|
||||
cnpgClient.Setup(c => c.DiscoverClustersAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<DiscoveredPostgresCluster>
|
||||
{
|
||||
new("dev-db", "default", "16.4", 1, "10Gi",
|
||||
new List<string> { "myapp" },
|
||||
null, null, null, false)
|
||||
});
|
||||
|
||||
AdoptPostgresClustersHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AdoptPostgresClustersRequest request = new(Guid.NewGuid(), Guid.NewGuid(), "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<List<Guid>> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — Cluster adopted with default MinIO settings.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().HaveCount(1);
|
||||
|
||||
IReadOnlyList<PostgresCluster> all = await repository.GetAllAsync();
|
||||
all[0].Name.Should().Be("dev-db");
|
||||
all[0].Status.Should().Be(PostgresClusterStatus.Running);
|
||||
}
|
||||
|
||||
// ─── Restore Backup ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreBackup_WithValidCluster_CallsCnpgClient()
|
||||
{
|
||||
// Arrange — A running cluster with a backup we want to restore from.
|
||||
// Restoring in CNPG means creating a new cluster from a recovery source.
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Adopt(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret",
|
||||
databases: new List<string> { "app_db" });
|
||||
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
RestoreBackupHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
RestoreBackupRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
BackupName: "prod-db-manual-20250510120000",
|
||||
RestoredClusterName: "prod-db-restored",
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
TargetTime: null);
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — A new cluster should be created from the backup.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
cnpgClient.Verify(c => c.RestoreBackupAsync(
|
||||
"config", "ctx",
|
||||
It.Is<RestoreFromBackupSpec>(s =>
|
||||
s.SourceClusterName == "prod-db" &&
|
||||
s.RestoredClusterName == "prod-db-restored" &&
|
||||
s.BackupName == "prod-db-manual-20250510120000" &&
|
||||
s.Namespace == "cnpg-system"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
|
||||
// The restored cluster should be persisted as a new aggregate.
|
||||
|
||||
IReadOnlyList<PostgresCluster> all = await repository.GetAllAsync();
|
||||
all.Should().HaveCount(2);
|
||||
all.Should().ContainSingle(c => c.Name == "prod-db-restored");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreBackup_WithPointInTimeRecovery_PassesTargetTime()
|
||||
{
|
||||
// Arrange — Restore to a specific point in time (PITR).
|
||||
|
||||
PostgresCluster pgCluster = PostgresCluster.Adopt(
|
||||
Guid.NewGuid(), Guid.NewGuid(), "prod-db", "cnpg-system", "16.4", 3, "50Gi",
|
||||
"minio:9000", "bucket", "secret");
|
||||
|
||||
await repository.AddAsync(pgCluster);
|
||||
|
||||
RestoreBackupHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
DateTimeOffset targetTime = new(2025, 5, 10, 10, 30, 0, TimeSpan.Zero);
|
||||
|
||||
RestoreBackupRequest request = new(
|
||||
PostgresClusterId: pgCluster.Id,
|
||||
BackupName: null,
|
||||
RestoredClusterName: "prod-db-pitr",
|
||||
KubeConfig: "config",
|
||||
ContextName: "ctx",
|
||||
TargetTime: targetTime);
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
cnpgClient.Verify(c => c.RestoreBackupAsync(
|
||||
"config", "ctx",
|
||||
It.Is<RestoreFromBackupSpec>(s =>
|
||||
s.TargetTime == targetTime &&
|
||||
s.RestoredClusterName == "prod-db-pitr"),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreBackup_SourceClusterNotFound_ReturnsFailure()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
RestoreBackupHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
RestoreBackupRequest request = new(
|
||||
Guid.NewGuid(), "backup-name", "restored-cluster", "config", "ctx", null);
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("not found");
|
||||
}
|
||||
|
||||
// ─── List Live Databases ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task AdoptCluster_WithLiveDatabases_IncludesAllDatabases()
|
||||
{
|
||||
// Arrange — Discovery from the CNPG CR spec only finds the bootstrap
|
||||
// database, but the live cluster has additional databases created via SQL.
|
||||
// The adoption should include all of them.
|
||||
|
||||
cnpgClient.Setup(c => c.DiscoverClustersAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<DiscoveredPostgresCluster>
|
||||
{
|
||||
new("prod-db", "cnpg-system", "16.4", 3, "100Gi",
|
||||
new List<string> { "app_db" },
|
||||
"minio:9000", "pg-backups", "minio-creds", true)
|
||||
});
|
||||
|
||||
// The live database listing discovers additional databases.
|
||||
|
||||
cnpgClient.Setup(c => c.ListLiveDatabasesAsync(
|
||||
It.IsAny<string>(), It.IsAny<string>(),
|
||||
"prod-db", "cnpg-system", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new List<string> { "app_db", "analytics_db", "reporting_db" });
|
||||
|
||||
AdoptPostgresClustersHandler handler = new(repository, cnpgClient.Object);
|
||||
|
||||
AdoptPostgresClustersRequest request = new(Guid.NewGuid(), Guid.NewGuid(), "config", "ctx");
|
||||
|
||||
// Act
|
||||
|
||||
Result<List<Guid>> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — All databases from the live cluster are included.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
|
||||
IReadOnlyList<PostgresCluster> all = await repository.GetAllAsync();
|
||||
all[0].Databases.Should().HaveCount(3);
|
||||
all[0].Databases.Should().Contain("app_db");
|
||||
all[0].Databases.Should().Contain("analytics_db");
|
||||
all[0].Databases.Should().Contain("reporting_db");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user