From 290e85cb38aa454017e5a6b179842477e602c606 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 2 Jun 2026 03:12:24 -0400 Subject: [PATCH] test(auth.apikeys): store-level arg guards + SetEnabledAsync idempotence (review M1/M2) --- .../SqliteApiKeyAdminStoreTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.ApiKeys.Tests/SqliteApiKeyAdminStoreTests.cs b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.ApiKeys.Tests/SqliteApiKeyAdminStoreTests.cs index b1692c3..8f1bf3f 100644 --- a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.ApiKeys.Tests/SqliteApiKeyAdminStoreTests.cs +++ b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.ApiKeys.Tests/SqliteApiKeyAdminStoreTests.cs @@ -253,6 +253,73 @@ public sealed class SqliteApiKeyAdminStoreTests : IAsyncLifetime () => _admin.DeleteAsync(keyId!, CancellationToken.None)); } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SetScopesAsync_NullOrWhitespaceKeyId_ThrowsArgumentException(string? keyId) + { + await Assert.ThrowsAnyAsync( + () => _admin.SetScopesAsync( + keyId!, + new HashSet(["read"], StringComparer.Ordinal), + CancellationToken.None)); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SetEnabledAsync_NullOrWhitespaceKeyId_ThrowsArgumentException(string? keyId) + { + await Assert.ThrowsAnyAsync( + () => _admin.SetEnabledAsync(keyId!, enabled: false, DateTimeOffset.UtcNow, CancellationToken.None)); + } + + [Fact] + public async Task SetScopesAsync_NullScopes_ThrowsArgumentNullException() + { + await _admin.CreateAsync(SampleRecord("key-1"), CancellationToken.None); + + await Assert.ThrowsAsync( + () => _admin.SetScopesAsync("key-1", null!, CancellationToken.None)); + } + + // --- SetEnabled idempotence --- + + [Fact] + public async Task SetEnabledAsync_OnAlreadyActiveKey_ReturnsTrue() + { + await _admin.CreateAsync(SampleRecord("key-1"), CancellationToken.None); + + bool result = await _admin.SetEnabledAsync( + "key-1", enabled: true, DateTimeOffset.UtcNow, CancellationToken.None); + + Assert.True(result); + ApiKeyRecord? active = await _read.FindActiveByKeyIdAsync("key-1", CancellationToken.None); + Assert.NotNull(active); + Assert.Null(active!.RevokedUtc); + } + + [Fact] + public async Task SetEnabledAsync_OnAlreadyDisabledKey_OverwritesTimestamp_ReturnsTrue() + { + await _admin.CreateAsync(SampleRecord("key-1"), CancellationToken.None); + var t1 = new DateTimeOffset(2026, 5, 1, 10, 0, 0, TimeSpan.Zero); + var t2 = new DateTimeOffset(2026, 5, 15, 10, 0, 0, TimeSpan.Zero); + + // Disable at t1. + await _admin.SetEnabledAsync("key-1", enabled: false, t1, CancellationToken.None); + + // Disable again at a later t2 (idempotent overwrite — no guard on revoked_utc). + bool result = await _admin.SetEnabledAsync("key-1", enabled: false, t2, CancellationToken.None); + + Assert.True(result); + IReadOnlyList listed = await _admin.ListAsync(CancellationToken.None); + ApiKeyListItem item = Assert.Single(listed, k => k.KeyId == "key-1"); + Assert.Equal(t2, item.RevokedUtc); + } + // --- Audit --- [Fact]