using FluentAssertions; using Nop.Core.Caching; using NUnit.Framework; namespace Nop.Tests.Nop.Core.Tests.Caching; [TestFixture] public class LockerTests : BaseNopTest { private ILocker _memCacheLocker; private ILocker _distCacheLocker; [OneTimeSetUp] public void Setup() { _memCacheLocker = GetService(); _distCacheLocker = GetService(); } [Test] public async Task Distributed_CanPerformLockAsync() { await TestLockerAsync(_distCacheLocker); } [Test] public async Task Memory_CanPerformLockAsync() { await TestLockerAsync(_memCacheLocker); } [Test] public async Task Distributed_CanRunWithHeartbeatAsync() { await TestHeartbeatAsync(_distCacheLocker); } [Test] public async Task Memory_CanRunWithHeartbeatAsync() { await TestHeartbeatAsync(_memCacheLocker); } [Test] public async Task Distributed_CanCancelAsync() { await TestCancellationAsync(_distCacheLocker); } [Test] public async Task Memory_CanCancelAsync() { await TestCancellationAsync(_memCacheLocker); } private static async Task TestLockerAsync(ILocker locker) { var key = Guid.NewGuid().ToString(); var expiration = TimeSpan.FromMinutes(2); var actionCount = 0; async Task action() { var res = await locker.PerformActionWithLockAsync(key, expiration, () => { Assert.Fail("Action in progress"); return Task.CompletedTask; }); res.Should().BeFalse(); actionCount++; } var result = await locker.PerformActionWithLockAsync(key, expiration, () => action()); result.Should().BeTrue(); actionCount.Should().Be(1); Assert.ThrowsAsync(() => locker.PerformActionWithLockAsync(key, expiration, FailingAction)); result = await locker.PerformActionWithLockAsync(key, expiration, action); result.Should().BeTrue(); actionCount.Should().Be(2); } private static async Task TestHeartbeatAsync(ILocker locker) { var key = Guid.NewGuid().ToString(); var expiration = TimeSpan.FromMinutes(2); var heartbeat = TimeSpan.FromSeconds(10); var actionCount = 0; async Task action(CancellationToken _) { await locker.RunWithHeartbeatAsync( key, expiration, heartbeat, _ => { Assert.Fail("Action in progress"); return Task.CompletedTask; }); actionCount++; } await locker.RunWithHeartbeatAsync(key, expiration, heartbeat, action); actionCount.Should().Be(1); Assert.ThrowsAsync(() => locker.RunWithHeartbeatAsync(key, expiration, heartbeat, _ => FailingAction())); await locker.RunWithHeartbeatAsync(key, expiration, heartbeat, action); actionCount.Should().Be(2); } private static async Task TestCancellationAsync(ILocker locker) { var key = Guid.NewGuid().ToString(); var expiration = TimeSpan.FromSeconds(1); var heartbeat = TimeSpan.FromMilliseconds(100); var delay = TimeSpan.FromSeconds(5); async Task testAsync(Func cancel, CancellationTokenSource tokenSource = default) { var cancelled = false; async Task action(CancellationToken token) { await locker.RunWithHeartbeatAsync( key, expiration, heartbeat, _ => { Assert.Fail("Action in progress"); return Task.CompletedTask; }); try { await Task.Delay(delay, token); } catch { cancelled = true; } } var task = locker.RunWithHeartbeatAsync(key, expiration, heartbeat, action, tokenSource); (await locker.IsTaskRunningAsync(key)).Should().BeTrue(); await cancel(); await task; cancelled.Should().BeTrue(); (await locker.IsTaskRunningAsync(key)).Should().BeFalse(); } await testAsync(() => locker.CancelTaskAsync(key, expiration)); var cancellationTokenSource = new CancellationTokenSource(); await testAsync(() => Task.Run(cancellationTokenSource.Cancel), cancellationTokenSource); } private static Task FailingAction() { throw new ApplicationException("This action is supposed to fail"); } }