Merge pull request #2298 from DuendeSoftware/jmdc/aaj-test-cleanup

Remove NSubstitute from JwtBearer extensions
This commit is contained in:
Joe DeCock 2025-12-10 12:51:06 -06:00 committed by GitHub
commit 1a93ff3ed6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 94 additions and 13 deletions

View file

@ -17,7 +17,6 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Meziantou.Extensions.Logging.Xunit" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="RichardSzalay.MockHttp" />
</ItemGroup>

View file

@ -2,7 +2,6 @@
// See LICENSE in the project root for license information.
using Duende.IdentityModel;
using NSubstitute;
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP;
@ -16,5 +15,9 @@ public static class AssertionExtensions
result.Error.ShouldBe(OidcConstants.TokenErrors.InvalidDPoPProof);
}
public static void ReplayCacheShouldNotBeCalled(this TestDPoPProofValidator validator) => validator.TestReplayCache.DidNotReceive().Add(Arg.Any<string>(), Arg.Any<TimeSpan>());
public static void ReplayCacheShouldNotBeCalled(this TestDPoPProofValidator validator)
{
var mockCache = (TestReplayCache)validator.TestReplayCache;
mockCache.VerifyAddWasNotCalled();
}
}

View file

@ -6,11 +6,10 @@ using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Duende.AspNetCore.Authentication.JwtBearer.DPoP.TestFramework;
using Duende.IdentityModel;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using NSubstitute;
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP;
@ -37,12 +36,11 @@ public abstract class DPoPProofValidatorTestBase
protected DPoPProofValidationContext Context;
protected DPoPOptions Options = new();
protected IReplayCache ReplayCache = Substitute.For<IReplayCache>();
protected TestReplayCache ReplayCache = new();
public TestDPoPProofValidator CreateProofValidator()
{
var optionsMonitor = Substitute.For<IOptionsMonitor<DPoPOptions>>();
optionsMonitor.Get(Arg.Any<string>()).Returns(Options);
var optionsMonitor = new TestOptionsMonitor<DPoPOptions>(Options);
return new TestDPoPProofValidator(
optionsMonitor,

View file

@ -1,8 +1,6 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using NSubstitute;
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP;
public class ReplayTests : DPoPProofValidatorTestBase
@ -11,7 +9,7 @@ public class ReplayTests : DPoPProofValidatorTestBase
[Trait("Category", "Unit")]
public async Task replays_detected_in_ValidateReplay_fail()
{
ReplayCache.Exists(TokenIdHash).Returns(true);
ReplayCache.ExistsFunc = jti => jti == TokenIdHash;
Result.TokenIdHash = TokenIdHash;
await ProofValidator.ValidateReplay(Context, Result);
@ -28,7 +26,7 @@ public class ReplayTests : DPoPProofValidatorTestBase
[InlineData(true, true, ClockSkew * 2, ClockSkew * 2)]
public async Task new_proof_tokens_are_added_to_replay_cache(bool validateIat, bool validateNonce, int clientClockSkew, int serverClockSkew)
{
ReplayCache.Exists(TokenIdHash).Returns(false);
ReplayCache.ExistsFunc = _ => false;
Options.ValidationMode = (validateIat && validateNonce) ? ExpirationValidationMode.Both
: validateIat ? ExpirationValidationMode.IssuedAt : ExpirationValidationMode.Nonce;
@ -46,6 +44,6 @@ public class ReplayTests : DPoPProofValidatorTestBase
: (validateIat ? clientClockSkew : serverClockSkew);
var expectedExpiration = TimeSpan.FromSeconds(skew * 2)
.Add(TimeSpan.FromSeconds(ValidFor));
await ReplayCache.Received().Add(TokenIdHash, expectedExpiration);
ReplayCache.VerifyAddWasCalled(TokenIdHash, expectedExpiration);
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
using Microsoft.Extensions.Options;
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP.TestFramework;
public class TestOptionsMonitor<T>(T options) : IOptionsMonitor<T>
{
private readonly T _options = options;
public T CurrentValue => _options;
public T Get(string? name) => _options;
public IDisposable? OnChange(Action<T, string?> listener) => null;
}

View file

@ -0,0 +1,66 @@
// Copyright (c) Duende Software. All rights reserved.
// See LICENSE in the project root for license information.
namespace Duende.AspNetCore.Authentication.JwtBearer.DPoP;
public class TestReplayCache : IReplayCache
{
private readonly Dictionary<string, (TimeSpan expiration, DateTime addedAt)> _cache = new();
private readonly List<(string jtiHash, TimeSpan expiration)> _addCalls = new();
private readonly List<string> _existsCalls = new();
// Configuration for test behavior
public Func<string, bool>? ExistsFunc { get; set; }
public Task Add(string jtiHash, TimeSpan expiration, CancellationToken cancellationToken = default)
{
_addCalls.Add((jtiHash, expiration));
_cache[jtiHash] = (expiration, DateTime.UtcNow);
return Task.CompletedTask;
}
public Task<bool> Exists(string jtiHash, CancellationToken cancellationToken = default)
{
_existsCalls.Add(jtiHash);
if (ExistsFunc != null)
{
return Task.FromResult(ExistsFunc(jtiHash));
}
return Task.FromResult(_cache.ContainsKey(jtiHash));
}
// Verification methods
public void VerifyAddWasCalled(string jtiHash, TimeSpan expectedExpiration)
{
var call = _addCalls.FirstOrDefault(c => c.jtiHash == jtiHash);
if (call == default)
{
throw new Exception($"Add was not called with jtiHash: {jtiHash}");
}
if (call.expiration != expectedExpiration)
{
throw new Exception($"Add was called with wrong expiration. Expected: {expectedExpiration}, Actual: {call.expiration}");
}
}
public void VerifyAddWasNotCalled()
{
if (_addCalls.Count > 0)
{
throw new Exception($"Add was called {_addCalls.Count} time(s) but should not have been called");
}
}
public bool WasAddCalled => _addCalls.Count > 0;
public IReadOnlyList<(string jtiHash, TimeSpan expiration)> AddCalls => _addCalls;
public void Clear()
{
_cache.Clear();
_addCalls.Clear();
_existsCalls.Clear();
}
}