too much for one commit
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
using EntKube.Clusters.Domain;
|
||||
using EntKube.Clusters.Features.AdoptCluster;
|
||||
using EntKube.Clusters.Infrastructure;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace EntKube.Clusters.Tests.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the EF Core cluster repository. These tests use a real
|
||||
/// SQLite database to verify that EF change tracking works correctly — something
|
||||
/// the in-memory repository can't catch. The key scenario: adding a child entity
|
||||
/// (StorageBucket) to an already-tracked aggregate and saving without a
|
||||
/// DbUpdateConcurrencyException.
|
||||
/// </summary>
|
||||
public class EfClusterRepositoryTests : IDisposable
|
||||
{
|
||||
private readonly SqliteConnection connection;
|
||||
private readonly ClustersDbContext db;
|
||||
private readonly EfClusterRepository repository;
|
||||
|
||||
public EfClusterRepositoryTests()
|
||||
{
|
||||
// Use an in-memory SQLite database that persists for the test lifetime.
|
||||
|
||||
connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
|
||||
DbContextOptions<ClustersDbContext> options = new DbContextOptionsBuilder<ClustersDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
db = new ClustersDbContext(options);
|
||||
db.Database.EnsureCreated();
|
||||
repository = new EfClusterRepository(db);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_AddingStorageBucket_DoesNotThrowConcurrencyException()
|
||||
{
|
||||
// Arrange — Register a cluster and persist it to the database.
|
||||
// This simulates the real flow: a cluster already exists, and
|
||||
// the user creates a storage bucket that needs to be saved.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"test-cluster", "https://k8s:6443", "kubeconfig-data",
|
||||
Guid.NewGuid(), "test-ctx", Guid.NewGuid());
|
||||
|
||||
cluster.MarkConnected();
|
||||
cluster.SetProvider(CloudProvider.Cleura, new ProviderCredentials("user", "pass", "eu-north-1"));
|
||||
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
// Act — Load the cluster back, add a new storage bucket, then save.
|
||||
// Before the fix, db.Clusters.Update() would mark the new bucket as
|
||||
// Modified instead of Added, causing DbUpdateConcurrencyException.
|
||||
|
||||
KubernetesCluster? loaded = await repository.GetByIdAsync(cluster.Id);
|
||||
loaded.Should().NotBeNull();
|
||||
|
||||
StorageBucket bucket = StorageBucket.Create(
|
||||
"my-encrypted-bucket", "AKID", "SECRET", "https://s3.example.com", "eu-north-1", true);
|
||||
|
||||
loaded!.AddStorageBucket(bucket);
|
||||
|
||||
Func<Task> act = async () => await repository.UpdateAsync(loaded);
|
||||
|
||||
// Assert — Should save without throwing.
|
||||
|
||||
await act.Should().NotThrowAsync<DbUpdateConcurrencyException>();
|
||||
|
||||
// Verify the bucket was actually persisted.
|
||||
|
||||
KubernetesCluster? reloaded = await repository.GetByIdAsync(cluster.Id);
|
||||
reloaded!.StorageBuckets.Should().HaveCount(1);
|
||||
reloaded.StorageBuckets[0].Name.Should().Be("my-encrypted-bucket");
|
||||
reloaded.StorageBuckets[0].Encrypted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAsync_ReplacingComponents_PersistsNewComponents()
|
||||
{
|
||||
// Arrange — Register a cluster, run a first adoption scan, and persist.
|
||||
// This mirrors the real flow: adoption scan detects components, then
|
||||
// a second scan replaces them with fresh results.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"scan-cluster", "https://k8s:6443", "kubeconfig-data",
|
||||
Guid.NewGuid(), "scan-ctx", Guid.NewGuid());
|
||||
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Simulate first adoption scan — detect kyverno as installed.
|
||||
|
||||
List<ComponentCheckResult> firstScan = new()
|
||||
{
|
||||
new ComponentCheckResult("kyverno", ComponentStatus.Installed,
|
||||
new List<string> { "Pod running" }, new List<string>(),
|
||||
new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary<string, string>()))
|
||||
};
|
||||
|
||||
cluster.UpdateComponents(firstScan);
|
||||
await repository.AddAsync(cluster);
|
||||
|
||||
// Act — Load the cluster, run a second scan with different results,
|
||||
// and save. This is the adoption flow: UpdateComponents replaces the
|
||||
// backing field, then UpdateAsync persists the change.
|
||||
|
||||
KubernetesCluster? loaded = await repository.GetByIdAsync(cluster.Id);
|
||||
loaded.Should().NotBeNull();
|
||||
loaded!.Components.Should().HaveCount(1);
|
||||
|
||||
List<ComponentCheckResult> secondScan = new()
|
||||
{
|
||||
new ComponentCheckResult("kyverno", ComponentStatus.Installed,
|
||||
new List<string> { "Pod running" }, new List<string>(),
|
||||
new DiscoveredConfiguration("3.8.0", "kyverno", "kyverno", new Dictionary<string, string>())),
|
||||
new ComponentCheckResult("cert-manager", ComponentStatus.NotInstalled,
|
||||
new List<string>(), new List<string> { "No pods found" })
|
||||
};
|
||||
|
||||
loaded.UpdateComponents(secondScan);
|
||||
|
||||
Func<Task> act = async () => await repository.UpdateAsync(loaded);
|
||||
|
||||
// Assert — Should save without error and persist both components.
|
||||
|
||||
await act.Should().NotThrowAsync();
|
||||
|
||||
KubernetesCluster? reloaded = await repository.GetByIdAsync(cluster.Id);
|
||||
reloaded!.Components.Should().HaveCount(2);
|
||||
reloaded.Components.Should().Contain(c => c.ComponentName == "kyverno" && c.Version == "3.8.0");
|
||||
reloaded.Components.Should().Contain(c => c.ComponentName == "cert-manager");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
db.Dispose();
|
||||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user