using EntKube.Provisioning.Domain; using EntKube.Provisioning.Features.Apps.Reconcile; using FluentAssertions; namespace EntKube.Provisioning.Tests.Features; public class ManifestGeneratorTests { // ─── Deployment Manifest ───────────────────────────────────────────── [Fact] public void GenerateDeployment_WithFullSpec_ProducesValidYaml() { // Arrange — A fully configured deployment spec for an API service. // The generator should produce a Kubernetes Deployment manifest with // the container image, port, replicas, resource limits, and env vars. DeploymentSpec spec = new( Image: "registry.example.com/api", Tag: "v1.2.3", Replicas: 3, ContainerPort: 8080, ServicePort: 80, HostName: "api.dev.example.com", PathPrefix: "/api", EnvironmentVariables: new Dictionary { ["ASPNETCORE_ENVIRONMENT"] = "Development", ["LOG_LEVEL"] = "Debug" }, Resources: new ResourceSpec("100m", "500m", "128Mi", "512Mi")); // Act string yaml = ManifestGenerator.GenerateDeployment("my-api", "api-dev", spec); // Assert — The YAML should contain the key Kubernetes Deployment fields. yaml.Should().Contain("kind: Deployment"); yaml.Should().Contain("name: my-api"); yaml.Should().Contain("namespace: api-dev"); yaml.Should().Contain("replicas: 3"); yaml.Should().Contain("registry.example.com/api:v1.2.3"); yaml.Should().Contain("containerPort: 8080"); yaml.Should().Contain("cpu: \"100m\""); yaml.Should().Contain("cpu: \"500m\""); yaml.Should().Contain("memory: \"128Mi\""); yaml.Should().Contain("memory: \"512Mi\""); yaml.Should().Contain("ASPNETCORE_ENVIRONMENT"); yaml.Should().Contain("Development"); } [Fact] public void GenerateDeployment_WithoutResources_OmitsResourceBlock() { // Arrange — A minimal deployment without resource constraints. DeploymentSpec spec = new( Image: "nginx", Tag: "latest", Replicas: 1, ContainerPort: 80, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); // Act string yaml = ManifestGenerator.GenerateDeployment("web", "default", spec); // Assert yaml.Should().Contain("kind: Deployment"); yaml.Should().Contain("nginx:latest"); yaml.Should().NotContain("resources:"); } // ─── Service Manifest ──────────────────────────────────────────────── [Fact] public void GenerateService_WithSpec_ProducesClusterIPService() { // Arrange — The generator creates a ClusterIP Service that maps // the service port to the container port, matching pod labels. DeploymentSpec spec = new( Image: "registry.example.com/api", Tag: "v1.0.0", Replicas: 2, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); // Act string yaml = ManifestGenerator.GenerateService("my-api", "api-dev", spec); // Assert yaml.Should().Contain("kind: Service"); yaml.Should().Contain("name: my-api"); yaml.Should().Contain("namespace: api-dev"); yaml.Should().Contain("port: 80"); yaml.Should().Contain("targetPort: 8080"); yaml.Should().Contain("app: my-api"); } // ─── HTTPRoute Manifest ────────────────────────────────────────────── [Fact] public void GenerateHttpRoute_WithHostAndPath_ProducesRouteYaml() { // Arrange — When a hostname and path prefix are provided, // we generate a Gateway API HTTPRoute resource. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: "api.dev.example.com", PathPrefix: "/api", EnvironmentVariables: null, Resources: null); // Act string yaml = ManifestGenerator.GenerateHttpRoute("my-api", "api-dev", spec); // Assert yaml.Should().Contain("kind: HTTPRoute"); yaml.Should().Contain("name: my-api"); yaml.Should().Contain("namespace: api-dev"); yaml.Should().Contain("api.dev.example.com"); yaml.Should().Contain("/api"); yaml.Should().Contain("port: 80"); } [Fact] public void GenerateHttpRoute_WithoutHost_ReturnsEmpty() { // Arrange — No hostname means no ingress routing needed. DeploymentSpec spec = new( Image: "worker", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); // Act string yaml = ManifestGenerator.GenerateHttpRoute("worker", "default", spec); // Assert yaml.Should().BeEmpty(); } // ─── Secrets Manifest ──────────────────────────────────────────────── [Fact] public void GenerateSecret_WithEntries_ProducesOpaqueSecret() { // Arrange — Secret values resolved from the vault become a // Kubernetes Opaque Secret with base64-encoded data. Dictionary resolvedSecrets = new() { ["DATABASE_URL"] = "postgresql://db:5432/mydb", ["API_KEY"] = "sk-abc123" }; // Act string yaml = ManifestGenerator.GenerateSecret("my-api", "api-dev", resolvedSecrets); // Assert yaml.Should().Contain("kind: Secret"); yaml.Should().Contain("name: my-api"); yaml.Should().Contain("namespace: api-dev"); yaml.Should().Contain("type: Opaque"); yaml.Should().Contain("DATABASE_URL:"); yaml.Should().Contain("API_KEY:"); } [Fact] public void GenerateSecret_Empty_ReturnsEmpty() { // Arrange — No secrets means no Secret resource needed. Dictionary empty = new(); // Act string yaml = ManifestGenerator.GenerateSecret("my-api", "default", empty); // Assert yaml.Should().BeEmpty(); } // ─── Environment Variable Injection ────────────────────────────────── [Fact] public void GenerateDeployment_WithSecrets_InjectsSecretKeyRefs() { // Arrange — When an app has secrets, the deployment should reference // them via secretKeyRef so the container gets environment variables // populated from the Kubernetes Secret resource. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); List secrets = new() { new AppSecret("DATABASE_URL", "db-conn-string"), new AppSecret("API_KEY", "api-key") }; // Act string yaml = ManifestGenerator.GenerateDeployment("my-api", "api-dev", spec, secrets); // Assert — Should have secretKeyRef entries for each secret. yaml.Should().Contain("secretKeyRef:"); yaml.Should().Contain("name: DATABASE_URL"); yaml.Should().Contain("key: DATABASE_URL"); yaml.Should().Contain("name: API_KEY"); } // ─── Volume Mounts ────────────────────────────────────────────────── [Fact] public void GenerateDeployment_WithVolumes_ProducesVolumeMountAndVolumeBlocks() { // Arrange — An app that needs persistent storage mounts a PVC // into the container at a specific path. The manifest should include // both a volumeMount on the container and a volume on the pod spec. DeploymentSpec spec = new( Image: "postgres", Tag: "16", Replicas: 1, ContainerPort: 5432, ServicePort: 5432, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: new List { new("data", new VolumePvcSource("postgres-data-pvc"), MountPath: "/var/lib/postgresql/data"), new("config", new VolumeConfigMapSource("postgres-config"), MountPath: "/etc/postgresql") }, Containers: null, SecurityContext: null, ServiceAccountName: null, Annotations: null, Labels: null, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("db", "data-ns", spec); // Assert — The YAML should have volume mounts on the container // and volume definitions on the pod spec. yaml.Should().Contain("volumeMounts:"); yaml.Should().Contain("name: data"); yaml.Should().Contain("mountPath: \"/var/lib/postgresql/data\""); yaml.Should().Contain("persistentVolumeClaim:"); yaml.Should().Contain("claimName: postgres-data-pvc"); yaml.Should().Contain("configMap:"); yaml.Should().Contain("name: postgres-config"); } // ─── Security Context ─────────────────────────────────────────────── [Fact] public void GenerateDeployment_WithSecurityContext_ProducesSecurityBlocks() { // Arrange — For hardened workloads, the deployment needs pod-level // and container-level security context settings: non-root user, // read-only root filesystem, dropped capabilities. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: null, SecurityContext: new PodSecurityContextSpec( RunAsUser: 1000, RunAsGroup: 1000, FsGroup: 2000, RunAsNonRoot: true), ServiceAccountName: null, Annotations: null, Labels: null, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("secure-api", "prod", spec); // Assert yaml.Should().Contain("securityContext:"); yaml.Should().Contain("runAsUser: 1000"); yaml.Should().Contain("runAsGroup: 1000"); yaml.Should().Contain("fsGroup: 2000"); yaml.Should().Contain("runAsNonRoot: true"); } // ─── Multiple Containers (Init + Sidecar) ─────────────────────────── [Fact] public void GenerateDeployment_WithAdditionalContainers_ProducesMultiContainerPod() { // Arrange — Some workloads need init containers (e.g. DB migrations) // and sidecar containers (e.g. proxy, log shipper). The spec supports // adding extra containers beyond the primary one. DeploymentSpec spec = new( Image: "api", Tag: "v2", Replicas: 2, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: new AdditionalContainersSpec( InitContainers: new List { new("migrate", "api", "v2", 0, new[] { "dotnet", "ef", "database", "update" }.ToList(), null, null, null, null, null) }, Sidecars: new List { new("log-shipper", "fluent/fluent-bit", "latest", 0, null, null, null, null, null, null) }), SecurityContext: null, ServiceAccountName: null, Annotations: null, Labels: null, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("my-api", "dev", spec); // Assert — Should have initContainers and sidecar containers. yaml.Should().Contain("initContainers:"); yaml.Should().Contain("name: migrate"); yaml.Should().Contain("command:"); yaml.Should().Contain("name: log-shipper"); yaml.Should().Contain("fluent/fluent-bit:latest"); } // ─── Probes ───────────────────────────────────────────────────────── [Fact] public void GenerateDeployment_WithProbes_ProducesProbeBlocks() { // Arrange — Kubernetes uses probes to determine container health. // A liveness probe restarts unhealthy containers; a readiness probe // removes them from service endpoints until they're ready. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: null, SecurityContext: null, ServiceAccountName: null, Annotations: null, Labels: null, Probes: new ProbesSpec( Liveness: new ProbeSpec("HTTP", "/healthz", 8080, 15, 20, 3), Readiness: new ProbeSpec("HTTP", "/ready", 8080, 5, 10, 1), Startup: new ProbeSpec("HTTP", "/healthz", 8080, 0, 5, 30)), ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("api", "prod", spec); // Assert yaml.Should().Contain("livenessProbe:"); yaml.Should().Contain("path: /healthz"); yaml.Should().Contain("readinessProbe:"); yaml.Should().Contain("path: /ready"); yaml.Should().Contain("startupProbe:"); } // ─── ConfigMaps ───────────────────────────────────────────────────── [Fact] public void GenerateConfigMap_WithEntries_ProducesConfigMapYaml() { // Arrange — Apps may need ConfigMap resources for configuration files // or simple key-value settings. The generator creates a ConfigMap // manifest from structured data. ConfigMapSpec configMap = new( "app-config", new Dictionary { ["appsettings.json"] = "{\"Logging\":{\"LogLevel\":{\"Default\":\"Information\"}}}", ["feature-flags.json"] = "{\"enableBeta\":true}" }); // Act string yaml = ManifestGenerator.GenerateConfigMap("my-api", "dev", configMap); // Assert yaml.Should().Contain("kind: ConfigMap"); yaml.Should().Contain("name: app-config"); yaml.Should().Contain("namespace: dev"); yaml.Should().Contain("appsettings.json:"); yaml.Should().Contain("feature-flags.json:"); } // ─── Service Account ──────────────────────────────────────────────── [Fact] public void GenerateDeployment_WithServiceAccount_IncludesServiceAccountName() { // Arrange — When a workload needs to access the Kubernetes API // or cloud resources, it runs under a specific service account. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: null, SecurityContext: null, ServiceAccountName: "api-service-account", Annotations: null, Labels: null, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("api", "prod", spec); // Assert yaml.Should().Contain("serviceAccountName: api-service-account"); } // ─── Annotations & Labels ─────────────────────────────────────────── [Fact] public void GenerateDeployment_WithAnnotationsAndLabels_IncludesMetadata() { // Arrange — Teams need custom annotations (e.g., Prometheus scrape config, // Istio injection) and labels (e.g., team ownership, cost center). DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: null, SecurityContext: null, ServiceAccountName: null, Annotations: new Dictionary { ["prometheus.io/scrape"] = "true", ["prometheus.io/port"] = "8080" }, Labels: new Dictionary { ["team"] = "platform", ["cost-center"] = "engineering" }, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("api", "prod", spec); // Assert yaml.Should().Contain("prometheus.io/scrape: \"true\""); yaml.Should().Contain("prometheus.io/port: \"8080\""); yaml.Should().Contain("team: platform"); yaml.Should().Contain("cost-center: engineering"); } // ─── Backward Compatibility ───────────────────────────────────────── [Fact] public void GenerateDeployment_MinimalSpecWithNewNullFields_StillWorks() { // Arrange — The existing minimal spec (with all new fields null/empty) // should still produce the same output as before, ensuring backward // compatibility for existing apps. DeploymentSpec spec = new( Image: "nginx", Tag: "latest", Replicas: 1, ContainerPort: 80, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, Volumes: null, Containers: null, SecurityContext: null, ServiceAccountName: null, Annotations: null, Labels: null, Probes: null, ConfigMaps: null); // Act string yaml = ManifestGenerator.GenerateDeployment("web", "default", spec); // Assert — Should produce a basic deployment without any of the // new blocks (no volumes, no security context, no probes, etc.) yaml.Should().Contain("kind: Deployment"); yaml.Should().Contain("nginx:latest"); yaml.Should().NotContain("volumeMounts:"); yaml.Should().NotContain("securityContext:"); yaml.Should().NotContain("initContainers:"); yaml.Should().NotContain("livenessProbe:"); yaml.Should().NotContain("serviceAccountName:"); } // ─── Multiple Services ────────────────────────────────────────────── [Fact] public void GenerateServices_WithMultipleSpecs_ProducesOneServicePerSpec() { // Arrange — An app might expose both an HTTP API on port 80 and a gRPC // endpoint on port 9090, each needing its own Kubernetes Service. // The services list replaces the old single ServicePort/ContainerPort pair. List services = new() { new ServiceSpec("my-api-http", 80, 8080, "TCP", "ClusterIP"), new ServiceSpec("my-api-grpc", 9090, 9090, "TCP", "ClusterIP") }; // Act List yamls = services.Select(s => ManifestGenerator.GenerateService("my-api", "api-dev", s)).ToList(); // Assert — Each service gets its own YAML with the correct ports. yamls.Should().HaveCount(2); yamls[0].Should().Contain("name: my-api-http"); yamls[0].Should().Contain("port: 80"); yamls[0].Should().Contain("targetPort: 8080"); yamls[1].Should().Contain("name: my-api-grpc"); yamls[1].Should().Contain("port: 9090"); } [Fact] public void GenerateService_WithUdpProtocol_SetsProtocolField() { // Arrange — A DNS or game server might need a UDP service. ServiceSpec svc = new("dns-server", 53, 5353, "UDP", "ClusterIP"); // Act string yaml = ManifestGenerator.GenerateService("dns", "infra", svc); // Assert yaml.Should().Contain("protocol: UDP"); yaml.Should().Contain("port: 53"); yaml.Should().Contain("targetPort: 5353"); } [Fact] public void GenerateService_WithNodePortType_SetsServiceType() { // Arrange — Some services need NodePort or LoadBalancer exposure. ServiceSpec svc = new("web-public", 80, 8080, "TCP", "NodePort"); // Act string yaml = ManifestGenerator.GenerateService("web", "prod", svc); // Assert yaml.Should().Contain("type: NodePort"); } // ─── Gateway API Routes ───────────────────────────────────────────── [Fact] public void GenerateRoute_HttpRoute_WithMatchesAndFilters() { // Arrange — An HTTPRoute with multiple path matches, header matching, // and a gateway reference. This is the full-featured version of the // old GenerateHttpRoute that only supported hostname + path prefix. RouteSpec route = new( Name: "api-route", Type: "HTTP", Hostnames: new List { "api.example.com", "api.internal.example.com" }, GatewayRef: new GatewayReference("main-gateway", "gateway-ns"), Rules: new List { new( Matches: new List { new("PathPrefix", "/api/v1", null), new("PathPrefix", "/api/v2", null) }, BackendRefs: new List { new("my-api-http", 80, null) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("my-api", "api-dev", route); // Assert yaml.Should().Contain("kind: HTTPRoute"); yaml.Should().Contain("api.example.com"); yaml.Should().Contain("api.internal.example.com"); yaml.Should().Contain("name: main-gateway"); yaml.Should().Contain("namespace: gateway-ns"); yaml.Should().Contain("/api/v1"); yaml.Should().Contain("/api/v2"); yaml.Should().Contain("name: my-api-http"); yaml.Should().Contain("port: 80"); } [Fact] public void GenerateRoute_TlsRoute_ProducesTLSRouteManifest() { // Arrange — A TLSRoute performs TLS passthrough routing. The Gateway // terminates at the SNI hostname level and passes the encrypted // connection directly to the backend — useful for services that // handle their own TLS (databases, MQTT brokers, etc.) RouteSpec route = new( Name: "db-tls-route", Type: "TLS", Hostnames: new List { "db.secure.example.com" }, GatewayRef: new GatewayReference("tls-gateway", "gateway-ns"), Rules: new List { new( Matches: null, BackendRefs: new List { new("db-service", 5432, null) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("db-app", "data-ns", route); // Assert yaml.Should().Contain("kind: TLSRoute"); yaml.Should().Contain("apiVersion: gateway.networking.k8s.io/v1alpha2"); yaml.Should().Contain("db.secure.example.com"); yaml.Should().Contain("name: db-service"); yaml.Should().Contain("port: 5432"); } [Fact] public void GenerateRoute_TcpRoute_ProducesTCPRouteManifest() { // Arrange — A TCPRoute routes raw TCP traffic. No hostname matching — // it's port-based. Used for databases, Redis, custom TCP protocols. RouteSpec route = new( Name: "redis-route", Type: "TCP", Hostnames: null, GatewayRef: new GatewayReference("tcp-gateway", "gateway-ns"), Rules: new List { new( Matches: null, BackendRefs: new List { new("redis-svc", 6379, null) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("redis", "cache-ns", route); // Assert yaml.Should().Contain("kind: TCPRoute"); yaml.Should().Contain("apiVersion: gateway.networking.k8s.io/v1alpha2"); yaml.Should().Contain("name: redis-svc"); yaml.Should().Contain("port: 6379"); yaml.Should().NotContain("hostnames:"); } [Fact] public void GenerateRoute_UdpRoute_ProducesUDPRouteManifest() { // Arrange — A UDPRoute for services that use UDP: DNS servers, // game servers, VoIP, etc. RouteSpec route = new( Name: "dns-route", Type: "UDP", Hostnames: null, GatewayRef: new GatewayReference("udp-gateway", "gateway-ns"), Rules: new List { new( Matches: null, BackendRefs: new List { new("dns-svc", 53, null) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("dns", "infra", route); // Assert yaml.Should().Contain("kind: UDPRoute"); yaml.Should().Contain("apiVersion: gateway.networking.k8s.io/v1alpha2"); yaml.Should().Contain("name: dns-svc"); yaml.Should().Contain("port: 53"); } [Fact] public void GenerateRoute_GrpcRoute_ProducesGRPCRouteManifest() { // Arrange — A GRPCRoute for gRPC services. Uses method matching // instead of path matching. RouteSpec route = new( Name: "grpc-route", Type: "GRPC", Hostnames: new List { "grpc.example.com" }, GatewayRef: new GatewayReference("main-gateway", "gateway-ns"), Rules: new List { new( Matches: null, BackendRefs: new List { new("grpc-svc", 9090, null) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("grpc-app", "api-ns", route); // Assert yaml.Should().Contain("kind: GRPCRoute"); yaml.Should().Contain("grpc.example.com"); yaml.Should().Contain("port: 9090"); } [Fact] public void GenerateRoute_HttpRouteWithFilters_IncludesFilterBlocks() { // Arrange — HTTPRoute filters can add/remove headers, redirect, // or rewrite URLs. This tests header modification filters. RouteSpec route = new( Name: "api-route", Type: "HTTP", Hostnames: new List { "api.example.com" }, GatewayRef: null, Rules: new List { new( Matches: new List { new("PathPrefix", "/api", null) }, BackendRefs: new List { new("api-svc", 80, null) }, Filters: new List { new("RequestHeaderModifier", new Dictionary { ["X-Forwarded-Proto"] = "https" }, null, null) }) }); // Act string yaml = ManifestGenerator.GenerateRoute("api", "prod", route); // Assert yaml.Should().Contain("RequestHeaderModifier"); yaml.Should().Contain("X-Forwarded-Proto"); yaml.Should().Contain("https"); } [Fact] public void GenerateRoute_HttpRouteWithWeightedBackends_IncludesWeight() { // Arrange — Canary deployments split traffic between backends using weights. RouteSpec route = new( Name: "canary-route", Type: "HTTP", Hostnames: new List { "app.example.com" }, GatewayRef: null, Rules: new List { new( Matches: new List { new("PathPrefix", "/", null) }, BackendRefs: new List { new("app-stable", 80, 90), new("app-canary", 80, 10) }, Filters: null) }); // Act string yaml = ManifestGenerator.GenerateRoute("app", "prod", route); // Assert yaml.Should().Contain("name: app-stable"); yaml.Should().Contain("weight: 90"); yaml.Should().Contain("name: app-canary"); yaml.Should().Contain("weight: 10"); } // ─── Legacy Service/Route backward compatibility ──────────────────── [Fact] public void GenerateService_FromDeploymentSpec_StillWorksForBackwardCompat() { // Arrange — The old single-service path using DeploymentSpec's // ServicePort/ContainerPort still works when no Services list is provided. DeploymentSpec spec = new( Image: "api", Tag: "v1", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); // Act — The old overload should still exist. string yaml = ManifestGenerator.GenerateService("api", "dev", spec); // Assert yaml.Should().Contain("kind: Service"); yaml.Should().Contain("port: 80"); yaml.Should().Contain("targetPort: 8080"); } // ─── Namespace Manifest ────────────────────────────────────────────── [Fact] public void GenerateNamespace_ProducesValidNamespaceManifest() { // Act string yaml = ManifestGenerator.GenerateNamespace("my-app-dev"); // Assert yaml.Should().Contain("kind: Namespace"); yaml.Should().Contain("name: my-app-dev"); yaml.Should().Contain("app.kubernetes.io/managed-by: entkube"); } // ─── ServiceAccount Manifest ───────────────────────────────────────── [Fact] public void GenerateServiceAccount_ProducesValidManifest() { // Act string yaml = ManifestGenerator.GenerateServiceAccount("my-sa", "my-ns"); // Assert yaml.Should().Contain("kind: ServiceAccount"); yaml.Should().Contain("name: my-sa"); yaml.Should().Contain("namespace: my-ns"); } // ─── PersistentVolumeClaim Manifest ────────────────────────────────── [Fact] public void GeneratePersistentVolumeClaim_ProducesValidManifest() { // Act string yaml = ManifestGenerator.GeneratePersistentVolumeClaim("data-pvc", "my-ns", "5Gi"); // Assert yaml.Should().Contain("kind: PersistentVolumeClaim"); yaml.Should().Contain("name: data-pvc"); yaml.Should().Contain("namespace: my-ns"); yaml.Should().Contain("storage: 5Gi"); yaml.Should().Contain("ReadWriteOnce"); } [Fact] public void GeneratePersistentVolumeClaim_WithStorageClass_IncludesIt() { // Act string yaml = ManifestGenerator.GeneratePersistentVolumeClaim( "data-pvc", "my-ns", "10Gi", "ReadWriteMany", "fast-ssd"); // Assert yaml.Should().Contain("storageClassName: fast-ssd"); yaml.Should().Contain("ReadWriteMany"); } // ─── Image Pull Secrets ────────────────────────────────────────────── [Fact] public void GenerateDeployment_WithHarborRegistry_IncludesImagePullSecrets() { // Arrange — When an app uses Harbor as its image registry, the // generated deployment should include an imagePullSecrets reference // so Kubernetes can authenticate against the private registry. DeploymentSpec spec = new( Image: "harbor.example.com/myproject/api", Tag: "v1.0.0", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, ImageRegistry: new ImageRegistryConfig( ImageRegistrySource.Harbor, HarborDomain: "harbor.example.com", HarborProject: "myproject", PullSecretName: null)); // Act string yaml = ManifestGenerator.GenerateDeployment("my-api", "dev", spec); // Assert — The YAML should contain the auto-generated pull secret name. yaml.Should().Contain("imagePullSecrets:"); yaml.Should().Contain("harbor-pull-myproject-my-api"); } [Fact] public void GenerateDeployment_WithCustomRegistry_IncludesSpecifiedPullSecret() { // Arrange — A custom registry with a manually specified pull secret name. DeploymentSpec spec = new( Image: "ghcr.io/org/api", Tag: "latest", Replicas: 1, ContainerPort: 8080, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null, ImageRegistry: new ImageRegistryConfig( ImageRegistrySource.Custom, HarborDomain: null, HarborProject: null, PullSecretName: "my-ghcr-secret")); // Act string yaml = ManifestGenerator.GenerateDeployment("api", "prod", spec); // Assert yaml.Should().Contain("imagePullSecrets:"); yaml.Should().Contain("my-ghcr-secret"); } [Fact] public void GenerateDeployment_WithPublicRegistry_OmitsImagePullSecrets() { // Arrange — A public registry needs no pull secret. DeploymentSpec spec = new( Image: "nginx", Tag: "latest", Replicas: 1, ContainerPort: 80, ServicePort: 80, HostName: null, PathPrefix: null, EnvironmentVariables: null, Resources: null); // Act string yaml = ManifestGenerator.GenerateDeployment("web", "default", spec); // Assert — No imagePullSecrets section should appear. yaml.Should().NotContain("imagePullSecrets:"); } }