too much for one commit

This commit is contained in:
Nils Blomgren
2026-05-13 14:01:32 +02:00
parent a96dd33039
commit 328d494530
394 changed files with 98104 additions and 78 deletions

View File

@@ -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();
}
}