using EntKube.Secrets.Domain; using FluentAssertions; namespace EntKube.Secrets.Tests.Domain; public class SecretScopeTests { // ─── Factory Method Tests ──────────────────────────────────────────── [Fact] public void ForCluster_CreatesInfrastructureScope() { // Arrange — Infrastructure secrets are scoped to Tenant + Environment + Cluster. // These are things like kubeconfig credentials, DNS tokens, cloud provider secrets. Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); // Act SecretScope scope = SecretScope.ForCluster(tenantId, environmentId, clusterId); // Assert scope.TenantId.Should().Be(tenantId); scope.EnvironmentId.Should().Be(environmentId); scope.ClusterId.Should().Be(clusterId); scope.CustomerId.Should().BeNull(); scope.AppName.Should().BeNull(); } [Fact] public void ForApp_CreatesApplicationScope() { // Arrange — Application secrets are scoped to Tenant + Customer + App + Environment. // These are things like database connection strings, API keys for third-party services. Guid tenantId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); // Act SecretScope scope = SecretScope.ForApp(tenantId, customerId, "my-api", environmentId); // Assert scope.TenantId.Should().Be(tenantId); scope.EnvironmentId.Should().Be(environmentId); scope.ClusterId.Should().BeNull(); scope.CustomerId.Should().Be(customerId); scope.AppName.Should().Be("my-api"); } // ─── Record Construction Tests ─────────────────────────────────────── [Fact] public void Create_WithAllFields_StoresAllValues() { // Arrange — A fully-scoped secret with all fields populated. Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); // Act SecretScope scope = new(tenantId, environmentId, clusterId, customerId, "my-api"); // Assert scope.TenantId.Should().Be(tenantId); scope.EnvironmentId.Should().Be(environmentId); scope.ClusterId.Should().Be(clusterId); scope.CustomerId.Should().Be(customerId); scope.AppName.Should().Be("my-api"); } [Fact] public void Create_WithTenantOnly_AllowsNullOptionalFields() { // Arrange & Act — A tenant-wide secret (e.g. a shared API key) // that isn't scoped to a specific environment, cluster, customer, or app. Guid tenantId = Guid.NewGuid(); SecretScope scope = new(tenantId, null, null, null, null); // Assert scope.TenantId.Should().Be(tenantId); scope.EnvironmentId.Should().BeNull(); scope.ClusterId.Should().BeNull(); scope.CustomerId.Should().BeNull(); scope.AppName.Should().BeNull(); } [Fact] public void Create_WithTenantAndEnvironment_ScopesToEnvironment() { // Arrange & Act — A secret scoped to a tenant + environment // (e.g. a dev database password shared across all customers in dev). Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); SecretScope scope = new(tenantId, environmentId, null, null, null); // Assert scope.TenantId.Should().Be(tenantId); scope.EnvironmentId.Should().Be(environmentId); scope.ClusterId.Should().BeNull(); scope.CustomerId.Should().BeNull(); scope.AppName.Should().BeNull(); } // ─── BuildPath Tests ───────────────────────────────────────────────── [Fact] public void BuildPath_TenantOnly_ReturnsPathWithTenantPrefix() { // Arrange — A tenant-scoped secret should have a path like // "tenants/{tenantId}/secrets/my-key". Guid tenantId = Guid.NewGuid(); SecretScope scope = new(tenantId, null, null, null, null); // Act string path = scope.BuildPath("my-key"); // Assert path.Should().Be($"tenants/{tenantId}/secrets/my-key"); } [Fact] public void BuildPath_TenantAndEnvironment_ReturnsEnvironmentScopedPath() { // Arrange Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); SecretScope scope = new(tenantId, environmentId, null, null, null); // Act string path = scope.BuildPath("db-password"); // Assert path.Should().Be($"tenants/{tenantId}/environments/{environmentId}/secrets/db-password"); } [Fact] public void BuildPath_ClusterScope_ReturnsClusterScopedPath() { // Arrange — Infrastructure secret scoped to a specific cluster. Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); Guid clusterId = Guid.NewGuid(); SecretScope scope = SecretScope.ForCluster(tenantId, environmentId, clusterId); // Act string path = scope.BuildPath("dns-credential"); // Assert — Path includes the cluster segment. path.Should().Be( $"tenants/{tenantId}/environments/{environmentId}/clusters/{clusterId}/secrets/dns-credential"); } [Fact] public void BuildPath_AppScope_ReturnsFullAppScopedPath() { // Arrange — Application secret: tenant → environment → customer → app. Guid tenantId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); SecretScope scope = SecretScope.ForApp(tenantId, customerId, "extract01", environmentId); // Act string path = scope.BuildPath("api-token"); // Assert path.Should().Be( $"tenants/{tenantId}/environments/{environmentId}/customers/{customerId}/apps/extract01/secrets/api-token"); } [Fact] public void BuildPath_CustomerWithoutApp_IncludesCustomerScope() { // Arrange — Customer-scoped without a specific app. Guid tenantId = Guid.NewGuid(); Guid environmentId = Guid.NewGuid(); Guid customerId = Guid.NewGuid(); SecretScope scope = new(tenantId, environmentId, null, customerId, null); // Act string path = scope.BuildPath("shared-token"); // Assert path.Should().Be( $"tenants/{tenantId}/environments/{environmentId}/customers/{customerId}/secrets/shared-token"); } // ─── Equality ──────────────────────────────────────────────────────── [Fact] public void Equality_SameValues_AreEqual() { // Arrange — Records should have value-based equality. Guid tenantId = Guid.NewGuid(); Guid envId = Guid.NewGuid(); SecretScope scope1 = new(tenantId, envId, null, null, null); SecretScope scope2 = new(tenantId, envId, null, null, null); // Assert scope1.Should().Be(scope2); } }