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; /// /// 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. /// 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 options = new DbContextOptionsBuilder() .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 act = async () => await repository.UpdateAsync(loaded); // Assert — Should save without throwing. await act.Should().NotThrowAsync(); // 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 firstScan = new() { new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Pod running" }, new List(), new DiscoveredConfiguration("3.3.4", "kyverno", "kyverno", new Dictionary())) }; 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 secondScan = new() { new ComponentCheckResult("kyverno", ComponentStatus.Installed, new List { "Pod running" }, new List(), new DiscoveredConfiguration("3.8.0", "kyverno", "kyverno", new Dictionary())), new ComponentCheckResult("cert-manager", ComponentStatus.NotInstalled, new List(), new List { "No pods found" }) }; loaded.UpdateComponents(secondScan); Func 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(); } }