400 lines
13 KiB
C#
400 lines
13 KiB
C#
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*");
|
|
}
|
|
}
|