using Microsoft.Extensions.Logging.Abstractions; using Npgsql; using StellaOps.EvidenceLocker.Core.Configuration; using StellaOps.EvidenceLocker.Core.Domain; using StellaOps.EvidenceLocker.Infrastructure.Db; using StellaOps.TestKit; using System; using System.Net.Http; using Testcontainers.PostgreSql; using Xunit; namespace StellaOps.EvidenceLocker.Tests; public sealed class DatabaseMigrationTests : IClassFixture { private readonly PostgreSqlFixture _fixture; public DatabaseMigrationTests(PostgreSqlFixture fixture) { _fixture = fixture; } [Trait("Category", TestCategories.Integration)] [Fact] public async Task ApplyAsync_CreatesExpectedSchemaAndPolicies() { await _fixture.EnsureInitializedAsync(); if (_fixture.SkipReason is not null) { Assert.Skip(_fixture.SkipReason); } var cancellationToken = CancellationToken.None; // Migrations already applied by the fixture; verify schema state. await using var connection = await _fixture.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(); 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_gate_artifacts", 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); await using var versionFourCommand = new NpgsqlCommand( "SELECT COUNT(*) FROM evidence_locker.evidence_schema_version WHERE version = 4;", connection); var appliedVersionFour = Convert.ToInt64(await versionFourCommand.ExecuteScalarAsync(cancellationToken) ?? 0L); Assert.Equal(1, appliedVersionFour); var tenant = TenantId.FromGuid(Guid.NewGuid()); await using var tenantConnection = await _fixture.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 _fixture.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 _fixture.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 _fixture.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(() => violationCommand.ExecuteNonQueryAsync(cancellationToken)); } }