using System.Globalization; using ZB.MOM.WW.Auth.ApiKeys.Sqlite; namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer.Tests.Live; /// /// Live provisioning tests for . /// /// /// Every test provisions its own throwaway schema — the concurrency case in particular needs a /// virgin one, because the race the retry loop exists for only happens while the schema and tables /// do not yet exist. All schemas created by the class are dropped in . /// [Collection("live-sqlserver")] public sealed class SqlServerApiKeyStoreMigratorLiveTests : IAsyncLifetime { private readonly List _schemas = []; /// public Task InitializeAsync() => Task.CompletedTask; /// public async Task DisposeAsync() { foreach (string schema in _schemas) { await LiveSqlServer.DropSchemaAsync(schema); } } private string NewSchema() { string schema = LiveSqlServer.NewSchemaName(); _schemas.Add(schema); return schema; } private static SqlServerApiKeyStoreMigrator MigratorFor(string schema) => new(new AuthSqlServerConnectionFactory(LiveSqlServer.OptionsFor(schema))); [SkippableFact] public async Task Migrate_OnEmptyDatabase_CreatesSchemaTablesAndVersionRow() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); string schema = NewSchema(); await MigratorFor(schema).MigrateAsync(CancellationToken.None); Assert.Equal( 1, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT COUNT(*) FROM sys.schemas WHERE name = N'{schema}'"), CultureInfo.InvariantCulture)); foreach (string table in new[] { SqlServerAuthSchema.SchemaVersionTable, SqlServerAuthSchema.ApiKeysTable, SqlServerAuthSchema.ApiKeyAuditTable, }) { Assert.NotNull( await LiveSqlServer.ScalarAsync($"SELECT OBJECT_ID(N'[{schema}].[{table}]', N'U')")); } Assert.Equal( SqlServerAuthSchema.CurrentVersion, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT version FROM [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}] WHERE id = 1"), CultureInfo.InvariantCulture)); } [SkippableFact] public async Task Migrate_Twice_IsIdempotent() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); string schema = NewSchema(); SqlServerApiKeyStoreMigrator migrator = MigratorFor(schema); await migrator.MigrateAsync(CancellationToken.None); await migrator.MigrateAsync(CancellationToken.None); Assert.Equal( 1, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT COUNT(*) FROM [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}]"), CultureInfo.InvariantCulture)); Assert.Equal( SqlServerAuthSchema.CurrentVersion, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT version FROM [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}] WHERE id = 1"), CultureInfo.InvariantCulture)); } [SkippableFact] public async Task Migrate_Concurrently_FromEightTasks_AllSucceed() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); // A virgin schema provisioned by eight nodes at once — the central-pair first boot, scaled // up. Each task gets its own factory (and therefore its own connection) so the DDL really // does run on eight sessions. string schema = NewSchema(); await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => Task.Run(() => MigratorFor(schema).MigrateAsync(CancellationToken.None)))); Assert.Equal( 1, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT COUNT(*) FROM [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}]"), CultureInfo.InvariantCulture)); Assert.Equal( SqlServerAuthSchema.CurrentVersion, Convert.ToInt32( await LiveSqlServer.ScalarAsync( $"SELECT version FROM [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}] WHERE id = 1"), CultureInfo.InvariantCulture)); } [SkippableFact] public async Task Migrate_WhenStoredVersionIsNewer_Throws() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); string schema = NewSchema(); SqlServerApiKeyStoreMigrator migrator = MigratorFor(schema); await migrator.MigrateAsync(CancellationToken.None); await LiveSqlServer.ExecuteAsync( $"UPDATE [{schema}].[{SqlServerAuthSchema.SchemaVersionTable}] SET version = 99 WHERE id = 1"); AuthStoreMigrationException exception = await Assert.ThrowsAsync( () => migrator.MigrateAsync(CancellationToken.None)); Assert.Contains("99", exception.Message, StringComparison.Ordinal); } }