da669bfc9b
The store was extracted from MxAccessGateway, whose deployed gateway-auth.db is at schema_version=2. The library capped at 1 and threw on a newer on-disk version -> gateway would fail to boot. Final schema is byte-identical since v1; stamp 2 so existing deployed DBs interoperate (no key re-issuance). +2 tests.
135 lines
4.9 KiB
C#
135 lines
4.9 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using ZB.MOM.WW.Auth.ApiKeys.Sqlite;
|
|
|
|
namespace ZB.MOM.WW.Auth.ApiKeys.Tests;
|
|
|
|
public sealed class SqliteMigratorTests : IDisposable
|
|
{
|
|
private readonly string _dbPath =
|
|
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".db");
|
|
|
|
private AuthSqliteConnectionFactory Factory => new(_dbPath);
|
|
|
|
[Fact]
|
|
public async Task MigrateAsync_CreatesAllThreeTables()
|
|
{
|
|
var migrator = new SqliteAuthStoreMigrator(Factory);
|
|
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
|
|
Assert.True(await TableExistsAsync(SqliteAuthSchema.ApiKeysTable));
|
|
Assert.True(await TableExistsAsync(SqliteAuthSchema.ApiKeyAuditTable));
|
|
Assert.True(await TableExistsAsync(SqliteAuthSchema.SchemaVersionTable));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MigrateAsync_RunTwice_IsIdempotentAndRecordsCurrentVersion()
|
|
{
|
|
var migrator = new SqliteAuthStoreMigrator(Factory);
|
|
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(SqliteAuthSchema.CurrentVersion, await ReadVersionAsync());
|
|
Assert.Equal(1, await CountSchemaVersionRowsAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentVersion_Is2_ToMatchDonorGatewayDeployedSchema() =>
|
|
// The store was extracted from MxAccessGateway, whose deployed gateway-auth.db is
|
|
// stamped version 2. The library must stamp 2 (not reset to 1) so it does not refuse
|
|
// those existing databases on first boot. Locking this invariant.
|
|
Assert.Equal(2, SqliteAuthSchema.CurrentVersion);
|
|
|
|
[Fact]
|
|
public async Task MigrateAsync_AgainstExistingVersion2Db_DoesNotThrow_AndStaysAt2()
|
|
{
|
|
// The deployed-gateway scenario: a database already provisioned at version 2.
|
|
var migrator = new SqliteAuthStoreMigrator(Factory);
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
await SetVersionAsync(2);
|
|
|
|
await migrator.MigrateAsync(CancellationToken.None); // must not throw
|
|
|
|
Assert.Equal(2, await ReadVersionAsync());
|
|
Assert.True(await TableExistsAsync(SqliteAuthSchema.ApiKeysTable));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task MigrateAsync_FutureSchemaVersion_Throws()
|
|
{
|
|
var migrator = new SqliteAuthStoreMigrator(Factory);
|
|
await migrator.MigrateAsync(CancellationToken.None);
|
|
|
|
await SetVersionAsync(99);
|
|
|
|
await Assert.ThrowsAsync<AuthStoreMigrationException>(
|
|
() => migrator.MigrateAsync(CancellationToken.None));
|
|
}
|
|
|
|
private async Task<bool> TableExistsAsync(string tableName)
|
|
{
|
|
await using SqliteConnection connection =
|
|
await Factory.OpenConnectionAsync(CancellationToken.None);
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText =
|
|
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = $name;";
|
|
command.Parameters.AddWithValue("$name", tableName);
|
|
long count = (long)(await command.ExecuteScalarAsync(CancellationToken.None) ?? 0L);
|
|
return count == 1;
|
|
}
|
|
|
|
private async Task<int> ReadVersionAsync()
|
|
{
|
|
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? value = await command.ExecuteScalarAsync(CancellationToken.None);
|
|
return Convert.ToInt32(value, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private async Task<int> CountSchemaVersionRowsAsync()
|
|
{
|
|
await using SqliteConnection connection =
|
|
await Factory.OpenConnectionAsync(CancellationToken.None);
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = "SELECT COUNT(*) FROM schema_version;";
|
|
long count = (long)(await command.ExecuteScalarAsync(CancellationToken.None) ?? 0L);
|
|
return (int)count;
|
|
}
|
|
|
|
private async Task SetVersionAsync(int version)
|
|
{
|
|
await using SqliteConnection connection =
|
|
await Factory.OpenConnectionAsync(CancellationToken.None);
|
|
await using SqliteCommand command = connection.CreateCommand();
|
|
command.CommandText = "UPDATE schema_version SET version = $version WHERE id = 1;";
|
|
command.Parameters.AddWithValue("$version", version);
|
|
await command.ExecuteNonQueryAsync(CancellationToken.None);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
SqliteConnection.ClearAllPools();
|
|
TryDelete(_dbPath);
|
|
TryDelete(_dbPath + "-wal");
|
|
TryDelete(_dbPath + "-shm");
|
|
}
|
|
|
|
private static void TryDelete(string path)
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
// Best-effort cleanup of the per-test temp database.
|
|
}
|
|
}
|
|
}
|