161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
using System;
|
|
using System.Net.Http;
|
|
using Docker.DotNet;
|
|
using DotNet.Testcontainers.Builders;
|
|
using DotNet.Testcontainers.Configurations;
|
|
using DotNet.Testcontainers.Containers;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Npgsql;
|
|
using StellaOps.EvidenceLocker.Core.Configuration;
|
|
using StellaOps.EvidenceLocker.Core.Domain;
|
|
using StellaOps.EvidenceLocker.Infrastructure.Db;
|
|
using Xunit;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.EvidenceLocker.Tests;
|
|
|
|
public sealed class DatabaseMigrationTests : IAsyncLifetime
|
|
{
|
|
private readonly PostgreSqlTestcontainer _postgres;
|
|
private EvidenceLockerDataSource? _dataSource;
|
|
private IEvidenceLockerMigrationRunner? _migrationRunner;
|
|
private string? _skipReason;
|
|
|
|
public DatabaseMigrationTests()
|
|
{
|
|
_postgres = new TestcontainersBuilder<PostgreSqlTestcontainer>()
|
|
.WithDatabase(new PostgreSqlTestcontainerConfiguration
|
|
{
|
|
Database = "evidence_locker_tests",
|
|
Username = "postgres",
|
|
Password = "postgres"
|
|
})
|
|
.WithCleanUp(true)
|
|
.Build();
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Integration)]
|
|
[Fact]
|
|
public async Task ApplyAsync_CreatesExpectedSchemaAndPolicies()
|
|
{
|
|
if (_skipReason is not null)
|
|
{
|
|
Assert.Skip(_skipReason);
|
|
}
|
|
|
|
var cancellationToken = CancellationToken.None;
|
|
|
|
await _migrationRunner!.ApplyAsync(cancellationToken);
|
|
|
|
await using var connection = await _dataSource!.OpenConnectionAsync(cancellationToken);
|
|
await using var tablesCommand = new NpgsqlCommand(
|
|
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'evidence_locker' ORDER BY table_name;",
|
|
connection);
|
|
var tables = new List<string>();
|
|
await using (var reader = await tablesCommand.ExecuteReaderAsync(cancellationToken))
|
|
{
|
|
while (await reader.ReadAsync(cancellationToken))
|
|
{
|
|
tables.Add(reader.GetString(0));
|
|
}
|
|
}
|
|
|
|
Assert.Contains("evidence_artifacts", tables);
|
|
Assert.Contains("evidence_bundles", tables);
|
|
Assert.Contains("evidence_holds", tables);
|
|
Assert.Contains("evidence_schema_version", tables);
|
|
|
|
await using var versionCommand = new NpgsqlCommand(
|
|
"SELECT COUNT(*) FROM evidence_locker.evidence_schema_version WHERE version = 1;",
|
|
connection);
|
|
var applied = Convert.ToInt64(await versionCommand.ExecuteScalarAsync(cancellationToken) ?? 0L);
|
|
Assert.Equal(1, applied);
|
|
|
|
var tenant = TenantId.FromGuid(Guid.NewGuid());
|
|
await using var tenantConnection = await _dataSource.OpenConnectionAsync(tenant, cancellationToken);
|
|
await using var insertCommand = new NpgsqlCommand(@"
|
|
INSERT INTO evidence_locker.evidence_bundles
|
|
(bundle_id, tenant_id, kind, status, root_hash, storage_key)
|
|
VALUES
|
|
(@bundle, @tenant, 1, 3, @hash, @key);",
|
|
tenantConnection);
|
|
insertCommand.Parameters.AddWithValue("bundle", Guid.NewGuid());
|
|
insertCommand.Parameters.AddWithValue("tenant", tenant.Value);
|
|
insertCommand.Parameters.AddWithValue("hash", new string('a', 64));
|
|
insertCommand.Parameters.AddWithValue("key", $"tenants/{tenant.Value:N}/bundles/test/resource");
|
|
await insertCommand.ExecuteNonQueryAsync(cancellationToken);
|
|
|
|
await using var isolationConnection = await _dataSource.OpenConnectionAsync(tenant, cancellationToken);
|
|
await using var selectCommand = new NpgsqlCommand(
|
|
"SELECT COUNT(*) FROM evidence_locker.evidence_bundles;",
|
|
isolationConnection);
|
|
var visibleCount = Convert.ToInt64(await selectCommand.ExecuteScalarAsync(cancellationToken) ?? 0L);
|
|
Assert.Equal(1, visibleCount);
|
|
|
|
await using var otherTenantConnection = await _dataSource.OpenConnectionAsync(TenantId.FromGuid(Guid.NewGuid()), cancellationToken);
|
|
await using var otherSelectCommand = new NpgsqlCommand(
|
|
"SELECT COUNT(*) FROM evidence_locker.evidence_bundles;",
|
|
otherTenantConnection);
|
|
var otherVisible = Convert.ToInt64(await otherSelectCommand.ExecuteScalarAsync(cancellationToken) ?? 0L);
|
|
Assert.Equal(0, otherVisible);
|
|
|
|
await using var violationConnection = await _dataSource.OpenConnectionAsync(tenant, cancellationToken);
|
|
await using var violationCommand = new NpgsqlCommand(@"
|
|
INSERT INTO evidence_locker.evidence_bundles
|
|
(bundle_id, tenant_id, kind, status, root_hash, storage_key)
|
|
VALUES
|
|
(@bundle, @tenant, 1, 3, @hash, @key);",
|
|
violationConnection);
|
|
violationCommand.Parameters.AddWithValue("bundle", Guid.NewGuid());
|
|
violationCommand.Parameters.AddWithValue("tenant", Guid.NewGuid());
|
|
violationCommand.Parameters.AddWithValue("hash", new string('b', 64));
|
|
violationCommand.Parameters.AddWithValue("key", "tenants/other/bundles/resource");
|
|
|
|
await Assert.ThrowsAsync<PostgresException>(() => violationCommand.ExecuteNonQueryAsync(cancellationToken));
|
|
}
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
try
|
|
{
|
|
await _postgres.StartAsync();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_skipReason = $"Docker endpoint unavailable: {ex.Message}";
|
|
return;
|
|
}
|
|
catch (Docker.DotNet.DockerApiException ex)
|
|
{
|
|
_skipReason = $"Docker API error: {ex.Message}";
|
|
return;
|
|
}
|
|
|
|
var databaseOptions = new DatabaseOptions
|
|
{
|
|
ConnectionString = _postgres.ConnectionString,
|
|
ApplyMigrationsAtStartup = false
|
|
};
|
|
|
|
_dataSource = new EvidenceLockerDataSource(databaseOptions, NullLogger<EvidenceLockerDataSource>.Instance);
|
|
_migrationRunner = new EvidenceLockerMigrationRunner(_dataSource, NullLogger<EvidenceLockerMigrationRunner>.Instance);
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_skipReason is not null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_dataSource is not null)
|
|
{
|
|
await _dataSource.DisposeAsync();
|
|
}
|
|
|
|
await _postgres.DisposeAsync();
|
|
}
|
|
}
|
|
|
|
|