128 lines
4.7 KiB
C#
128 lines
4.7 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using ZB.MOM.WW.Secrets.Abstractions;
|
|
using ZB.MOM.WW.Secrets.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.Secrets.Tests.Sqlite;
|
|
|
|
public sealed class SqliteSecretsStoreMigratorTests : IDisposable
|
|
{
|
|
private readonly string _dbPath =
|
|
Path.Combine(Path.GetTempPath(), $"zb-secrets-migrator-{Guid.NewGuid():N}.db");
|
|
|
|
[Fact]
|
|
public async Task Migrate_IsIdempotent()
|
|
{
|
|
var factory = new SecretsSqliteConnectionFactory(_dbPath);
|
|
var migrator = new SqliteSecretsStoreMigrator(factory);
|
|
|
|
// Running twice must not throw and must leave the schema version at CurrentVersion.
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
|
|
long version = await ReadSchemaVersionAsync(factory);
|
|
Assert.Equal(SqliteSecretsSchema.CurrentVersion, version);
|
|
Assert.Equal(1, version);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Migrate_CreatesSecretTableWithExpectedColumns()
|
|
{
|
|
var factory = new SecretsSqliteConnectionFactory(_dbPath);
|
|
var migrator = new SqliteSecretsStoreMigrator(factory);
|
|
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
|
|
HashSet<string> columns = await ReadSecretColumnsAsync(factory);
|
|
|
|
string[] expected =
|
|
[
|
|
"name", "description", "content_type", "ciphertext", "nonce", "tag",
|
|
"wrapped_dek", "wrap_nonce", "wrap_tag", "kek_id", "revision",
|
|
"is_deleted", "deleted_utc", "created_utc", "updated_utc",
|
|
"created_by", "updated_by",
|
|
];
|
|
|
|
Assert.Equal(17, expected.Length);
|
|
Assert.Equal(17, columns.Count);
|
|
foreach (string column in expected)
|
|
{
|
|
Assert.Contains(column, columns);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Migrate_RefusesNewerSchema()
|
|
{
|
|
var factory = new SecretsSqliteConnectionFactory(_dbPath);
|
|
|
|
// Manually stamp an on-disk version newer than this build supports.
|
|
await using (SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None))
|
|
{
|
|
await using SqliteCommand create = connection.CreateCommand();
|
|
create.CommandText = SqliteSecretsSchema.CreateSchemaVersionTable;
|
|
await create.ExecuteNonQueryAsync();
|
|
|
|
await using SqliteCommand insert = connection.CreateCommand();
|
|
insert.CommandText = """
|
|
INSERT INTO schema_version (id, version, applied_utc)
|
|
VALUES (1, $version, $applied_utc);
|
|
""";
|
|
insert.Parameters.AddWithValue("$version", 999);
|
|
insert.Parameters.AddWithValue("$applied_utc", DateTimeOffset.UtcNow.ToString("O"));
|
|
await insert.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
var migrator = new SqliteSecretsStoreMigrator(factory);
|
|
|
|
await Assert.ThrowsAsync<SecretStoreMigrationException>(
|
|
() => migrator.MigrateAsync(CancellationToken.None));
|
|
}
|
|
|
|
private static async Task<long> ReadSchemaVersionAsync(SecretsSqliteConnectionFactory factory)
|
|
{
|
|
await using SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None);
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = "SELECT version FROM schema_version WHERE id = 1;";
|
|
object? result = await command.ExecuteScalarAsync();
|
|
return Convert.ToInt64(result);
|
|
}
|
|
|
|
private static async Task<HashSet<string>> ReadSecretColumnsAsync(SecretsSqliteConnectionFactory factory)
|
|
{
|
|
await using SqliteConnection connection = await factory.OpenConnectionAsync(CancellationToken.None);
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = $"PRAGMA table_info({SqliteSecretsSchema.SecretTable});";
|
|
|
|
var columns = new HashSet<string>(StringComparer.Ordinal);
|
|
await using SqliteDataReader reader = await command.ExecuteReaderAsync();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
// PRAGMA table_info columns: cid(0), name(1), type(2), ...
|
|
columns.Add(reader.GetString(1));
|
|
}
|
|
|
|
return columns;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Drop pooled connections so the WAL/-shm/-wal sidecars release before we delete.
|
|
SqliteConnection.ClearAllPools();
|
|
|
|
foreach (string path in new[] { _dbPath, _dbPath + "-wal", _dbPath + "-shm" })
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Best-effort temp cleanup; a leaked temp file is not a test failure.
|
|
}
|
|
}
|
|
}
|
|
}
|