You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
16 KiB
459 lines
16 KiB
using System.Text; |
|
using Bit.Core.Auth.Identity.TokenProviders; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using Microsoft.Extensions.Caching.Distributed; |
|
using Microsoft.Extensions.Options; |
|
using NSubstitute; |
|
using Xunit; |
|
|
|
namespace Bit.Core.Test.Auth.Identity; |
|
|
|
[SutProviderCustomize] |
|
public class OtpTokenProviderTests |
|
{ |
|
private readonly string _defaultTokenProviderName = "DefaultOtpProvider"; |
|
|
|
private readonly DefaultOtpTokenProviderOptions _defaultOtpTokenProviderOptions = new() |
|
{ |
|
TokenLength = 6, |
|
TokenAlpha = false, |
|
TokenNumeric = true |
|
}; |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_Success_ReturnsToken( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(_defaultOtpTokenProviderOptions); |
|
sutProvider.Create(); |
|
|
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.NotNull(result); |
|
Assert.NotEmpty(result); |
|
Assert.Equal(6, result.Length); // Default length |
|
Assert.True(result.All(char.IsDigit)); // Default is numeric only |
|
|
|
// Verify cache was called with correct key |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.Received(1) |
|
.SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_CustomConfiguration_ReturnsCorrectFormat( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string tokenProviderName, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
var otpConfig = new DefaultOtpTokenProviderOptions |
|
{ |
|
TokenLength = 8, |
|
TokenAlpha = true, |
|
TokenNumeric = true |
|
}; |
|
|
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(otpConfig); |
|
sutProvider.Create(); |
|
|
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(tokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.NotNull(result); |
|
Assert.Equal(8, result.Length); |
|
Assert.Contains(result, char.IsLetterOrDigit); |
|
} |
|
|
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_NumericOnly_ReturnsOnlyDigits( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
var otpConfig = new DefaultOtpTokenProviderOptions |
|
{ |
|
TokenLength = 10, |
|
TokenAlpha = false, |
|
TokenNumeric = true |
|
}; |
|
|
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(otpConfig); |
|
sutProvider.Create(); |
|
|
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.Equal(10, result.Length); |
|
Assert.True(result.All(char.IsDigit)); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_ValidToken_ReturnsTrue( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier, |
|
string token) |
|
{ |
|
// Arrange |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
var tokenBytes = Encoding.UTF8.GetBytes(token); |
|
|
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns(tokenBytes); |
|
|
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.True(result); |
|
|
|
// Verify token was removed from cache after successful validation |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.Received(1) |
|
.RemoveAsync(expectedCacheKey); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_InvalidToken_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier, |
|
string token, |
|
string wrongToken) |
|
{ |
|
// Arrange |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
var tokenBytes = Encoding.UTF8.GetBytes(wrongToken); // Different token in cache |
|
|
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns(tokenBytes); |
|
|
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify token was NOT removed from cache for invalid validation |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.RemoveAsync(expectedCacheKey); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_TokenNotFound_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier, |
|
string token) |
|
{ |
|
// Arrange |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
|
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns((byte[])null); // Token not found in cache |
|
|
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify removal was not attempted |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.RemoveAsync(Arg.Any<string>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_EmptyToken_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync("", _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_NullToken_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(null, _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
} |
|
|
|
// Tests for null/empty purpose and uniqueIdentifier parameters |
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_NullPurpose_ReturnsNull( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, null, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.Null(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_EmptyPurpose_ReturnsNull( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, "", uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.Null(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_NullUniqueIdentifier_ReturnsNull( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, null); |
|
|
|
// Assert |
|
Assert.Null(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_EmptyUniqueIdentifier_ReturnsNull( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, ""); |
|
|
|
// Assert |
|
Assert.Null(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.SetAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_NullPurpose_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string token, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, null, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.GetAsync(Arg.Any<string>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_EmptyPurpose_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string token, |
|
string uniqueIdentifier) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, "", uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.GetAsync(Arg.Any<string>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_NullUniqueIdentifier_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string token, |
|
string purpose) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, null); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.GetAsync(Arg.Any<string>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_EmptyUniqueIdentifier_ReturnsFalse( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string token, |
|
string purpose) |
|
{ |
|
// Act |
|
var result = await sutProvider.Sut.ValidateTokenAsync(token, _defaultTokenProviderName, purpose, ""); |
|
|
|
// Assert |
|
Assert.False(result); |
|
|
|
// Verify cache was not called |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.DidNotReceive() |
|
.GetAsync(Arg.Any<string>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task GenerateTokenAsync_OverwritesExistingToken( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(_defaultOtpTokenProviderOptions); |
|
sutProvider.Create(); |
|
|
|
// Act - Generate token twice with same parameters |
|
var firstToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
var secondToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.NotEqual(firstToken, secondToken); // Should be different tokens |
|
|
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.Received(2) // Called twice - once for each generation |
|
.SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task CacheKeyFormat_IsCorrect( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(_defaultOtpTokenProviderOptions); |
|
sutProvider.Create(); |
|
|
|
// Act |
|
await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
await sutProvider.GetDependency<IDistributedCache>() |
|
.Received(1) |
|
.SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>()); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task ValidateTokenAsync_CaseSensitive( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
var token = "ABC123"; |
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
var tokenBytes = Encoding.UTF8.GetBytes(token); |
|
|
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns(tokenBytes); |
|
|
|
// Act & Assert |
|
var validResult = await sutProvider.Sut.ValidateTokenAsync("ABC123", _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
Assert.True(validResult); |
|
|
|
// Reset the cache mock to return the token again |
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns(tokenBytes); |
|
|
|
var invalidResult = await sutProvider.Sut.ValidateTokenAsync("abc123", _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
Assert.False(invalidResult); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task RoundTrip_GenerateAndValidate_Success( |
|
SutProvider<OtpTokenProvider<DefaultOtpTokenProviderOptions>> sutProvider, |
|
string purpose, |
|
string uniqueIdentifier) |
|
{ |
|
// Arrange |
|
sutProvider.GetDependency<IOptions<DefaultOtpTokenProviderOptions>>() |
|
.Value.Returns(_defaultOtpTokenProviderOptions); |
|
sutProvider.Create(); |
|
|
|
var expectedCacheKey = $"{_defaultTokenProviderName}_{purpose}_{uniqueIdentifier}"; |
|
byte[] storedToken = null; |
|
|
|
// Setup cache to capture stored token and return it on get |
|
sutProvider.GetDependency<IDistributedCache>() |
|
.When(x => x.SetAsync(expectedCacheKey, Arg.Any<byte[]>(), Arg.Any<DistributedCacheEntryOptions>())) |
|
.Do(callInfo => storedToken = callInfo.ArgAt<byte[]>(1)); |
|
|
|
sutProvider.GetDependency<IDistributedCache>() |
|
.GetAsync(expectedCacheKey) |
|
.Returns(callInfo => storedToken); |
|
|
|
// Act |
|
var generatedToken = await sutProvider.Sut.GenerateTokenAsync(_defaultTokenProviderName, purpose, uniqueIdentifier); |
|
var isValid = await sutProvider.Sut.ValidateTokenAsync(generatedToken, _defaultTokenProviderName, purpose, uniqueIdentifier); |
|
|
|
// Assert |
|
Assert.True(isValid); |
|
Assert.NotNull(generatedToken); |
|
Assert.NotEmpty(generatedToken); |
|
} |
|
}
|
|
|