using System.Diagnostics;
using adas_core.Application.Services.Caching;
using Moq;
using StackExchange.Redis;
namespace adas_core.Test.Services;
///
/// Represents a test fixture that contains unit tests for the RedisLockProvider class.
///
///
/// Decorated with the TestFixture attribute so that NUnit-compatible test runners can discover and execute its test methods.
///
///
[TestFixture]
public class RedisLockProviderTest
{
private Mock _mockDb = null!;
private RedisLockProvider _provider = null!;
///
/// Initializes the test environment by creating a mock and instantiating a configured to use the mocked database.
///
///
[SetUp]
public void SetUp()
{
_mockDb = new Mock();
_provider = new RedisLockProvider(() => _mockDb.Object);
}
#region TC-45
///
/// Verifies that AcquireAsync returns true, stores the lock token under the lock: prefixed key, applies the configured TTL, and uses the When.NotExists flag when Redis accepts the NX SET operation. Also asserts that a subsequent ReleaseAsync invokes the Redis release script.
///
///
[Test]
public async Task AcquireAsync_ReturnsTrue_AndStoresToken_WhenRedisAcceptsNxSet()
{
_mockDb
.Setup(db => db.StringSetAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync(true);
_mockDb
.Setup(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Returns(Task.FromResult(null!));
var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
Assert.That(result, Is.True);
_mockDb.Verify(db => db.StringSetAsync(
It.Is(k => k == (RedisKey)"lock:key"),
It.IsAny(),
(TimeSpan?)TimeSpan.FromSeconds(5),
When.NotExists), Times.Once());
await _provider.ReleaseAsync("key");
_mockDb.Verify(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()), Times.Once());
}
#endregion
#region TC-46
///
/// Verifies that AcquireAsync retries the underlying Nx-style StringSetAsync 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.
///
///
[Test]
public async Task AcquireAsync_RetriesWithDelays_UntilNxSucceeds()
{
_mockDb
.SetupSequence(db => db.StringSetAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.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(),
It.IsAny(),
It.IsAny(),
It.IsAny()), Times.Exactly(4));
Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100));
}
#endregion
#region TC-47
///
/// Verifies that AcquireAsync returns false and performs multiple retry attempts when the underlying lock acquisition operation consistently fails within the specified timeout.
///
///
[Test]
public async Task AcquireAsync_ReturnsFalse_AfterTimeoutWithMultipleRetries()
{
_mockDb
.Setup(db => db.StringSetAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.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(),
It.IsAny(),
It.IsAny(),
It.IsAny()), Times.AtLeast(2));
}
#endregion
#region TC-48
///
/// 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.
///
///
[Test]
public async Task ReleaseAsync_CallsScriptEvaluateWithCorrectKeyAndToken()
{
RedisValue capturedToken = default;
_mockDb
.Setup(db => db.StringSetAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Callback(
(_, v, _, _) => capturedToken = v)
.ReturnsAsync(true);
_mockDb
.Setup(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Returns(Task.FromResult(null!));
await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5));
await _provider.ReleaseAsync("key");
_mockDb.Verify(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.Is(keys => keys != null && keys[0] == (RedisKey)"lock:key"),
It.Is(vals => vals != null && vals[0] == capturedToken),
It.IsAny()), Times.Once());
}
#endregion
#region TC-49
///
/// 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.
///
///
[Test]
public async Task ReleaseAsync_IsIdempotent_WhenCalledWithoutPriorAcquire()
{
_mockDb
.Setup(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()))
.Returns(Task.FromResult(null!));
await _provider.ReleaseAsync("key");
_mockDb.Verify(db => db.ScriptEvaluateAsync(
It.IsAny(),
It.IsAny(),
It.IsAny(),
It.IsAny()), Times.Never());
}
#endregion
}