Mark subproject as dirty in CMKS - Infra
This commit is contained in:
399
tests/EntKube.Web.Tests/CleuraS3ManagementTests.cs
Normal file
399
tests/EntKube.Web.Tests/CleuraS3ManagementTests.cs
Normal file
@@ -0,0 +1,399 @@
|
||||
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 Cleura S3 bucket management operations exposed via StorageService.
|
||||
/// These verify that the StorageService correctly validates inputs, loads
|
||||
/// credentials from the vault, and delegates to OpenStackS3Service.
|
||||
///
|
||||
/// Since actual S3/Keystone calls require live infrastructure, we mock
|
||||
/// OpenStackS3Service and focus on the orchestration logic.
|
||||
/// </summary>
|
||||
public class CleuraS3ManagementTests : 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 Mock<IHttpClientFactory> httpFactory;
|
||||
private readonly StorageService sut;
|
||||
|
||||
// We'll use a real OpenStackS3Service since the methods we're testing
|
||||
// on StorageService validate state before calling it. For actual S3 calls
|
||||
// that would fail, we test error paths.
|
||||
|
||||
public CleuraS3ManagementTests()
|
||||
{
|
||||
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);
|
||||
httpFactory = new Mock<IHttpClientFactory>();
|
||||
OpenStackS3Service openStackS3 = new(vaultService, httpFactory.Object);
|
||||
sut = new StorageService(dbFactory, vaultService, openStackS3);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
db.Dispose();
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
// ──────── Helpers ────────
|
||||
|
||||
private async Task<(Tenant tenant, Data.Environment env, StorageLink link)> CreateCleuraLinkWithCredentialsAsync()
|
||||
{
|
||||
// Create tenant, environment, OpenStack connection, and a Cleura S3 storage link
|
||||
// with access/secret keys stored in the vault.
|
||||
|
||||
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);
|
||||
|
||||
OpenStackConnection osConn = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
Name = "Cleura Kna1",
|
||||
AuthUrl = "https://identity.c2.citycloud.com:5000/v3",
|
||||
Region = "Kna1",
|
||||
Username = "testuser",
|
||||
ProjectId = "project-123"
|
||||
};
|
||||
|
||||
db.OpenStackConnections.Add(osConn);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Test Bucket",
|
||||
Endpoint = "https://s3-kna1.citycloud.com:8080",
|
||||
BucketName = "my-test-bucket",
|
||||
Region = "Kna1",
|
||||
OpenStackConnectionId = osConn.Id
|
||||
};
|
||||
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Store credentials in vault.
|
||||
|
||||
await vaultService.InitializeVaultAsync(tenant.Id);
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "ACCESS_KEY", "AKTEST123", default);
|
||||
await vaultService.SetStorageLinkSecretAsync(tenant.Id, link.Id, "SECRET_KEY", "secrettest456", default);
|
||||
|
||||
return (tenant, env, link);
|
||||
}
|
||||
|
||||
// ──────── ListCleuraS3BucketsAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task ListCleuraS3BucketsAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange — no storage link exists.
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.ListCleuraS3BucketsAsync(tenant.Id, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCleuraS3BucketsAsync_MissingCredentials_Throws()
|
||||
{
|
||||
// Arrange — link exists but no vault secrets.
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
|
||||
Data.Environment env = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Dev" };
|
||||
db.Set<Data.Environment>().Add(env);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "No Creds",
|
||||
Endpoint = "https://s3-kna1.citycloud.com:8080",
|
||||
BucketName = "bucket",
|
||||
Region = "Kna1"
|
||||
};
|
||||
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await vaultService.InitializeVaultAsync(tenant.Id);
|
||||
|
||||
// Act & Assert — should throw because credentials don't exist in vault.
|
||||
|
||||
Func<Task> act = () => sut.ListCleuraS3BucketsAsync(tenant.Id, link.Id);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*credentials*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ListCleuraS3BucketsAsync_WrongProvider_Throws()
|
||||
{
|
||||
// Arrange — link exists but is AWS S3, not Cleura.
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
|
||||
Data.Environment env = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Dev" };
|
||||
db.Set<Data.Environment>().Add(env);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.AwsS3,
|
||||
Name = "AWS Bucket",
|
||||
Endpoint = "https://s3.eu-west-1.amazonaws.com",
|
||||
BucketName = "bucket",
|
||||
Region = "eu-west-1"
|
||||
};
|
||||
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert — wrong provider should not be found.
|
||||
|
||||
Func<Task> act = () => sut.ListCleuraS3BucketsAsync(tenant.Id, link.Id);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
// ──────── DeleteCleuraS3BucketAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteCleuraS3BucketAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.DeleteCleuraS3BucketAsync(tenant.Id, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
// ──────── RotateCleuraS3CredentialsAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task RotateCleuraS3CredentialsAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.RotateCleuraS3CredentialsAsync(tenant.Id, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RotateCleuraS3CredentialsAsync_MissingOpenStackConnection_Throws()
|
||||
{
|
||||
// Arrange — link exists but its OpenStack connection has been deleted.
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
|
||||
Data.Environment env = new() { Id = Guid.NewGuid(), TenantId = tenant.Id, Name = "Dev" };
|
||||
db.Set<Data.Environment>().Add(env);
|
||||
|
||||
// Create a real OpenStack connection so the FK is valid.
|
||||
|
||||
OpenStackConnection osConn = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
Name = "ToBeDeleted",
|
||||
AuthUrl = "https://identity.example.com:5000/v3",
|
||||
Region = "Kna1",
|
||||
Username = "user"
|
||||
};
|
||||
|
||||
db.OpenStackConnections.Add(osConn);
|
||||
|
||||
StorageLink link = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenant.Id,
|
||||
EnvironmentId = env.Id,
|
||||
Provider = StorageProvider.CleuraS3,
|
||||
Name = "Orphaned",
|
||||
Endpoint = "https://s3-kna1.citycloud.com:8080",
|
||||
BucketName = "bucket",
|
||||
Region = "Kna1",
|
||||
OpenStackConnectionId = osConn.Id
|
||||
};
|
||||
|
||||
db.StorageLinks.Add(link);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Now delete the OpenStack connection to simulate orphaned state.
|
||||
|
||||
db.OpenStackConnections.Remove(osConn);
|
||||
link.OpenStackConnectionId = null;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.RotateCleuraS3CredentialsAsync(tenant.Id, link.Id);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*OpenStack connection not found*");
|
||||
}
|
||||
|
||||
// ──────── GetStorageLinkSecretValueAsync (VaultService) ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetStorageLinkSecretValueAsync_ReturnsDecryptedValue()
|
||||
{
|
||||
// Arrange — store a secret and retrieve it.
|
||||
|
||||
(Tenant tenant, _, StorageLink link) = await CreateCleuraLinkWithCredentialsAsync();
|
||||
|
||||
// Act
|
||||
|
||||
string? accessKey = await vaultService.GetStorageLinkSecretValueAsync(tenant.Id, link.Id, "ACCESS_KEY");
|
||||
|
||||
// Assert
|
||||
|
||||
accessKey.Should().Be("AKTEST123");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetStorageLinkSecretValueAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
await vaultService.InitializeVaultAsync(tenant.Id);
|
||||
|
||||
// Act
|
||||
|
||||
string? result = await vaultService.GetStorageLinkSecretValueAsync(tenant.Id, Guid.NewGuid(), "MISSING");
|
||||
|
||||
// Assert
|
||||
|
||||
result.Should().BeNull();
|
||||
}
|
||||
|
||||
// ──────── GetBucketCorsAsync / SetBucketCorsAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetBucketCorsAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.GetBucketCorsAsync(tenant.Id, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetBucketCorsAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
List<S3CorsRule> rules = [new() { AllowedOrigins = ["*"], AllowedMethods = ["GET"] }];
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.SetBucketCorsAsync(tenant.Id, Guid.NewGuid(), rules);
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
// ──────── GetBucketPolicyAsync / SetBucketPolicyAsync ────────
|
||||
|
||||
[Fact]
|
||||
public async Task GetBucketPolicyAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.GetBucketPolicyAsync(tenant.Id, Guid.NewGuid());
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SetBucketPolicyAsync_LinkNotFound_Throws()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
Tenant tenant = new() { Id = Guid.NewGuid(), Name = "T", Slug = "t" };
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
// Act & Assert
|
||||
|
||||
Func<Task> act = () => sut.SetBucketPolicyAsync(tenant.Id, Guid.NewGuid(), "{}");
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not found*");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user