using EntKube.Provisioning.Domain; using FluentAssertions; namespace EntKube.Provisioning.Tests.Domain; public class AppEnvironmentTests { // ─── ConfigureDeployment ───────────────────────────────────────────── [Fact] public void ConfigureDeployment_WithValidSpec_SetsDeploymentSpec() { // Arrange — After adding an environment, the admin configures // the deployment details: which container image, how many replicas, // port mappings, and routing. This is the equivalent of writing // deployment.yaml + service.yaml + httproute.yaml by hand. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); DeploymentSpec spec = new( Image: "registry.example.com/api", Tag: "v1.2.3", Replicas: 2, ContainerPort: 8080, ServicePort: 80, HostName: "api.dev.example.com", PathPrefix: "/", EnvironmentVariables: new Dictionary { ["ASPNETCORE_ENVIRONMENT"] = "Development" }, Resources: new ResourceSpec("100m", "500m", "128Mi", "512Mi")); // Act env.ConfigureDeployment(spec); // Assert env.DeploymentSpec.Should().NotBeNull(); env.DeploymentSpec!.Image.Should().Be("registry.example.com/api"); env.DeploymentSpec.Tag.Should().Be("v1.2.3"); env.DeploymentSpec.Replicas.Should().Be(2); env.DeploymentSpec.ContainerPort.Should().Be(8080); env.DeploymentSpec.ServicePort.Should().Be(80); env.DeploymentSpec.HostName.Should().Be("api.dev.example.com"); env.DeploymentSpec.Resources.Should().NotBeNull(); env.SyncStatus.Should().Be(AppSyncStatus.Pending, "changing config resets sync to pending"); } // ─── ConfigureHelmRelease ──────────────────────────────────────────── [Fact] public void ConfigureHelmRelease_WithValidSpec_SetsHelmSpec() { // Arrange — For a Helm-type app, the admin points to a Helm chart // repository, selects the chart and version, and provides a values.yaml. // This is the equivalent of `helm install` with custom values. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "Redis", "redis", AppType.HelmChart); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "redis-dev"); HelmReleaseSpec spec = new( RepoUrl: "https://charts.bitnami.com/bitnami", ChartName: "redis", ChartVersion: "18.6.1", ValuesYaml: "replica:\n replicaCount: 3"); // Act env.ConfigureHelmRelease(spec); // Assert env.HelmReleaseSpec.Should().NotBeNull(); env.HelmReleaseSpec!.RepoUrl.Should().Be("https://charts.bitnami.com/bitnami"); env.HelmReleaseSpec.ChartName.Should().Be("redis"); env.HelmReleaseSpec.ChartVersion.Should().Be("18.6.1"); env.HelmReleaseSpec.ValuesYaml.Should().Contain("replicaCount: 3"); env.SyncStatus.Should().Be(AppSyncStatus.Pending); } // ─── Secrets ───────────────────────────────────────────────────────── [Fact] public void AddSecret_WithValidInputs_AddsSecretReference() { // Arrange — Secrets for an app environment are stored in the vault. // Here we add a reference that maps a Kubernetes secret key name // to the vault path where the actual secret value lives. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); // Act env.AddSecret("DATABASE_URL", "database-connection-string"); // Assert env.Secrets.Should().HaveCount(1); env.Secrets[0].Name.Should().Be("DATABASE_URL"); env.Secrets[0].VaultKey.Should().Be("database-connection-string"); env.SyncStatus.Should().Be(AppSyncStatus.Pending, "adding a secret resets sync"); } [Fact] public void AddSecret_DuplicateName_ThrowsInvalidOperation() { // Arrange — Each secret name must be unique within an environment. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); env.AddSecret("DATABASE_URL", "db-conn-string"); // Act Action act = () => env.AddSecret("DATABASE_URL", "another-path"); // Assert act.Should().Throw() .WithMessage("*already exists*"); } [Fact] public void RemoveSecret_ExistingSecret_RemovesIt() { // Arrange App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); env.AddSecret("DATABASE_URL", "db-conn-string"); // Act env.RemoveSecret("DATABASE_URL"); // Assert env.Secrets.Should().BeEmpty(); env.SyncStatus.Should().Be(AppSyncStatus.Pending); } // ─── Sync Status ───────────────────────────────────────────────────── [Fact] public void MarkSynced_UpdatesSyncStatus() { // Arrange — The reconciler successfully deployed the app. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); // Act env.MarkSynced(); // Assert env.SyncStatus.Should().Be(AppSyncStatus.Synced); env.LastSyncAt.Should().NotBeNull(); } [Fact] public void MarkError_SetsSyncStatusAndMessage() { // Arrange — Something went wrong during deployment. App app = App.Create(Guid.NewGuid(), Guid.NewGuid(), "API", "api", AppType.Deployment); AppEnvironment env = app.AddEnvironment(Guid.NewGuid(), Guid.NewGuid(), "api-dev"); // Act env.MarkError("ImagePullBackOff: registry.example.com/api:v1.2.3"); // Assert env.SyncStatus.Should().Be(AppSyncStatus.Error); env.SyncStatusMessage.Should().Contain("ImagePullBackOff"); } }