Add unit tests for KubernetesCluster, Tenant, ServiceInstance, and RegisterClusterHandler
- Implement tests for KubernetesCluster including registration, connectivity status, and error handling. - Create tests for Tenant creation, member management, and status changes. - Add tests for ServiceInstance provisioning and state management. - Introduce RegisterClusterHandler tests to validate registration requests and error scenarios. - Set up project files for new test projects with necessary dependencies.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
using EntKube.Clusters.Domain;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Clusters.Tests.Domain;
|
||||
|
||||
public class KubernetesClusterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_WithValidInputs_CreatesClusterInPendingState()
|
||||
{
|
||||
// Arrange & Act — A tenant admin registers a new cluster with its name and API URL.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register(
|
||||
"production-eu",
|
||||
"https://k8s.example.com:6443",
|
||||
"secret-ref-123");
|
||||
|
||||
// Assert — The cluster should be created with a unique ID and start in Pending state
|
||||
// because we haven't verified connectivity yet.
|
||||
|
||||
cluster.Id.Should().NotBe(Guid.Empty);
|
||||
cluster.Name.Should().Be("production-eu");
|
||||
cluster.ApiServerUrl.Should().Be("https://k8s.example.com:6443");
|
||||
cluster.KubeConfigSecret.Should().Be("secret-ref-123");
|
||||
cluster.Status.Should().Be(ClusterStatus.Pending);
|
||||
cluster.RegisteredAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WithEmptyName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act — Attempting to register a cluster without a name should fail
|
||||
// because every cluster needs a human-readable identifier.
|
||||
|
||||
Action act = () => KubernetesCluster.Register("", "https://k8s.example.com", null);
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_WithEmptyApiServerUrl_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act — Without an API URL, we can't communicate with the cluster.
|
||||
|
||||
Action act = () => KubernetesCluster.Register("my-cluster", "", null);
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("apiServerUrl");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkConnected_SetsStatusToConnected()
|
||||
{
|
||||
// Arrange — A freshly registered cluster in Pending state.
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", null);
|
||||
|
||||
// Act — The health check confirms the cluster is reachable.
|
||||
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Status.Should().Be(ClusterStatus.Connected);
|
||||
cluster.LastHealthCheckAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkUnreachable_SetsStatusToUnreachable()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
KubernetesCluster cluster = KubernetesCluster.Register("test", "https://api.test", null);
|
||||
cluster.MarkConnected();
|
||||
|
||||
// Act — The health check fails.
|
||||
|
||||
cluster.MarkUnreachable();
|
||||
|
||||
// Assert
|
||||
|
||||
cluster.Status.Should().Be(ClusterStatus.Unreachable);
|
||||
cluster.LastHealthCheckAt.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
28
tests/EntKube.Clusters.Tests/EntKube.Clusters.Tests.csproj
Normal file
28
tests/EntKube.Clusters.Tests/EntKube.Clusters.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\EntKube.Clusters\EntKube.Clusters.csproj" />
|
||||
<ProjectReference Include="..\..\src\EntKube.SharedKernel\EntKube.SharedKernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,74 @@
|
||||
using EntKube.Clusters.Domain;
|
||||
using EntKube.Clusters.Features.RegisterCluster;
|
||||
using EntKube.Clusters.Infrastructure;
|
||||
using EntKube.SharedKernel.Domain;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Clusters.Tests.Features;
|
||||
|
||||
public class RegisterClusterHandlerTests
|
||||
{
|
||||
private readonly RegisterClusterHandler handler;
|
||||
private readonly InMemoryClusterRepository repository;
|
||||
|
||||
public RegisterClusterHandlerTests()
|
||||
{
|
||||
repository = new InMemoryClusterRepository();
|
||||
handler = new RegisterClusterHandler(repository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithValidRequest_ReturnsSuccessWithClusterId()
|
||||
{
|
||||
// Arrange — A valid registration request with all required fields.
|
||||
|
||||
RegisterClusterRequest request = new("production-eu", "https://k8s.example.com:6443", "secret-ref");
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert — The handler should succeed and the cluster should be persisted.
|
||||
|
||||
result.IsSuccess.Should().BeTrue();
|
||||
result.Value.Should().NotBe(Guid.Empty);
|
||||
|
||||
KubernetesCluster? persisted = await repository.GetByIdAsync(result.Value!);
|
||||
persisted.Should().NotBeNull();
|
||||
persisted!.Name.Should().Be("production-eu");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithEmptyName_ReturnsFailure()
|
||||
{
|
||||
// Arrange — Missing cluster name.
|
||||
|
||||
RegisterClusterRequest request = new("", "https://k8s.example.com:6443", null);
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithEmptyApiUrl_ReturnsFailure()
|
||||
{
|
||||
// Arrange — Missing API server URL.
|
||||
|
||||
RegisterClusterRequest request = new("my-cluster", "", null);
|
||||
|
||||
// Act
|
||||
|
||||
Result<Guid> result = await handler.HandleAsync(request);
|
||||
|
||||
// Assert
|
||||
|
||||
result.IsFailure.Should().BeTrue();
|
||||
result.Error.Should().Contain("API server URL");
|
||||
}
|
||||
}
|
||||
103
tests/EntKube.Identity.Tests/Domain/TenantTests.cs
Normal file
103
tests/EntKube.Identity.Tests/Domain/TenantTests.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using EntKube.Identity.Domain;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Identity.Tests.Domain;
|
||||
|
||||
public class TenantTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_WithValidInputs_CreatesTenantWithAdminMember()
|
||||
{
|
||||
// Arrange & Act — An admin creates a new tenant organization.
|
||||
|
||||
Guid userId = Guid.NewGuid();
|
||||
Tenant tenant = Tenant.Create("Acme Corp", "acme-corp", userId);
|
||||
|
||||
// Assert — The tenant should be active and the creator should be admin.
|
||||
|
||||
tenant.Id.Should().NotBe(Guid.Empty);
|
||||
tenant.Name.Should().Be("Acme Corp");
|
||||
tenant.Slug.Should().Be("acme-corp");
|
||||
tenant.Status.Should().Be(TenantStatus.Active);
|
||||
tenant.Members.Should().HaveCount(1);
|
||||
tenant.Members[0].UserId.Should().Be(userId);
|
||||
tenant.Members[0].Role.Should().Be(TenantRole.Admin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptyName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
Action act = () => Tenant.Create("", "slug", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_WithEmptySlug_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
Action act = () => Tenant.Create("Acme", "", Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("slug");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddMember_NewUser_AddsMemberToTenant()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = Tenant.Create("Acme", "acme", Guid.NewGuid());
|
||||
Guid newUserId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
|
||||
tenant.AddMember(newUserId, TenantRole.Member);
|
||||
|
||||
// Assert
|
||||
|
||||
tenant.Members.Should().HaveCount(2);
|
||||
tenant.Members.Should().Contain(m => m.UserId == newUserId && m.Role == TenantRole.Member);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddMember_ExistingUser_DoesNotDuplicate()
|
||||
{
|
||||
// Arrange — Create a tenant (creator is already a member).
|
||||
|
||||
Guid userId = Guid.NewGuid();
|
||||
Tenant tenant = Tenant.Create("Acme", "acme", userId);
|
||||
|
||||
// Act — Try adding the same user again.
|
||||
|
||||
tenant.AddMember(userId, TenantRole.Viewer);
|
||||
|
||||
// Assert — Still only one member.
|
||||
|
||||
tenant.Members.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Suspend_SetsStatusToSuspended()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = Tenant.Create("Acme", "acme", Guid.NewGuid());
|
||||
|
||||
// Act
|
||||
|
||||
tenant.Suspend();
|
||||
|
||||
// Assert
|
||||
|
||||
tenant.Status.Should().Be(TenantStatus.Suspended);
|
||||
}
|
||||
}
|
||||
28
tests/EntKube.Identity.Tests/EntKube.Identity.Tests.csproj
Normal file
28
tests/EntKube.Identity.Tests/EntKube.Identity.Tests.csproj
Normal file
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\EntKube.Identity\EntKube.Identity.csproj" />
|
||||
<ProjectReference Include="..\..\src\EntKube.SharedKernel\EntKube.SharedKernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,79 @@
|
||||
using EntKube.Provisioning.Domain;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace EntKube.Provisioning.Tests.Domain;
|
||||
|
||||
public class ServiceInstanceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Provision_WithValidInputs_CreatesInstanceInPendingState()
|
||||
{
|
||||
// Arrange & Act — Request provisioning of a MinIO instance.
|
||||
|
||||
Guid clusterId = Guid.NewGuid();
|
||||
|
||||
ServiceInstance instance = ServiceInstance.Provision(
|
||||
clusterId,
|
||||
ServiceType.MinIO,
|
||||
"tenant-storage",
|
||||
"minio-system");
|
||||
|
||||
// Assert — Starts pending until the reconciler deploys it.
|
||||
|
||||
instance.Id.Should().NotBe(Guid.Empty);
|
||||
instance.ClusterId.Should().Be(clusterId);
|
||||
instance.ServiceType.Should().Be(ServiceType.MinIO);
|
||||
instance.Name.Should().Be("tenant-storage");
|
||||
instance.Namespace.Should().Be("minio-system");
|
||||
instance.DesiredState.Should().Be(ServiceState.Running);
|
||||
instance.CurrentState.Should().Be(ServiceState.Pending);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Provision_WithEmptyName_ThrowsArgumentException()
|
||||
{
|
||||
// Arrange & Act
|
||||
|
||||
Action act = () => ServiceInstance.Provision(Guid.NewGuid(), ServiceType.MinIO, "", "ns");
|
||||
|
||||
// Assert
|
||||
|
||||
act.Should().Throw<ArgumentException>()
|
||||
.WithParameterName("name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkRunning_UpdatesCurrentState()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
ServiceInstance instance = ServiceInstance.Provision(Guid.NewGuid(), ServiceType.CloudNativePG, "db", "cnpg-system");
|
||||
|
||||
// Act
|
||||
|
||||
instance.MarkRunning();
|
||||
|
||||
// Assert
|
||||
|
||||
instance.CurrentState.Should().Be(ServiceState.Running);
|
||||
instance.LastReconcileAt.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequestDecommission_SetsDesiredStateToDecommissioned()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
ServiceInstance instance = ServiceInstance.Provision(Guid.NewGuid(), ServiceType.Keycloak, "auth", "keycloak-system");
|
||||
instance.MarkRunning();
|
||||
|
||||
// Act — Tenant admin decides to tear down this service.
|
||||
|
||||
instance.RequestDecommission();
|
||||
|
||||
// Assert — Desired state changes but current state remains until reconciler acts.
|
||||
|
||||
instance.DesiredState.Should().Be(ServiceState.Decommissioned);
|
||||
instance.CurrentState.Should().Be(ServiceState.Running);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\EntKube.Provisioning\EntKube.Provisioning.csproj" />
|
||||
<ProjectReference Include="..\..\src\EntKube.SharedKernel\EntKube.SharedKernel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
27
tests/EntKube.Web.Tests/EntKube.Web.Tests.csproj
Normal file
27
tests/EntKube.Web.Tests/EntKube.Web.Tests.csproj
Normal file
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="FluentAssertions" Version="8.9.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\EntKube.Web\EntKube.Web.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user