using EntKube.Clusters.Domain; using EntKube.Clusters.Features.AdoptCluster; using EntKube.Clusters.Features.AdoptCluster.Components; using EntKube.Clusters.Features.AdoptCluster.ConfigureComponent; using EntKube.Clusters.Features.AdoptCluster.DeployComponent; using EntKube.Clusters.Infrastructure; using EntKube.SharedKernel.Domain; using FluentAssertions; using k8s.Models; using Moq; namespace EntKube.Clusters.Tests.Features; /// /// Tests for the Harbor container registry component. Harbor provides: /// /// 1. **Container registry** — hosting private container images /// 2. **Helm chart repository** — hosting Helm charts via OCI /// 3. **Pull-through cache** — caching upstream registries (Docker Hub, ghcr.io, quay.io, etc.) /// /// The pull-through cache is especially valuable: all services and tenant apps /// pull images through Harbor, gaining caching, vulnerability scanning, and /// network isolation from public registries. /// /// Dependency chain: ingress (for Harbor UI/API) → cert-manager (TLS) → Harbor /// public class HarborTests { private readonly InMemoryClusterRepository repository; private readonly Mock harborInstaller; public HarborTests() { repository = new InMemoryClusterRepository(); harborInstaller = new Mock(); harborInstaller.Setup(i => i.ComponentName).Returns("harbor"); } // ─── Harbor Installation ────────────────────────────────────────────────── [Fact] public async Task Harbor_Install_DeploysRegistryWithPullThroughCache() { // Arrange — We want a full Harbor deployment with pull-through proxy cache // enabled for major upstream registries. This gives us a local mirror that // speeds up pulls and survives upstream outages. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Harbor 2.12.0 installed with pull-through cache for docker.io, ghcr.io, quay.io", Actions: new List { "Ensured namespace 'harbor'", "Helm install harbor v2.12.0", "Created proxy cache project 'dockerhub-cache' → docker.io", "Created proxy cache project 'ghcr-cache' → ghcr.io", "Created proxy cache project 'quay-cache' → quay.io", "Harbor registry available at registry.internal.corp.com" })); DeployComponentHandler handler = new(repository, new List { harborInstaller.Object }, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); DeployComponentRequest request = new("harbor", Version: "2.12.0", Parameters: new Dictionary { ["harborDomain"] = "registry.internal.corp.com", ["adminPassword"] = "Harbor12345", ["storageClass"] = "longhorn", ["storageSize"] = "100Gi", ["cacheRegistries"] = "docker.io,ghcr.io,quay.io", ["enableChartMuseum"] = "true" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — Harbor deployed with proxy cache projects for each registry. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("Helm install harbor")); result.Value.Actions.Should().Contain(a => a.Contains("dockerhub-cache")); result.Value.Actions.Should().Contain(a => a.Contains("ghcr-cache")); result.Value.Actions.Should().Contain(a => a.Contains("quay-cache")); result.Value.Actions.Should().Contain(a => a.Contains("registry.internal.corp.com")); harborInstaller.Verify(i => i.InstallAsync( cluster, It.Is(o => o.Parameters!["harborDomain"] == "registry.internal.corp.com" && o.Parameters["cacheRegistries"] == "docker.io,ghcr.io,quay.io"), It.IsAny()), Times.Once); } [Fact] public async Task Harbor_Install_WithTLS_UsesClusterIssuerForCertificate() { // Arrange — Harbor should use a cert-manager ClusterIssuer to auto-provision // TLS certificates. This integrates with the internal CA or domain CA // we set up earlier. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Harbor 2.12.0 installed with TLS via ClusterIssuer 'platform-internal-ca'", Actions: new List { "Ensured namespace 'harbor'", "Helm install harbor v2.12.0", "Configured TLS via ClusterIssuer 'platform-internal-ca'", "Created Ingress for registry.internal.corp.com with TLS", "Harbor registry available at registry.internal.corp.com" })); DeployComponentHandler handler = new(repository, new List { harborInstaller.Object }, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); DeployComponentRequest request = new("harbor", Version: "2.12.0", Parameters: new Dictionary { ["harborDomain"] = "registry.internal.corp.com", ["adminPassword"] = "Harbor12345", ["storageClass"] = "longhorn", ["storageSize"] = "50Gi", ["tlsIssuer"] = "platform-internal-ca", ["enableTLS"] = "true" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — TLS configured via cert-manager integration. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("TLS via ClusterIssuer")); result.Value.Actions.Should().Contain(a => a.Contains("Ingress")); } [Fact] public async Task Harbor_Install_WithoutPrerequisites_ReportsFailure() { // Arrange — Harbor requires ingress to be accessible. If the cluster // has no ingress controller, the install should fail gracefully. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: false, ComponentName: "harbor", Message: "Harbor requires an ingress controller but none was detected", Actions: new List { "Prerequisite check failed: no ingress controller found" })); DeployComponentHandler handler = new(repository, new List { harborInstaller.Object }, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); DeployComponentRequest request = new("harbor", Parameters: new Dictionary { ["harborDomain"] = "registry.internal.corp.com", ["adminPassword"] = "Harbor12345" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — Prerequisite failure reported. result.IsFailure.Should().BeTrue(); result.Error.Should().Contain("ingress"); } // ─── Harbor Configuration ───────────────────────────────────────────────── [Fact] public async Task Harbor_Configure_AddProxyCacheRegistry() { // Arrange — Harbor is running. We want to add a new upstream registry // as a proxy cache (e.g., registry.k8s.io for Kubernetes images). KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Proxy cache project added for registry.k8s.io", Actions: new List { "Created proxy cache project 'k8s-registry-cache' → registry.k8s.io", "Proxy cache now covers: docker.io, ghcr.io, quay.io, registry.k8s.io" })); List installers = new() { harborInstaller.Object }; ConfigureComponentHandler configHandler = new(repository, installers); ConfigureComponentRequest configRequest = new( ComponentName: "harbor", Values: new Dictionary { ["addProxyCache"] = "registry.k8s.io", ["proxyCacheProjectName"] = "k8s-registry-cache" }); // Act Result result = await configHandler.HandleAsync(cluster.Id, configRequest); // Assert — New proxy cache project created. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("registry.k8s.io")); result.Value.Actions.Should().Contain(a => a.Contains("k8s-registry-cache")); } [Fact] public async Task Harbor_Configure_SetAsDefaultPullThroughCache() { // Arrange — We want to configure the cluster so all pod image pulls // go through Harbor as a mirror/cache. This typically involves configuring // containerd mirror settings or using Kyverno to mutate image references. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Cluster configured to use Harbor as default pull-through cache", Actions: new List { "Created Kyverno ClusterPolicy 'harbor-image-mirror' to rewrite image references", "Policy rewrites docker.io/* → registry.internal.corp.com/dockerhub-cache/*", "Policy rewrites ghcr.io/* → registry.internal.corp.com/ghcr-cache/*", "Policy rewrites quay.io/* → registry.internal.corp.com/quay-cache/*" })); List installers = new() { harborInstaller.Object }; ConfigureComponentHandler configHandler = new(repository, installers); ConfigureComponentRequest configRequest = new( ComponentName: "harbor", Values: new Dictionary { ["setDefaultCache"] = "true", ["harborDomain"] = "registry.internal.corp.com", ["cacheStrategy"] = "kyverno-rewrite" }); // Act Result result = await configHandler.HandleAsync(cluster.Id, configRequest); // Assert — Kyverno policy created to rewrite image refs through Harbor. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("ClusterPolicy")); result.Value.Actions.Should().Contain(a => a.Contains("docker.io")); result.Value.Actions.Should().Contain(a => a.Contains("ghcr.io")); } [Fact] public async Task Harbor_Configure_CreatePrivateProject() { // Arrange — Create a private project in Harbor for tenant images. // Each tenant gets their own project with isolation. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Private project 'tenant-alpha' created", Actions: new List { "Created private project 'tenant-alpha'", "Created robot account 'robot$tenant-alpha-pull' with pull access", "Created image pull Secret 'harbor-pull-tenant-alpha' in namespace 'tenant-alpha'" })); List installers = new() { harborInstaller.Object }; ConfigureComponentHandler configHandler = new(repository, installers); ConfigureComponentRequest configRequest = new( ComponentName: "harbor", Values: new Dictionary { ["createProject"] = "tenant-alpha", ["projectVisibility"] = "private", ["createPullSecret"] = "true", ["targetNamespace"] = "tenant-alpha" }); // Act Result result = await configHandler.HandleAsync(cluster.Id, configRequest); // Assert — Project, robot account, and pull secret created. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("private project 'tenant-alpha'")); result.Value.Actions.Should().Contain(a => a.Contains("robot account")); result.Value.Actions.Should().Contain(a => a.Contains("pull Secret")); } [Fact] public async Task Harbor_Configure_EnableVulnerabilityScanning() { // Arrange — Enable automatic vulnerability scanning on all pushed images. // Harbor uses Trivy for scanning. We want to configure the scan policy. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Vulnerability scanning configured", Actions: new List { "Enabled automatic scan on push for all projects", "Set vulnerability severity threshold to 'High'", "Configured scan schedule: daily at 02:00 UTC" })); List installers = new() { harborInstaller.Object }; ConfigureComponentHandler configHandler = new(repository, installers); ConfigureComponentRequest configRequest = new( ComponentName: "harbor", Values: new Dictionary { ["enableScanOnPush"] = "true", ["severityThreshold"] = "High", ["scanSchedule"] = "0 2 * * *" }); // Act Result result = await configHandler.HandleAsync(cluster.Id, configRequest); // Assert — Scanning policies configured. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("scan on push")); result.Value.Actions.Should().Contain(a => a.Contains("severity threshold")); } // ─── Wire All Platform Services ────────────────────────────────────────── [Fact] public async Task Harbor_Configure_WireAllServices_CreatesComprehensiveCacheAndPolicy() { // Arrange — Harbor is running. We want to wire ALL platform services // (MinIO, CloudNativePG, cert-manager, Kyverno, Istio, Grafana, Prometheus, // Redis, RabbitMQ, External Secrets) to pull images through Harbor. // This creates proxy cache projects for every upstream registry used by // our services and a Kyverno policy to rewrite image references. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.ConfigureAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "All platform services wired to pull through Harbor", Actions: new List { "Ensured proxy cache project 'dockerhub-cache' → docker.io (MinIO, Istio, Grafana, Redis, RabbitMQ)", "Ensured proxy cache project 'ghcr-cache' → ghcr.io (CloudNativePG, Kyverno, External Secrets)", "Ensured proxy cache project 'quay-cache' → quay.io (cert-manager, trust-manager, Prometheus)", "Ensured proxy cache project 'k8s-cache' → registry.k8s.io (CoreDNS, metrics-server)", "Ensured proxy cache project 'gcr-cache' → gcr.io (Istio release images)", "Created Kyverno ClusterPolicy 'harbor-image-mirror' to rewrite image references", "Policy rewrites docker.io/* → registry.internal.corp.com/dockerhub-cache/*", "Policy rewrites ghcr.io/* → registry.internal.corp.com/ghcr-cache/*", "Policy rewrites quay.io/* → registry.internal.corp.com/quay-cache/*", "Policy rewrites registry.k8s.io/* → registry.internal.corp.com/k8s-cache/*", "Policy rewrites gcr.io/* → registry.internal.corp.com/gcr-cache/*", "All 12 platform services now pull through Harbor" })); List installers = new() { harborInstaller.Object }; ConfigureComponentHandler configHandler = new(repository, installers); ConfigureComponentRequest configRequest = new( ComponentName: "harbor", Values: new Dictionary { ["wireAllServices"] = "true", ["harborDomain"] = "registry.internal.corp.com" }); // Act Result result = await configHandler.HandleAsync(cluster.Id, configRequest); // Assert — All registries cached and policy covers all platforms. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("docker.io") && a.Contains("MinIO")); result.Value.Actions.Should().Contain(a => a.Contains("ghcr.io") && a.Contains("CloudNativePG")); result.Value.Actions.Should().Contain(a => a.Contains("quay.io") && a.Contains("cert-manager")); result.Value.Actions.Should().Contain(a => a.Contains("registry.k8s.io")); result.Value.Actions.Should().Contain(a => a.Contains("gcr.io")); result.Value.Actions.Should().Contain(a => a.Contains("ClusterPolicy")); result.Value.Actions.Should().Contain(a => a.Contains("12 platform services")); harborInstaller.Verify(i => i.ConfigureAsync( cluster, It.Is(c => c.Values["wireAllServices"] == "true" && c.Values["harborDomain"] == "registry.internal.corp.com"), It.IsAny()), Times.Once); } [Fact] public async Task Harbor_Install_WithAllServices_IncludesAllRegistryCaches() { // Arrange — When installing Harbor with wireAllServices=true, it should // automatically create proxy caches for all 5 upstream registries // needed by platform services, not just the default 3. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Harbor 2.12.0 installed with pull-through cache for all platform registries", Actions: new List { "Ensured namespace 'harbor'", "Helm install harbor v2.12.0", "Created proxy cache project 'dockerhub-cache' → docker.io", "Created proxy cache project 'ghcr-cache' → ghcr.io", "Created proxy cache project 'quay-cache' → quay.io", "Created proxy cache project 'k8s-cache' → registry.k8s.io", "Created proxy cache project 'gcr-cache' → gcr.io", "Harbor registry available at registry.internal.corp.com" })); DeployComponentHandler handler = new(repository, new List { harborInstaller.Object }, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); DeployComponentRequest request = new("harbor", Version: "2.12.0", Parameters: new Dictionary { ["harborDomain"] = "registry.internal.corp.com", ["adminPassword"] = "Harbor12345", ["storageClass"] = "longhorn", ["storageSize"] = "100Gi", ["cacheRegistries"] = "docker.io,ghcr.io,quay.io,registry.k8s.io,gcr.io" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — All 5 registries have proxy cache projects. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("dockerhub-cache")); result.Value.Actions.Should().Contain(a => a.Contains("ghcr-cache")); result.Value.Actions.Should().Contain(a => a.Contains("quay-cache")); result.Value.Actions.Should().Contain(a => a.Contains("k8s-cache")); result.Value.Actions.Should().Contain(a => a.Contains("gcr-cache")); } // ─── Harbor Adoption Check ──────────────────────────────────────────────── [Fact] public async Task Harbor_Check_DetectsExistingHarborDeployment() { // Arrange — A cluster already has Harbor installed via Helm. // The adoption check should discover it, report version, and enumerate // existing proxy cache projects. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); Mock harborCheck = new(); harborCheck.Setup(c => c.ComponentName).Returns("harbor"); harborCheck.Setup(c => c.DisplayName).Returns("Harbor (Container Registry & Helm Charts)"); harborCheck .Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult( "harbor", ComponentStatus.Installed, Details: new List { "Harbor 2.12.0 detected (Helm release 'harbor' in namespace 'harbor')", "Registry endpoint: registry.internal.corp.com", "Proxy cache projects: dockerhub-cache, ghcr-cache, quay-cache", "Trivy scanner enabled", "ChartMuseum enabled" }, MissingItems: new List(), Configuration: new DiscoveredConfiguration( Version: "2.12.0", Namespace: "harbor", HelmReleaseName: "harbor", Values: new Dictionary { ["registryEndpoint"] = "registry.internal.corp.com", ["proxyCacheProjects"] = "dockerhub-cache,ghcr-cache,quay-cache", ["trivyEnabled"] = "true", ["chartMuseumEnabled"] = "true", ["storageClass"] = "longhorn", ["storageSize"] = "100Gi" }))); // Act ComponentCheckResult checkResult = await harborCheck.Object.CheckAsync(cluster); // Assert — Harbor detected with full configuration discovery. checkResult.Status.Should().Be(ComponentStatus.Installed); checkResult.Configuration.Should().NotBeNull(); checkResult.Configuration!.Version.Should().Be("2.12.0"); checkResult.Configuration.HelmReleaseName.Should().Be("harbor"); checkResult.Configuration.Values["proxyCacheProjects"].Should().Contain("dockerhub-cache"); checkResult.Details.Should().Contain(d => d.Contains("Trivy")); } [Fact] public async Task Harbor_Check_DetectsPartialInstallation() { // Arrange — Harbor is installed but some pods are not healthy (e.g., // the database pod is in CrashLoopBackOff). The check reports Degraded. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); Mock harborCheck = new(); harborCheck.Setup(c => c.ComponentName).Returns("harbor"); harborCheck.Setup(c => c.DisplayName).Returns("Harbor (Container Registry & Helm Charts)"); harborCheck .Setup(c => c.CheckAsync(cluster, It.IsAny())) .ReturnsAsync(new ComponentCheckResult( "harbor", ComponentStatus.Degraded, Details: new List { "Harbor Helm release found in namespace 'harbor'", "Core component: Running", "Registry component: Running", "Database component: CrashLoopBackOff" }, MissingItems: new List { "harbor-database pod unhealthy (CrashLoopBackOff)" })); // Act ComponentCheckResult checkResult = await harborCheck.Object.CheckAsync(cluster); // Assert — Degraded status with details about what's wrong. checkResult.Status.Should().Be(ComponentStatus.Degraded); checkResult.MissingItems.Should().Contain(m => m.Contains("database")); checkResult.Details.Should().Contain(d => d.Contains("CrashLoopBackOff")); } private static KubernetesCluster CreateConnectedCluster() { KubernetesCluster cluster = KubernetesCluster.Register( "prod-cluster", "https://k8s.example.com:6443", "apiVersion: v1\nkind: Config\nclusters: []\nusers: []\ncontexts: []", Guid.NewGuid(), "prod-ctx", Guid.NewGuid()); cluster.MarkConnected(); return cluster; } // ─── CNPG Database Integration ──────────────────────────────────────────── [Fact] public void HarborSchema_ContainsCnpgClusterNameParameter() { // Harbor should be able to reference the shared CNPG cluster for its // database instead of using the embedded PostgreSQL sub-chart. ConfigurationSchema schema = ComponentSchemas.GetSchema("harbor"); schema.Parameters.Should().Contain(p => p.Key == "cnpgClusterName" && p.Type == ParameterType.PostgresCluster); } [Fact] public void HarborSchema_ContainsCnpgNamespaceParameter() { // The CNPG cluster namespace is needed to resolve the primary service endpoint. ConfigurationSchema schema = ComponentSchemas.GetSchema("harbor"); schema.Parameters.Should().Contain(p => p.Key == "cnpgNamespace"); } // ─── Harbor Database Credentials ────────────────────────────────────── [Fact] public async Task Harbor_Install_WithCnpg_StoresCredentialsInVault() { // Arrange — When Harbor is installed with a CNPG cluster, the installer // should store the generated database credentials in the vault at // infrastructure/{clusterId}/harbor/database/* so they're auditable // and recoverable. KubernetesCluster cluster = CreateConnectedCluster(); await repository.AddAsync(cluster); // Track vault calls made by the installer. List vaultPaths = new(); harborInstaller .Setup(i => i.InstallAsync(cluster, It.IsAny(), It.IsAny())) .ReturnsAsync(new InstallResult( Success: true, ComponentName: "harbor", Message: "Harbor installed with external CNPG database", Actions: new List { "Created database 'harbor' on CNPG cluster 'shared-pg'", "Stored database credentials in Kubernetes Secret and vault", "Helm install harbor v1.16.0" })); DeployComponentHandler handler = new(repository, new List { harborInstaller.Object }, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); DeployComponentRequest request = new("harbor", Parameters: new Dictionary { ["harborDomain"] = "registry.dev.example.com", ["cnpgClusterName"] = "shared-pg", ["cnpgNamespace"] = "cnpg-system" }); // Act Result result = await handler.HandleAsync(cluster.Id, request); // Assert — The install result should mention both the K8s Secret and vault. result.IsSuccess.Should().BeTrue(); result.Value!.Actions.Should().Contain(a => a.Contains("Stored database credentials")); } [Fact] public void CnpgCredentialsSecret_ContainsAllRequiredKeys() { // When Harbor uses an external CNPG database, a K8s Secret is created // with the database connection details. This secret must contain all // four keys that Harbor expects: host, database, username, password. V1Secret secret = CnpgDatabaseProvisioner.BuildCredentialsSecret( "harbor-cnpg-credentials", "harbor", "shared-pg-rw.cnpg-system.svc.cluster.local", "harbor", "harbor", "secret-password-123"); secret.StringData.Should().ContainKey("host"); secret.StringData.Should().ContainKey("database"); secret.StringData.Should().ContainKey("username"); secret.StringData.Should().ContainKey("password"); secret.StringData["host"].Should().Be("shared-pg-rw.cnpg-system.svc.cluster.local"); secret.StringData["database"].Should().Be("harbor"); secret.StringData["username"].Should().Be("harbor"); secret.StringData["password"].Should().Be("secret-password-123"); } [Fact] public void CnpgCredentialsSecret_HasManagementLabels() { // The credentials secret should be labeled for EntKube management // so it can be discovered and cleaned up. V1Secret secret = CnpgDatabaseProvisioner.BuildCredentialsSecret( "harbor-cnpg-credentials", "harbor", "host", "harbor", "harbor", "pass"); secret.Metadata.Labels["app.kubernetes.io/managed-by"].Should().Be("entkube"); secret.Metadata.Labels["entkube.io/purpose"].Should().Be("database-credentials"); } }