using ZB.MOM.WW.Auth.Abstractions.ApiKeys; namespace ZB.MOM.WW.Auth.ApiKeys.SqlServer.Tests; /// /// Auth-008 (scadaproj#46), the offline half: a value longer than its column is rejected or treated /// as absent BEFORE any connection is opened. Every SqlParameter carries an explicit size, and /// SqlClient truncates an over-length value to that size silently — so a 70-character key id was /// stored as its first 64 characters, and a different 70-character id sharing that prefix then /// "found" it. These tests point at a refused loopback port: pre-fix they reach the network and throw /// SqlException; post-fix nothing is sent. /// public sealed class SqlServerColumnLimitTests { private static readonly AuthSqlServerConnectionFactory Unreachable = new(new SqlServerApiKeyOptions { ConnectionString = "Server=tcp:127.0.0.1,1;Connect Timeout=3;ConnectRetryCount=0;TrustServerCertificate=true", }); private static readonly string LongKeyId = new('k', 65); private static ApiKeyRecord Record(string keyId = "key-1", string prefix = "sbk", string displayName = "Key") => new( KeyId: keyId, KeyPrefix: prefix, SecretHash: [1, 2, 3], DisplayName: displayName, Scopes: new HashSet(StringComparer.Ordinal), ConstraintsJson: null, CreatedUtc: DateTimeOffset.UnixEpoch, LastUsedUtc: null, RevokedUtc: null); public static TheoryData OverLength() => new() { { Record(keyId: LongKeyId), "KeyId" }, { Record(prefix: new string('p', 17)), "KeyPrefix" }, { Record(displayName: new string('d', 257)), "DisplayName" }, }; [Theory] [MemberData(nameof(OverLength))] public async Task Create_OverLengthValue_IsRejected_NotTruncated(ApiKeyRecord record, string field) { var store = new SqlServerApiKeyAdminStore(Unreachable); var ex = await Assert.ThrowsAsync(() => store.CreateAsync(record, CancellationToken.None)); Assert.Contains(field, ex.Message, StringComparison.Ordinal); } [Fact] public async Task Create_AtTheColumnLimits_ReachesTheDatabase() { // Positive control: values exactly at the limits are not rejected — the call gets as far as // the (unreachable) server. var store = new SqlServerApiKeyAdminStore(Unreachable); var record = Record(keyId: new string('k', 64), prefix: new string('p', 16), displayName: new string('d', 256)); await Assert.ThrowsAsync(() => store.CreateAsync(record, CancellationToken.None)); } [Fact] public async Task Lookups_OfAnOverLengthKeyId_FindNothing_WithoutQuerying() { var read = new SqlServerApiKeyStore(Unreachable); var admin = new SqlServerApiKeyAdminStore(Unreachable); var now = DateTimeOffset.UnixEpoch; Assert.Null(await read.FindByKeyIdAsync(LongKeyId, CancellationToken.None)); Assert.Null(await read.FindActiveByKeyIdAsync(LongKeyId, CancellationToken.None)); await read.MarkUsedAsync(LongKeyId, now, CancellationToken.None); Assert.False(await admin.RevokeAsync(LongKeyId, now, CancellationToken.None)); Assert.False(await admin.RotateAsync(LongKeyId, [1], CancellationToken.None)); Assert.False(await admin.SetScopesAsync(LongKeyId, new HashSet(), CancellationToken.None)); Assert.False(await admin.SetEnabledAsync(LongKeyId, true, now, CancellationToken.None)); Assert.False(await admin.DeleteAsync(LongKeyId, CancellationToken.None)); } }