using EntKube.Provisioning.Domain; using FluentAssertions; namespace EntKube.Provisioning.Tests.Domain; public class AppTests { // ─── Create ────────────────────────────────────────────────────────── [Fact] public void Create_WithValidInputs_CreatesActiveApp() { // Arrange — A customer admin creates a new app (e.g. "frontend-portal") // that will eventually be deployed across environments. The app starts // in an Active state and is ready to have environments added. Guid tenantId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); // Act App app = App.Create(tenantId, customerId, "Frontend Portal", "frontend-portal", AppType.Deployment); // Assert — The app should be active and linked to its customer and tenant. app.Id.Should().NotBe(Guid.Empty); app.TenantId.Should().Be(tenantId); app.CustomerId.Should().Be(customerId); app.Name.Should().Be("Frontend Portal"); app.Slug.Should().Be("frontend-portal"); app.Type.Should().Be(AppType.Deployment); app.Status.Should().Be(AppStatus.Active); app.CreatedAt.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(5)); app.Environments.Should().BeEmpty(); } [Fact] public void Create_NormalizesSlugToLowerCase() { // Arrange & Act — Slugs should always be lowercase for consistency. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "My App", "MY-APP", AppType.HelmChart); // Assert app.Slug.Should().Be("my-app"); } [Fact] public void Create_WithEmptyTenantId_ThrowsArgumentException() { // Arrange & Act Action act = () => App.Create(Guid.Empty, Guid.NewGuid(), "App", "app", AppType.Deployment); // Assert act.Should().Throw() .WithParameterName("tenantId"); } [Fact] public void Create_WithEmptyCustomerId_ThrowsArgumentException() { // Arrange & Act Action act = () => App.Create(Guid.NewGuid(), Guid.Empty, "App", "app", AppType.Deployment); // Assert act.Should().Throw() .WithParameterName("customerId"); } [Fact] public void Create_WithEmptyName_ThrowsArgumentException() { // Arrange & Act Action act = () => App.Create(Guid.NewGuid(), Guid.NewGuid(), "", "app", AppType.Deployment); // Assert act.Should().Throw() .WithParameterName("name"); } [Fact] public void Create_WithEmptySlug_ThrowsArgumentException() { // Arrange & Act Action act = () => App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "", AppType.Deployment); // Assert act.Should().Throw() .WithParameterName("slug"); } // ─── Suspend / Activate ────────────────────────────────────────────── [Fact] public void Suspend_SetsStatusToSuspended() { // Arrange — An active app that the admin wants to pause deployments for. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); // Act app.Suspend(); // Assert app.Status.Should().Be(AppStatus.Suspended); } [Fact] public void Activate_SetsStatusToActive() { // Arrange — A previously suspended app being re-enabled. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); app.Suspend(); // Act app.Activate(); // Assert app.Status.Should().Be(AppStatus.Active); } // ─── AddEnvironment ────────────────────────────────────────────────── [Fact] public void AddEnvironment_WithValidInputs_CreatesAppEnvironment() { // Arrange — After creating an app, the admin adds it to a specific // environment (e.g. "dev"). This links the app to a cluster and // namespace where it will be deployed. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API Service", "api-service", AppType.Deployment); Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); // Act AppEnvironment env = app.AddEnvironment(environmentId, clusterId, "api-dev"); // Assert — The environment should be created in Pending state. env.Id.Should().NotBe(Guid.Empty); env.EnvironmentId.Should().Be(environmentId); env.ClusterId.Should().Be(clusterId); env.Namespace.Should().Be("api-dev"); env.SyncStatus.Should().Be(AppSyncStatus.Pending); app.Environments.Should().HaveCount(1); } [Fact] public void AddEnvironment_DuplicateEnvironmentId_ThrowsInvalidOperation() { // Arrange — An app can only be deployed to each environment once. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); Guid environmentId = Guid.NewGuid(); app.AddEnvironment(environmentId, Guid.NewGuid(), "app-dev"); // Act Action act = () => app.AddEnvironment(environmentId, Guid.NewGuid(), "app-dev-2"); // Assert act.Should().Throw() .WithMessage("*already*"); } [Fact] public void AddEnvironment_WithEmptyNamespace_ThrowsArgumentException() { // Arrange & Act App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); Action act = () => app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), ""); // Assert act.Should().Throw() .WithParameterName("ns"); } // ─── RemoveEnvironment ─────────────────────────────────────────────── [Fact] public void RemoveEnvironment_ExistingEnvironment_RemovesIt() { // Arrange — An app deployed to dev that the admin wants to remove. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); Guid environmentId = Guid.NewGuid(); app.AddEnvironment(environmentId, Guid.NewGuid(), "app-dev"); // Act app.RemoveEnvironment(environmentId); // Assert app.Environments.Should().BeEmpty(); } [Fact] public void RemoveEnvironment_NonExistent_ThrowsInvalidOperation() { // Arrange & Act App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "App", "app", AppType.Deployment); Action act = () => app.RemoveEnvironment(Guid.NewGuid()); // Assert act.Should().Throw() .WithMessage("*not found*"); } }