198 lines
7.5 KiB
C#
198 lines
7.5 KiB
C#
using System.Diagnostics;
|
|
using adas_core.Application.Services.Caching;
|
|
using Moq;
|
|
using StackExchange.Redis;
|
|
|
|
namespace adas_core.Test.Services;
|
|
|
|
[TestFixture]
|
|
public class RedisLockProviderTest
|
|
{
|
|
private Mock<IDatabase> _mockDb = null!;
|
|
private RedisLockProvider _provider = null!;
|
|
|
|
/// <summary>
|
|
/// Initializes the test environment by creating a mock <see cref="IDatabase"/> and instantiating a <see cref="RedisLockProvider"/> configured to use the mocked database.
|
|
/// </summary>
|
|
[SetUp]
|
|
public void SetUp()
|
|
{
|
|
_mockDb = new Mock<IDatabase>();
|
|
_provider = new RedisLockProvider(() => _mockDb.Object);
|
|
}
|
|
|
|
#region TC-45
|
|
/// <summary>
|
|
/// Verifies that <c>AcquireAsync</c> returns <c>true</c>, stores the lock token under the <c>lock:</c> prefixed key, applies the configured TTL, and uses the <c>When.NotExists</c> flag when Redis accepts the NX SET operation. Also asserts that a subsequent <c>ReleaseAsync</c> invokes the Redis release script.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task AcquireAsync_ReturnsTrue_AndStoresToken_WhenRedisAcceptsNxSet()
|
|
{
|
|
_mockDb
|
|
.Setup(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()))
|
|
.ReturnsAsync(true);
|
|
|
|
_mockDb
|
|
.Setup(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<RedisKey[]?>(),
|
|
It.IsAny<RedisValue[]?>(),
|
|
It.IsAny<CommandFlags>()))
|
|
.Returns(Task.FromResult<RedisResult>(null!));
|
|
|
|
var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
|
|
|
|
Assert.That(result, Is.True);
|
|
|
|
_mockDb.Verify(db => db.StringSetAsync(
|
|
It.Is<RedisKey>(k => k == (RedisKey)"lock:key"),
|
|
It.IsAny<RedisValue>(),
|
|
(TimeSpan?)TimeSpan.FromSeconds(5),
|
|
When.NotExists), Times.Once());
|
|
|
|
await _provider.ReleaseAsync("key");
|
|
|
|
_mockDb.Verify(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<RedisKey[]?>(),
|
|
It.IsAny<RedisValue[]?>(),
|
|
It.IsAny<CommandFlags>()), Times.Once());
|
|
}
|
|
#endregion
|
|
|
|
#region TC-46
|
|
/// <summary>
|
|
/// Verifies that <c>AcquireAsync</c> retries the underlying Nx-style <c>StringSetAsync</c> call with delays
|
|
/// between attempts when the operation initially fails, continuing until it succeeds.
|
|
/// Asserts the call is retried the expected number of times and that the elapsed time confirms
|
|
/// delays were actually applied between attempts.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task AcquireAsync_RetriesWithDelays_UntilNxSucceeds()
|
|
{
|
|
_mockDb
|
|
.SetupSequence(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()))
|
|
.ReturnsAsync(false)
|
|
.ReturnsAsync(false)
|
|
.ReturnsAsync(false)
|
|
.ReturnsAsync(true);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1));
|
|
sw.Stop();
|
|
|
|
Assert.That(result, Is.True);
|
|
|
|
_mockDb.Verify(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()), Times.Exactly(4));
|
|
|
|
Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100));
|
|
}
|
|
#endregion
|
|
|
|
#region TC-47
|
|
/// <summary>
|
|
/// Verifies that AcquireAsync returns <c>false</c> and performs multiple retry attempts when the underlying lock acquisition operation consistently fails within the specified timeout.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task AcquireAsync_ReturnsFalse_AfterTimeoutWithMultipleRetries()
|
|
{
|
|
_mockDb
|
|
.Setup(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()))
|
|
.ReturnsAsync(false);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(200));
|
|
sw.Stop();
|
|
|
|
Assert.That(result, Is.False);
|
|
Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100));
|
|
|
|
_mockDb.Verify(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()), Times.AtLeast(2));
|
|
}
|
|
#endregion
|
|
|
|
#region TC-48
|
|
/// <summary>
|
|
/// Verifies that releasing a lock invokes the Lua script evaluation with the correct lock key (prefixed with "lock:") and the token previously captured during lock acquisition.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReleaseAsync_CallsScriptEvaluateWithCorrectKeyAndToken()
|
|
{
|
|
RedisValue capturedToken = default;
|
|
|
|
_mockDb
|
|
.Setup(db => db.StringSetAsync(
|
|
It.IsAny<RedisKey>(),
|
|
It.IsAny<RedisValue>(),
|
|
It.IsAny<TimeSpan?>(),
|
|
It.IsAny<When>()))
|
|
.Callback<RedisKey, RedisValue, TimeSpan?, When>(
|
|
(_, v, _, _) => capturedToken = v)
|
|
.ReturnsAsync(true);
|
|
|
|
_mockDb
|
|
.Setup(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<RedisKey[]?>(),
|
|
It.IsAny<RedisValue[]?>(),
|
|
It.IsAny<CommandFlags>()))
|
|
.Returns(Task.FromResult<RedisResult>(null!));
|
|
|
|
await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
|
|
await _provider.ReleaseAsync("key");
|
|
|
|
_mockDb.Verify(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.Is<RedisKey[]?>(keys => keys != null && keys[0] == (RedisKey)"lock:key"),
|
|
It.Is<RedisValue[]?>(vals => vals != null && vals[0] == capturedToken),
|
|
It.IsAny<CommandFlags>()), Times.Once());
|
|
}
|
|
#endregion
|
|
|
|
#region TC-49
|
|
/// <summary>
|
|
/// Verifies that ReleaseAsync is idempotent when invoked without a prior acquire operation,
|
|
/// ensuring that no script evaluation is performed on the underlying database in this scenario.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task ReleaseAsync_IsIdempotent_WhenCalledWithoutPriorAcquire()
|
|
{
|
|
_mockDb
|
|
.Setup(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<RedisKey[]?>(),
|
|
It.IsAny<RedisValue[]?>(),
|
|
It.IsAny<CommandFlags>()))
|
|
.Returns(Task.FromResult<RedisResult>(null!));
|
|
|
|
await _provider.ReleaseAsync("key");
|
|
|
|
_mockDb.Verify(db => db.ScriptEvaluateAsync(
|
|
It.IsAny<string>(),
|
|
It.IsAny<RedisKey[]?>(),
|
|
It.IsAny<RedisValue[]?>(),
|
|
It.IsAny<CommandFlags>()), Times.Never());
|
|
}
|
|
#endregion
|
|
}
|