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]")));
}
}