feat(auth): typed ApiKeyPepperUnavailableException for missing API-key pepper; bump to 0.4.2 (scadaproj#23)
Derives from InvalidOperationException so existing catches keep working. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -163,7 +163,7 @@ public sealed class ApiKeyAdminCommandsTests : IAsyncLifetime
|
||||
|
||||
int auditCountBefore = (await _audit.ListRecentAsync(50, CancellationToken.None)).Count;
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => commands.CreateKeyAsync(
|
||||
await Assert.ThrowsAsync<ApiKeyPepperUnavailableException>(() => commands.CreateKeyAsync(
|
||||
"key-x",
|
||||
"No Pepper",
|
||||
new HashSet<string>(StringComparer.Ordinal),
|
||||
@@ -297,6 +297,52 @@ public sealed class ApiKeyAdminCommandsTests : IAsyncLifetime
|
||||
Assert.Equal("not-found", auditRow.Details);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PepperUnavailable_IsStillCatchableAsInvalidOperationException_WithOriginalMessage()
|
||||
{
|
||||
// Back-compat: consumers that caught InvalidOperationException before the typed exception
|
||||
// existed must keep working, for both create and rotate.
|
||||
ApiKeyAdminCommands setup = BuildCommands(pepper: Pepper);
|
||||
await setup.InitDbAsync(null, CancellationToken.None);
|
||||
await setup.CreateKeyAsync("key-bc", "BC", new HashSet<string>(StringComparer.Ordinal), null, null, CancellationToken.None);
|
||||
ApiKeyAdminCommands nopepper = BuildCommands(pepper: null);
|
||||
|
||||
InvalidOperationException create = await Assert.ThrowsAnyAsync<InvalidOperationException>(() =>
|
||||
nopepper.CreateKeyAsync("key-bc2", "BC2", new HashSet<string>(StringComparer.Ordinal), null, null, CancellationToken.None));
|
||||
InvalidOperationException rotate = await Assert.ThrowsAnyAsync<InvalidOperationException>(() =>
|
||||
nopepper.RotateKeyAsync("key-bc", null, CancellationToken.None));
|
||||
|
||||
Assert.IsType<ApiKeyPepperUnavailableException>(create);
|
||||
Assert.IsType<ApiKeyPepperUnavailableException>(rotate);
|
||||
Assert.Equal("pepper unavailable", create.Message);
|
||||
Assert.Equal("pepper unavailable", rotate.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task CreateKey_NullEmptyOrWhitespacePepper_ThrowsTypedException(string? pepper)
|
||||
{
|
||||
ApiKeyAdminCommands commands = BuildCommands(pepper);
|
||||
await new ApiKeyAdminCommands(_options, _admin, _audit, new FakePepperProvider(Pepper), _migrator)
|
||||
.InitDbAsync(null, CancellationToken.None);
|
||||
|
||||
await Assert.ThrowsAsync<ApiKeyPepperUnavailableException>(() => commands.CreateKeyAsync(
|
||||
"key-w", "W", new HashSet<string>(StringComparer.Ordinal), null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PepperUnavailableException_ConstructorOverloads_AreInvalidOperationExceptions()
|
||||
{
|
||||
var inner = new Exception("inner");
|
||||
Assert.Equal("pepper unavailable", new ApiKeyPepperUnavailableException().Message);
|
||||
Assert.Equal("custom", new ApiKeyPepperUnavailableException("custom").Message);
|
||||
var withInner = new ApiKeyPepperUnavailableException("m", inner);
|
||||
Assert.Same(inner, withInner.InnerException);
|
||||
Assert.IsAssignableFrom<InvalidOperationException>(withInner);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RotateKey_PepperUnavailable_Throws_HashUnchanged_AndAppendsNoAudit()
|
||||
{
|
||||
@@ -319,7 +365,7 @@ public sealed class ApiKeyAdminCommandsTests : IAsyncLifetime
|
||||
|
||||
// Act: rotate with no pepper available.
|
||||
ApiKeyAdminCommands nopepper = BuildCommands(pepper: null);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
await Assert.ThrowsAsync<ApiKeyPepperUnavailableException>(() =>
|
||||
nopepper.RotateKeyAsync("key-1", null, CancellationToken.None));
|
||||
|
||||
// Assert: stored hash is unchanged.
|
||||
|
||||
Reference in New Issue
Block a user