Mark subproject as dirty in CMKS - Infra
This commit is contained in:
797
tests/EntKube.Web.Tests/StorageServiceTests.cs
Normal file
797
tests/EntKube.Web.Tests/StorageServiceTests.cs
Normal file
@@ -0,0 +1,797 @@
|
||||
using EntKube.Web.Data;
|
||||
using EntKube.Web.Services;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moq;
|
||||
|
||||
namespace EntKube.Web.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the StorageService which manages external storage links (AWS S3,
|
||||
/// Azure Storage, Cleura S3) and MinIO discovery. Uses SQLite in-memory for
|
||||
/// fast, isolated database tests.
|
||||
///
|
||||
/// Note: MinIO discovery tests are not included here because they require
|
||||
/// a live Kubernetes cluster. These tests focus on the CRUD operations
|
||||
/// for external storage links and vault credential management.
|
||||
/// </summary>
|
||||
public class StorageServiceTests : IDisposable
|
||||
{
|
||||
private static readonly byte[] TestRootKey = Convert.FromBase64String(
|
||||
"dGhpcyBpcyBhIDMyIGJ5dGUga2V5ISEhMTIzNDU2Nzg=");
|
||||
|
||||
private readonly SqliteConnection connection;
|
||||
private readonly ApplicationDbContext db;
|
||||
private readonly TestDbContextFactory dbFactory;
|
||||
private readonly VaultService vaultService;
|
||||
private readonly StorageService sut;
|
||||
|
||||
public StorageServiceTests()
|
||||
{
|
||||
connection = new SqliteConnection("DataSource=:memory:");
|
||||
connection.Open();
|
||||
|
||||
DbContextOptions<ApplicationDbContext> options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
|
||||
db = new ApplicationDbContext(options);
|
||||
dbFactory = new TestDbContextFactory(connection);
|
||||
db.Database.EnsureCreated();
|
||||
|
||||
VaultEncryptionService encryption = new(TestRootKey);
|
||||
vaultService = new VaultService(dbFactory, encryption);
|
||||
Mock<IHttpClientFactory> httpFactory = new();
|
||||
OpenStackS3Service openStackS3 = new(vaultService, httpFactory.Object);
|
||||
sut = new StorageService(dbFactory, vaultService, openStackS3);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
db.Dispose();
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
private (Tenant tenant, Data.Environment env) CreateTenantWithEnvironment()
|
||||
{
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "TestCo", Slug = "testco" };
|
||||
db.Tenants.Add(tenant);
|
||||
|
||||
Data.Environment env = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
Name = "Production"
|
||||
};
|
||||
db.Set<Data.Environment>().Add(env);
|
||||
db.SaveChanges();
|
||||
|
||||
return (tenant, env);
|
||||
}
|
||||
|
||||
// ──────── IsMinioAvailableAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task IsMinioAvailableAsync_NoComponents_ReturnsFalse()
|
||||
{
|
||||
// Arrange — tenant with no clusters/components.
|
||||
|
||||
(Tenant tenant, _) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act
|
||||
|
||||
bool result = await sut.IsMinioAvailableAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsMinioAvailableAsync_MinioInstalled_ReturnsTrue()
|
||||
{
|
||||
// Arrange — tenant with a cluster that has minio installed.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
KubernetesCluster cluster = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Name = "prod-cluster",
|
||||
ApiServerUrl = "https://k8s.example.com"
|
||||
};
|
||||
db.KubernetesClusters.Add(cluster);
|
||||
|
||||
ClusterComponent component = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ClusterId = cluster.Id,
|
||||
Name = "minio",
|
||||
ComponentType = "helm",
|
||||
Status = ComponentStatus.Installed
|
||||
};
|
||||
db.ClusterComponents.Add(component);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
|
||||
bool result = await sut.IsMinioAvailableAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().BeTrue();
|
||||
}
|
||||
|
||||
// ──────── GetStorageLinksAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetStorageLinksAsync_NoLinks_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, _) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act
|
||||
|
||||
List<StorageLink> result = await sut.GetStorageLinksAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStorageLinksAsync_WithLinks_ReturnsAll()
|
||||
{
|
||||
// Arrange — add two links for the tenant.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
db.StorageLinks.AddRange(
|
||||
new StorageLink
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.AwsS3,
|
||||
Name = "Backups"
|
||||
},
|
||||
new StorageLink
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Media"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
|
||||
List<StorageLink> result = await sut.GetStorageLinksAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
result.Select(s => s.Name).Should().Contain("Backups").And.Contain("Media");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStorageLinksAsync_FilterByEnvironment_ReturnsFiltered()
|
||||
{
|
||||
// Arrange — two environments, one link each.
|
||||
|
||||
(Tenant tenant, Data.Environment env1) = CreateTenantWithEnvironment();
|
||||
|
||||
Data.Environment env2 = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
Name = "Staging"
|
||||
};
|
||||
db.Set<Data.Environment>().Add(env2);
|
||||
|
||||
db.StorageLinks.AddRange(
|
||||
new StorageLink
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env1.Id,
|
||||
Provider = StorageProvider.AwsS3,
|
||||
Name = "Prod Backups"
|
||||
},
|
||||
new StorageLink
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env2.Id,
|
||||
Provider = StorageProvider.AwsS3,
|
||||
Name = "Staging Backups"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
|
||||
List<StorageLink> result = await sut.GetStorageLinksAsync(tenant.Id, env1.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].Name.Should().Be("Prod Backups");
|
||||
}
|
||||
|
||||
// ──────── CreateStorageLinkAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task CreateStorageLinkAsync_CreatesLinkAndStoresCredentials()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act — create a link with credentials.
|
||||
|
||||
StorageLink link = await sut.CreateStorageLinkAsync(
|
||||
tenant.Id,
|
||||
env.Id,
|
||||
StorageProvider.AwsS3,
|
||||
"My Bucket",
|
||||
"https://s3.eu-west-1.amazonaws.com",
|
||||
"my-app-backups",
|
||||
"eu-west-1",
|
||||
"AKIAIOSFODNN7EXAMPLE",
|
||||
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"Production backup bucket");
|
||||
|
||||
// Assert — the link is persisted.
|
||||
|
||||
StorageLink? saved = await db.StorageLinks.FindAsync(link.Id);
|
||||
saved.Should().NotBeNull();
|
||||
saved!.Name.Should().Be("My Bucket");
|
||||
saved.Provider.Should().Be(StorageProvider.AwsS3);
|
||||
saved.BucketName.Should().Be("my-app-backups");
|
||||
saved.Region.Should().Be("eu-west-1");
|
||||
saved.Endpoint.Should().Be("https://s3.eu-west-1.amazonaws.com");
|
||||
saved.Notes.Should().Be("Production backup bucket");
|
||||
|
||||
// Assert — credentials are stored in the vault.
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secrets.Should().HaveCount(2);
|
||||
secrets.Select(s => s.Name).Should().Contain("ACCESS_KEY").And.Contain("SECRET_KEY");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateStorageLinkAsync_WithoutCredentials_CreatesLinkOnly()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act — create a link without credentials (just a reference).
|
||||
|
||||
StorageLink link = await sut.CreateStorageLinkAsync(
|
||||
tenant.Id,
|
||||
env.Id,
|
||||
StorageProvider.AzureStorage,
|
||||
"Azure Media",
|
||||
"https://myaccount.blob.core.windows.net",
|
||||
"media-container",
|
||||
"swedencentral",
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
// Assert — link exists, no vault secrets.
|
||||
|
||||
StorageLink? saved = await db.StorageLinks.FindAsync(link.Id);
|
||||
saved.Should().NotBeNull();
|
||||
saved!.Provider.Should().Be(StorageProvider.AzureStorage);
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secrets.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ──────── UpdateStorageLinkAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateStorageLinkAsync_UpdatesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Old Name",
|
||||
BucketName = "old-bucket"
|
||||
};
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
|
||||
await sut.UpdateStorageLinkAsync(
|
||||
link.Id, "New Name", "https://new-endpoint.com", "new-bucket", "eu-north-1", "Updated notes");
|
||||
|
||||
// Assert — clear tracking so we get fresh data from DB
|
||||
|
||||
db.ChangeTracker.Clear();
|
||||
StorageLink? updated = await db.StorageLinks.FindAsync(link.Id);
|
||||
updated!.Name.Should().Be("New Name");
|
||||
updated.Endpoint.Should().Be("https://new-endpoint.com");
|
||||
updated.BucketName.Should().Be("new-bucket");
|
||||
updated.Region.Should().Be("eu-north-1");
|
||||
updated.Notes.Should().Be("Updated notes");
|
||||
}
|
||||
|
||||
// ──────── DeleteStorageLinkAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteStorageLinkAsync_RemovesLinkAndVaultSecrets()
|
||||
{
|
||||
// Arrange — create a link with credentials.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
StorageLink link = await sut.CreateStorageLinkAsync(
|
||||
tenant.Id, env.Id, StorageProvider.AwsS3, "To Delete",
|
||||
"https://s3.amazonaws.com", "bucket", "us-east-1",
|
||||
"key123", "secret456", null);
|
||||
|
||||
// Verify credentials exist before deletion.
|
||||
|
||||
List<VaultSecret> secretsBefore = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secretsBefore.Should().HaveCount(2);
|
||||
|
||||
// Act
|
||||
|
||||
await sut.DeleteStorageLinkAsync(tenant.Id, link.Id);
|
||||
|
||||
// Assert — link and secrets are gone.
|
||||
|
||||
StorageLink? deleted = await db.StorageLinks.FindAsync(link.Id);
|
||||
deleted.Should().BeNull();
|
||||
|
||||
List<VaultSecret> secretsAfter = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secretsAfter.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ──────── GetEnvironmentsAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetEnvironmentsAsync_ReturnsTenantEnvironments()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act
|
||||
|
||||
List<Data.Environment> result = await sut.GetEnvironmentsAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().HaveCount(1);
|
||||
result[0].Name.Should().Be("Production");
|
||||
}
|
||||
|
||||
// ──────── OpenStack Connections ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetOpenStackConnectionsAsync_NoConnections_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, _) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act
|
||||
|
||||
List<OpenStackConnection> result = await sut.GetOpenStackConnectionsAsync(tenant.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateOpenStackConnectionAsync_CreatesConnectionAndStoresPassword()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, _) = CreateTenantWithEnvironment();
|
||||
|
||||
// Act
|
||||
|
||||
OpenStackConnection connection = await sut.CreateOpenStackConnectionAsync(
|
||||
tenant.Id,
|
||||
"Cleura Prod",
|
||||
"https://identity.c2.citycloud.com:5000/v3",
|
||||
"Kna1",
|
||||
"my-project",
|
||||
"abc123",
|
||||
"Default",
|
||||
"Default",
|
||||
"admin@example.com",
|
||||
"supersecret");
|
||||
|
||||
// Assert — connection is persisted with metadata.
|
||||
|
||||
OpenStackConnection? saved = await db.OpenStackConnections.FindAsync(connection.Id);
|
||||
saved.Should().NotBeNull();
|
||||
saved!.Name.Should().Be("Cleura Prod");
|
||||
saved.AuthUrl.Should().Be("https://identity.c2.citycloud.com:5000/v3");
|
||||
saved.Region.Should().Be("Kna1");
|
||||
saved.ProjectName.Should().Be("my-project");
|
||||
saved.ProjectId.Should().Be("abc123");
|
||||
saved.UserDomainName.Should().Be("Default");
|
||||
saved.ProjectDomainName.Should().Be("Default");
|
||||
saved.Username.Should().Be("admin@example.com");
|
||||
|
||||
// Assert — password is in the vault.
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetOpenStackSecretsAsync(tenant.Id, connection.Id);
|
||||
secrets.Should().HaveCount(1);
|
||||
secrets[0].Name.Should().Be("OS_PASSWORD");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOpenStackConnectionAsync_RemovesConnectionAndVaultSecrets()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, _) = CreateTenantWithEnvironment();
|
||||
|
||||
OpenStackConnection connection = await sut.CreateOpenStackConnectionAsync(
|
||||
tenant.Id, "ToDelete", "https://auth.example.com/v3",
|
||||
"Sto2", null, null, null, null, "user", "pass123");
|
||||
|
||||
// Act
|
||||
|
||||
bool deleted = await sut.DeleteOpenStackConnectionAsync(tenant.Id, connection.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
deleted.Should().BeTrue();
|
||||
|
||||
OpenStackConnection? found = await db.OpenStackConnections.FindAsync(connection.Id);
|
||||
found.Should().BeNull();
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetOpenStackSecretsAsync(tenant.Id, connection.Id);
|
||||
secrets.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteOpenStackConnectionAsync_WithLinkedStorage_ReturnsFalse()
|
||||
{
|
||||
// Arrange — create a connection with a storage link referencing it.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
OpenStackConnection connection = await sut.CreateOpenStackConnectionAsync(
|
||||
tenant.Id, "InUse", "https://auth.example.com/v3",
|
||||
"Kna1", null, null, null, null, null, null);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Cleura Bucket",
|
||||
OpenStackConnectionId = connection.Id
|
||||
};
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act — should not delete because a link depends on it.
|
||||
|
||||
bool deleted = await sut.DeleteOpenStackConnectionAsync(tenant.Id, connection.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
deleted.Should().BeFalse();
|
||||
|
||||
OpenStackConnection? stillExists = await db.OpenStackConnections.FindAsync(connection.Id);
|
||||
stillExists.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateStorageLinkAsync_CleuraWithOpenStack_SetsConnectionId()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
OpenStackConnection connection = await sut.CreateOpenStackConnectionAsync(
|
||||
tenant.Id, "Cleura", "https://auth.example.com/v3",
|
||||
"Kna1", null, null, null, null, null, null);
|
||||
|
||||
// Act
|
||||
|
||||
StorageLink link = await sut.CreateStorageLinkAsync(
|
||||
tenant.Id, env.Id, StorageProvider.CleuraS3, "Cleura Bucket",
|
||||
"https://s3-kna1.cloudferro.com", "my-bucket", "Kna1",
|
||||
null, null, null, connection.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
StorageLink? saved = await db.StorageLinks.FindAsync(link.Id);
|
||||
saved.Should().NotBeNull();
|
||||
saved!.OpenStackConnectionId.Should().Be(connection.Id);
|
||||
saved.Provider.Should().Be(StorageProvider.CleuraS3);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Storage Bindings
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
private (KubernetesCluster cluster, AppDeployment deployment, StorageLink link) CreateDeploymentWithStorageLink(
|
||||
Tenant tenant, Data.Environment env)
|
||||
{
|
||||
Customer customer = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Acme" };
|
||||
db.Customers.Add(customer);
|
||||
|
||||
App app = new() { Id = Guid.NewGuid(), CustomerId = customer.Id, Name = "billing-api" };
|
||||
db.Apps.Add(app);
|
||||
|
||||
KubernetesCluster cluster = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Name = "prod-cluster",
|
||||
ApiServerUrl = "https://k8s.example.com"
|
||||
};
|
||||
db.KubernetesClusters.Add(cluster);
|
||||
|
||||
AppDeployment deployment = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AppId = app.Id,
|
||||
Name = "billing-api-prod",
|
||||
Type = DeploymentType.HelmChart,
|
||||
EnvironmentId = env.Id,
|
||||
ClusterId = cluster.Id,
|
||||
Namespace = "billing"
|
||||
};
|
||||
db.AppDeployments.Add(deployment);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.AwsS3,
|
||||
Name = "Invoice PDFs",
|
||||
Endpoint = "https://s3.eu-west-1.amazonaws.com",
|
||||
BucketName = "acme-invoices",
|
||||
Region = "eu-west-1"
|
||||
};
|
||||
db.StorageLinks.Add(link);
|
||||
db.SaveChanges();
|
||||
|
||||
return (cluster, deployment, link);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindStorageToDeploymentAsync_CreatesBinding()
|
||||
{
|
||||
// Arrange — a deployment and a storage link ready to be connected.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
(_, AppDeployment deployment, StorageLink link) = CreateDeploymentWithStorageLink(tenant, env);
|
||||
|
||||
// Act — bind the storage to the deployment so credentials will be synced
|
||||
// to a K8s Secret named "invoice-storage" in the deployment's namespace.
|
||||
|
||||
StorageBinding binding = await sut.BindStorageToDeploymentAsync(
|
||||
link.Id, deployment.Id, "invoice-storage");
|
||||
|
||||
// Assert — the binding exists and references both sides correctly.
|
||||
|
||||
binding.Should().NotBeNull();
|
||||
binding.StorageLinkId.Should().Be(link.Id);
|
||||
binding.AppDeploymentId.Should().Be(deployment.Id);
|
||||
binding.ComponentId.Should().BeNull();
|
||||
binding.KubernetesSecretName.Should().Be("invoice-storage");
|
||||
binding.SyncEnabled.Should().BeTrue();
|
||||
|
||||
StorageBinding? persisted = await db.Set<StorageBinding>().FindAsync(binding.Id);
|
||||
persisted.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindStorageToComponentAsync_CreatesBinding()
|
||||
{
|
||||
// Arrange — a cluster component that needs access to storage.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
|
||||
KubernetesCluster cluster = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Name = "infra-cluster",
|
||||
ApiServerUrl = "https://k8s.infra.example.com"
|
||||
};
|
||||
db.KubernetesClusters.Add(cluster);
|
||||
|
||||
ClusterComponent component = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ClusterId = cluster.Id,
|
||||
Name = "loki",
|
||||
ComponentType = "helm",
|
||||
Namespace = "monitoring"
|
||||
};
|
||||
db.ClusterComponents.Add(component);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Loki Chunks",
|
||||
Endpoint = "https://s3-kna1.citycloud.com",
|
||||
BucketName = "loki-chunks",
|
||||
Region = "Kna1"
|
||||
};
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act — bind storage to the component.
|
||||
|
||||
StorageBinding binding = await sut.BindStorageToComponentAsync(
|
||||
link.Id, component.Id, "loki-s3-credentials");
|
||||
|
||||
// Assert
|
||||
|
||||
binding.Should().NotBeNull();
|
||||
binding.StorageLinkId.Should().Be(link.Id);
|
||||
binding.AppDeploymentId.Should().BeNull();
|
||||
binding.ComponentId.Should().Be(component.Id);
|
||||
binding.KubernetesSecretName.Should().Be("loki-s3-credentials");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetBindingsForDeploymentAsync_ReturnsOnlyDeploymentBindings()
|
||||
{
|
||||
// Arrange — one deployment with two storage bindings.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
(_, AppDeployment deployment, StorageLink link) = CreateDeploymentWithStorageLink(tenant, env);
|
||||
|
||||
StorageLink link2 = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.AzureStorage,
|
||||
Name = "Logs",
|
||||
Endpoint = "https://acmelogs.blob.core.windows.net",
|
||||
BucketName = "logs"
|
||||
};
|
||||
db.StorageLinks.Add(link2);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await sut.BindStorageToDeploymentAsync(link.Id, deployment.Id, "invoices");
|
||||
await sut.BindStorageToDeploymentAsync(link2.Id, deployment.Id, "logs");
|
||||
|
||||
// Act
|
||||
|
||||
List<StorageBinding> bindings = await sut.GetBindingsForDeploymentAsync(deployment.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
bindings.Should().HaveCount(2);
|
||||
bindings.Select(b => b.KubernetesSecretName)
|
||||
.Should().Contain("invoices").And.Contain("logs");
|
||||
bindings.Should().AllSatisfy(b => b.StorageLink.Should().NotBeNull());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnbindStorageAsync_RemovesBinding()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
(_, AppDeployment deployment, StorageLink link) = CreateDeploymentWithStorageLink(tenant, env);
|
||||
|
||||
StorageBinding binding = await sut.BindStorageToDeploymentAsync(
|
||||
link.Id, deployment.Id, "temp-storage");
|
||||
|
||||
// Act — unbind the storage.
|
||||
|
||||
bool removed = await sut.UnbindStorageAsync(binding.Id);
|
||||
|
||||
// Assert
|
||||
|
||||
removed.Should().BeTrue();
|
||||
StorageBinding? found = await db.Set<StorageBinding>().FindAsync(binding.Id);
|
||||
found.Should().BeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnbindStorageAsync_NonExistentBinding_ReturnsFalse()
|
||||
{
|
||||
// Act
|
||||
|
||||
bool removed = await sut.UnbindStorageAsync(Guid.NewGuid());
|
||||
|
||||
// Assert
|
||||
|
||||
removed.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BindStorageToDeploymentAsync_ConfiguresVaultSecretsForSync()
|
||||
{
|
||||
// Arrange — a storage link with credentials already in the vault.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
(_, AppDeployment deployment, StorageLink link) = CreateDeploymentWithStorageLink(tenant, env);
|
||||
|
||||
// Store S3 credentials in the vault for this storage link.
|
||||
|
||||
await vaultService.InitializeVaultAsync(tenant.Id);
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "ACCESS_KEY", "AKIAEXAMPLE");
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "SECRET_KEY", "s3cr3t");
|
||||
|
||||
// Act — bind the storage, which should mark vault secrets for K8s sync.
|
||||
|
||||
StorageBinding binding = await sut.BindStorageToDeploymentAsync(
|
||||
link.Id, deployment.Id, "invoice-storage");
|
||||
|
||||
// Assert — the storage link's secrets should now have K8s sync configured
|
||||
// targeting the deployment's namespace and the binding's secret name.
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secrets.Should().HaveCount(2);
|
||||
secrets.Should().AllSatisfy(s =>
|
||||
{
|
||||
s.SyncToKubernetes.Should().BeTrue();
|
||||
s.KubernetesSecretName.Should().Be("invoice-storage");
|
||||
s.KubernetesNamespace.Should().Be("billing");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnbindStorageAsync_DisablesVaultSecretSync()
|
||||
{
|
||||
// Arrange — bind a storage with credentials, then unbind.
|
||||
|
||||
(Tenant tenant, Data.Environment env) = CreateTenantWithEnvironment();
|
||||
(_, AppDeployment deployment, StorageLink link) = CreateDeploymentWithStorageLink(tenant, env);
|
||||
|
||||
await vaultService.InitializeVaultAsync(tenant.Id);
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "ACCESS_KEY", "AKIAEXAMPLE");
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "SECRET_KEY", "s3cr3t");
|
||||
|
||||
StorageBinding binding = await sut.BindStorageToDeploymentAsync(
|
||||
link.Id, deployment.Id, "invoice-storage");
|
||||
|
||||
// Act — unbind.
|
||||
|
||||
await sut.UnbindStorageAsync(binding.Id);
|
||||
|
||||
// Assert — secrets should no longer be marked for K8s sync.
|
||||
|
||||
List<VaultSecret> secrets = await vaultService.GetStorageLinkSecretsAsync(tenant.Id, link.Id);
|
||||
secrets.Should().AllSatisfy(s =>
|
||||
{
|
||||
s.SyncToKubernetes.Should().BeFalse();
|
||||
s.KubernetesSecretName.Should().BeNull();
|
||||
s.KubernetesNamespace.Should().BeNull();
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user