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 environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); ServiceInstance instance = ServiceInstance.Provision( environmentId, ServiceType.MinIO, "tenant-storage", "minio-system", clusterId); // Assert — Starts pending until the reconciler deploys it. instance.Id.Should().NotBe(Guid.Empty); instance.EnvironmentId.Should().Be(environmentId); 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", Guid.NewGuid()); // Assert act.Should().Throw() .WithParameterName("name"); } [Fact] public void MarkRunning_UpdatesCurrentState() { // Arrange ServiceInstance instance = ServiceInstance.Provision(Guid.NewGuid(), ServiceType.CloudNativePG, "db", "cnpg-system", Guid.NewGuid()); // 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", Guid.NewGuid()); 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); } [Fact] public void ServiceType_IncludesGitea() { // Gitea is a shared service that can be provisioned on a cluster, // so ServiceType must include it as a valid option. ServiceType type = ServiceType.Gitea; type.Should().BeDefined(); } [Fact] public void ServiceType_IncludesHarbor() { // Harbor is a shared container registry that can be provisioned // on a cluster, so ServiceType must include it. ServiceType type = ServiceType.Harbor; type.Should().BeDefined(); } [Fact] public void Provision_Gitea_CreatesInstanceInPendingState() { // Gitea should be provisionable just like any other service type. ServiceInstance instance = ServiceInstance.Provision( Guid.NewGuid(), ServiceType.Gitea, "team-gitea", "gitea", Guid.NewGuid()); instance.ServiceType.Should().Be(ServiceType.Gitea); instance.CurrentState.Should().Be(ServiceState.Pending); } [Fact] public void Provision_Harbor_CreatesInstanceInPendingState() { // Harbor should be provisionable just like any other service type. ServiceInstance instance = ServiceInstance.Provision( Guid.NewGuid(), ServiceType.Harbor, "team-harbor", "harbor", Guid.NewGuid()); instance.ServiceType.Should().Be(ServiceType.Harbor); instance.CurrentState.Should().Be(ServiceState.Pending); } }