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