143 lines
5.6 KiB
C#
143 lines
5.6 KiB
C#
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();
|
|
}
|
|
}
|