You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
16 KiB
C#
460 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
using Moq;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Haoliang.Core.Services;
|
|
using Haoliang.Models.Models.Device;
|
|
using Haoliang.Models.Models.Production;
|
|
|
|
namespace Haoliang.Tests.Services
|
|
{
|
|
public class CacheServiceTests
|
|
{
|
|
private readonly Mock<IMemoryCache> _mockMemoryCache;
|
|
private readonly Mock<IProductionRepository> _mockProductionRepository;
|
|
private readonly Mock<IDeviceRepository> _mockDeviceRepository;
|
|
private readonly Mock<ICollectionRepository> _mockCollectionRepository;
|
|
private readonly CacheService _cacheService;
|
|
|
|
public CacheServiceTests()
|
|
{
|
|
_mockMemoryCache = new Mock<IMemoryCache>();
|
|
_mockProductionRepository = new Mock<IProductionRepository>();
|
|
_mockDeviceRepository = new Mock<IDeviceRepository>();
|
|
_mockCollectionRepository = new Mock<ICollectionRepository>();
|
|
|
|
_cacheService = new CacheService(_mockMemoryCache.Object);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrSetAsync_ValueNotInCache_ExecutesFactoryAndCachesResult()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
var factoryResult = "test-value";
|
|
|
|
var factoryMock = new Mock<Func<Task<string>>>();
|
|
factoryMock.Setup(f => f()).ReturnsAsync(factoryResult);
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Setup cache set
|
|
_mockMemoryCache.Setup(cache => cache.Set(
|
|
key,
|
|
factoryResult,
|
|
It.IsAny<MemoryCacheEntryOptions>()))
|
|
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
|
|
// Act
|
|
var result = await _cacheService.GetOrSetAsync(key, factoryMock.Object);
|
|
|
|
// Assert
|
|
Assert.Equal(factoryResult, result);
|
|
factoryMock.Verify(f => f(), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Set(
|
|
key,
|
|
factoryResult,
|
|
It.IsAny<MemoryCacheEntryOptions>()), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrSetAsync_ValueInCache_ReturnsCachedValue()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
var cachedValue = "cached-value";
|
|
|
|
var factoryMock = new Mock<Func<Task<string>>>();
|
|
factoryMock.Setup(f => f()).ReturnsAsync("new-value");
|
|
|
|
// Setup cache hit
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
.Callback<string, out string>((k, out string v) => v = cachedValue)
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = await _cacheService.GetOrSetAsync(key, factoryMock.Object);
|
|
|
|
// Assert
|
|
Assert.Equal(cachedValue, result);
|
|
factoryMock.Verify(f => f(), Times.Never);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Set(
|
|
key,
|
|
It.IsAny<string>(),
|
|
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public void Get_ValueExists_ReturnsCachedValue()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
var cachedValue = "cached-value";
|
|
|
|
// Setup cache hit
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
.Callback<string, out string>((k, out string v) => v = cachedValue)
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = _cacheService.Get<string>(key);
|
|
|
|
// Assert
|
|
Assert.Equal(cachedValue, result);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Get_ValueNotExists_ReturnsDefault()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Act
|
|
var result = _cacheService.Get<string>(key);
|
|
|
|
// Assert
|
|
Assert.Null(result);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<string>.IsAny), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Set_ValidValue_CachesValue()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
var value = "test-value";
|
|
|
|
var mockEntryOptions = new Mock<MemoryCacheEntryOptions>();
|
|
_mockMemoryCache.Setup(cache => cache.Set(key, value, mockEntryOptions.Object))
|
|
.Verifiable();
|
|
|
|
// Act
|
|
_cacheService.Set(key, value, mockEntryOptions.Object);
|
|
|
|
// Assert
|
|
_mockMemoryCache.Verify(cache => cache.Set(key, value, mockEntryOptions.Object), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Set_NullValue_DoesNotCache()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Act
|
|
_cacheService.Set<string>(key, null);
|
|
|
|
// Assert
|
|
_mockMemoryCache.Verify(cache => cache.Set(
|
|
It.IsAny<string>(),
|
|
It.IsAny<string>(),
|
|
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public void Remove_KeyExists_RemovesFromCache()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Setup cache hit for removal check
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
.Returns(true);
|
|
|
|
_mockMemoryCache.Setup(cache => cache.Remove(key))
|
|
.Verifiable();
|
|
|
|
// Act
|
|
var result = _cacheService.Remove(key);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Remove_KeyNotExists_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Act
|
|
var result = _cacheService.Remove(key);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exists_KeyExists_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Setup cache hit
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Returns(true);
|
|
|
|
// Act
|
|
var result = _cacheService.Exists(key);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exists_KeyNotExists_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Act
|
|
var result = _cacheService.Exists(key);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
_mockMemoryCache.Verify(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Clear_ClearsAllCache()
|
|
{
|
|
// Act
|
|
_cacheService.Clear();
|
|
|
|
// Assert
|
|
_mockMemoryCache.Verify(cache => cache.Compact(1.0), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetStatistics_ReturnsCacheStatistics()
|
|
{
|
|
// Arrange
|
|
_mockMemoryCache.Setup(cache => cache.Count)
|
|
.Returns(5);
|
|
|
|
// Act
|
|
var result = _cacheService.GetStatistics();
|
|
|
|
// Assert
|
|
Assert.NotNull(result);
|
|
Assert.True(result.TotalItems >= 0);
|
|
Assert.True(result.HitRate >= 0 && result.HitRate <= 1);
|
|
Assert.True(result.MemoryUsageBytes >= 0);
|
|
Assert.NotNull(result.ItemsByType);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetKeys_MatchingPattern_ReturnsKeys()
|
|
{
|
|
// Arrange
|
|
var pattern = "device:*";
|
|
var expectedKeys = new List<string> { "device:1", "device:2", "device:3" };
|
|
|
|
// Mock cache keys
|
|
var mockKeys = new List<string> { "device:1", "template:1", "device:2", "config:1", "device:3" };
|
|
_mockMemoryCache.Setup(cache => cache.Keys)
|
|
.Returns(mockKeys.Cast<object>().ToList());
|
|
|
|
// Act
|
|
var result = _cacheService.GetKeys(pattern).ToList();
|
|
|
|
// Assert
|
|
Assert.Equal(3, result.Count);
|
|
Assert.Contains("device:1", result);
|
|
Assert.Contains("device:2", result);
|
|
Assert.Contains("device:3", result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetKeys_NoMatches_ReturnsEmptyList()
|
|
{
|
|
// Arrange
|
|
var pattern = "nonexistent:*";
|
|
|
|
// Mock cache keys
|
|
var mockKeys = new List<string> { "device:1", "template:1", "config:1" };
|
|
_mockMemoryCache.Setup(cache => cache.Keys)
|
|
.Returns(mockKeys.Cast<object>().ToList());
|
|
|
|
// Act
|
|
var result = _cacheService.GetKeys(pattern).ToList();
|
|
|
|
// Assert
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Refresh_ExistingKey_RefreshesCache()
|
|
{
|
|
// Arrange
|
|
var key = "test-key";
|
|
var value = "test-value";
|
|
var mockOptions = new MemoryCacheEntryOptions();
|
|
|
|
// Setup cache hit
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Callback<string, out object>((k, out object v) => v = value)
|
|
.Returns(true);
|
|
|
|
_mockMemoryCache.Setup(cache => cache.Remove(key))
|
|
.Verifiable();
|
|
|
|
_mockMemoryCache.Setup(cache => cache.Set(key, value, mockOptions))
|
|
.Verifiable();
|
|
|
|
// Act
|
|
var result = _cacheService.Refresh<object>(key);
|
|
|
|
// Assert
|
|
Assert.True(result);
|
|
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Set(key, value, mockOptions), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Refresh_NonExistingKey_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
var key = "nonexistent-key";
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(key, out It.Ref<object>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Act
|
|
var result = _cacheService.Refresh<object>(key);
|
|
|
|
// Assert
|
|
Assert.False(result);
|
|
_mockMemoryCache.Verify(cache => cache.Remove(key), Times.Never);
|
|
_mockMemoryCache.Verify(cache => cache.Set(
|
|
It.IsAny<string>(),
|
|
It.IsAny<object>(),
|
|
It.IsAny<MemoryCacheEntryOptions>()), Times.Never);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrSetDeviceAsync_ValidDevice_ReturnsDevice()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
var device = new CNCDevice { Id = deviceId, Name = "Test Device" };
|
|
|
|
var factoryMock = new Mock<Func<Task<CNCDevice>>>();
|
|
factoryMock.Setup(f => f()).ReturnsAsync(device);
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<CNCDevice>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Setup cache set
|
|
_mockMemoryCache.Setup(cache => cache.Set(
|
|
It.IsAny<string>(),
|
|
device,
|
|
It.IsAny<MemoryCacheEntryOptions>()))
|
|
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
|
|
// Act
|
|
var result = await _cacheService.GetOrSetDeviceAsync(deviceId, factoryMock.Object);
|
|
|
|
// Assert
|
|
Assert.Equal(device, result);
|
|
factoryMock.Verify(f => f(), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidateDeviceCache_ValidDevice_RemovesRelatedKeys()
|
|
{
|
|
// Arrange
|
|
var deviceId = 1;
|
|
|
|
// Setup cache hits for removal
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<object>.IsAny))
|
|
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
.Returns(true);
|
|
|
|
_mockMemoryCache.Setup(cache => cache.Remove(It.IsAny<string>()))
|
|
.Verifiable();
|
|
|
|
// Act
|
|
_cacheService.InvalidateDeviceCache(deviceId, "additional:key");
|
|
|
|
// Assert
|
|
_mockMemoryCache.Verify(cache => cache.Remove("device:1"), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Remove("device:status:1"), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Remove(It.IsAny<string>()), Times.AtLeast(3));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetOrSetSystemConfigurationAsync_ValidFactory_ReturnsConfiguration()
|
|
{
|
|
// Arrange
|
|
var config = new SystemConfiguration { DailyProductionTarget = 100 };
|
|
|
|
var factoryMock = new Mock<Func<Task<SystemConfiguration>>>();
|
|
factoryMock.Setup(f => f()).ReturnsAsync(config);
|
|
|
|
// Setup cache miss
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<SystemConfiguration>.IsAny))
|
|
.Returns(false);
|
|
|
|
// Setup cache set
|
|
_mockMemoryCache.Setup(cache => cache.Set(
|
|
It.IsAny<string>(),
|
|
config,
|
|
It.IsAny<MemoryCacheEntryOptions>()))
|
|
.Callback<string, object, MemoryCacheEntryOptions>((k, v, o) => { });
|
|
|
|
// Act
|
|
var result = await _cacheService.GetOrSetSystemConfigurationAsync(factoryMock.Object);
|
|
|
|
// Assert
|
|
Assert.Equal(config, result);
|
|
factoryMock.Verify(f => f(), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidateSystemConfigCache_RemovesSystemConfigKeys()
|
|
{
|
|
// Arrange
|
|
// Setup cache hits for removal
|
|
_mockMemoryCache.Setup(cache => cache.TryGetValue(It.IsAny<string>(), out It.Ref<object>.IsAny))
|
|
.Callback<string, out object>((k, out object v) => v = "some-value")
|
|
.Returns(true);
|
|
|
|
_mockMemoryCache.Setup(cache => cache.Remove(It.IsAny<string>()))
|
|
.Verifiable();
|
|
|
|
// Act
|
|
_cacheService.InvalidateSystemConfigCache();
|
|
|
|
// Assert
|
|
_mockMemoryCache.Verify(cache => cache.Remove("config:system"), Times.Once);
|
|
_mockMemoryCache.Verify(cache => cache.Remove("config:alerts"), Times.Once);
|
|
}
|
|
}
|
|
} |