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 _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 }