Files
git.stella-ops.org/src/EvidenceLocker/StellaOps.EvidenceLocker/StellaOps.EvidenceLocker.Tests/DatabaseMigrationTests.cs

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();
}
}