using Microsoft.Data.Sqlite; using ZB.MOM.WW.Auth.Abstractions.ApiKeys; using ZB.MOM.WW.Auth.ApiKeys.Sqlite; namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer.Tests.Live; /// /// Auth-008 (scadaproj#46), the live half: key ids match exactly, as they do in the SQLite store /// this one ports. The key_id column uses the database's default collation — case-insensitive /// on a stock SQL Server — so LINE1 found the row stored as line1, and the importer /// counted a SQLite Line1 beside line1 as an ordinary "skipped" duplicate. /// [Collection("live-sqlserver")] public sealed class KeyIdFidelityLiveTests : IAsyncLifetime { private readonly string _schema = LiveSqlServer.NewSchemaName(); private readonly string _sqlitePath = Path.Combine(Path.GetTempPath(), $"zbauth-keyid-{Guid.NewGuid():N}.db"); private AuthSqlServerConnectionFactory _factory = null!; public async Task InitializeAsync() { if (!LiveSqlServer.IsEnabled) return; _factory = new AuthSqlServerConnectionFactory(LiveSqlServer.OptionsFor(_schema)); await new SqlServerApiKeyStoreMigrator(_factory).MigrateAsync(CancellationToken.None); } public async Task DisposeAsync() { SqliteConnection.ClearAllPools(); foreach (string path in new[] { _sqlitePath, _sqlitePath + "-wal", _sqlitePath + "-shm" }) { if (File.Exists(path)) File.Delete(path); } await LiveSqlServer.DropSchemaAsync(_schema); } private static ApiKeyRecord Record(string keyId) => new( KeyId: keyId, KeyPrefix: "sbk", SecretHash: [1, 2, 3, 4], DisplayName: "Key " + keyId, Scopes: new HashSet(["read"], StringComparer.Ordinal), ConstraintsJson: null, CreatedUtc: new DateTimeOffset(2026, 5, 1, 8, 30, 0, TimeSpan.Zero), LastUsedUtc: null, RevokedUtc: null); [SkippableFact] public async Task Lookups_AreCaseSensitive_LikeTheSqliteStore() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); var admin = new SqlServerApiKeyAdminStore(_factory); var read = new SqlServerApiKeyStore(_factory); await admin.CreateAsync(Record("line1"), CancellationToken.None); Assert.NotNull(await read.FindByKeyIdAsync("line1", CancellationToken.None)); Assert.Null(await read.FindByKeyIdAsync("LINE1", CancellationToken.None)); Assert.Null(await read.FindActiveByKeyIdAsync("Line1", CancellationToken.None)); // Trailing spaces are significant too (SQL Server's '=' pads them away). Assert.Null(await read.FindByKeyIdAsync("line1 ", CancellationToken.None)); Assert.False(await admin.RevokeAsync("LINE1", DateTimeOffset.UtcNow, CancellationToken.None)); Assert.Null((await read.FindByKeyIdAsync("line1", CancellationToken.None))!.RevokedUtc); } [SkippableFact] public async Task Import_CaseVariantKeyIds_AreReportedAsCollisions_NotSkipped() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); var sqliteFactory = new AuthSqliteConnectionFactory(_sqlitePath); await new SqliteAuthStoreMigrator(sqliteFactory).MigrateAsync(CancellationToken.None); var sqliteAdmin = new SqliteApiKeyAdminStore(sqliteFactory); await sqliteAdmin.CreateAsync(Record("line1"), CancellationToken.None); await sqliteAdmin.CreateAsync(Record("Line1"), CancellationToken.None); ApiKeyImportResult result = await new SqliteApiKeyStoreImporter(_factory).ImportAsync(_sqlitePath, CancellationToken.None); Assert.Equal(1, result.KeysInserted); Assert.Equal(0, result.KeysSkipped); Assert.Single(result.KeyIdCollisions); // Re-running: the inserted one is now an ordinary skip, the other still a collision. ApiKeyImportResult again = await new SqliteApiKeyStoreImporter(_factory).ImportAsync(_sqlitePath, CancellationToken.None); Assert.Equal(0, again.KeysInserted); Assert.Equal(1, again.KeysSkipped); Assert.Single(again.KeyIdCollisions); } [SkippableFact] public async Task Import_OverLengthKey_IsRejected_NotTruncated() { Skip.IfNot(LiveSqlServer.IsEnabled, LiveSqlServer.SkipReason); string longId = new('k', 70); var sqliteFactory = new AuthSqliteConnectionFactory(_sqlitePath); await new SqliteAuthStoreMigrator(sqliteFactory).MigrateAsync(CancellationToken.None); var sqliteAdmin = new SqliteApiKeyAdminStore(sqliteFactory); await sqliteAdmin.CreateAsync(Record(longId), CancellationToken.None); await sqliteAdmin.CreateAsync(Record("ok-key"), CancellationToken.None); ApiKeyImportResult result = await new SqliteApiKeyStoreImporter(_factory).ImportAsync(_sqlitePath, CancellationToken.None); Assert.Equal(1, result.KeysInserted); Assert.Equal(longId, Assert.Single(result.KeysRejected)); Assert.Equal(1L, Convert.ToInt64(await LiveSqlServer.ScalarAsync($"SELECT COUNT(*) FROM [{_schema}].[api_keys]"))); } }