Skip to content

Commit 308257a

Browse files
test(Core.Application.Tests): add unit tests for caching behavior to verify cache handling and configuration validation
1 parent 41ba0a7 commit 308257a

File tree

2 files changed

+576
-0
lines changed

2 files changed

+576
-0
lines changed
Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
using System.Text;
2+
using System.Text.Json;
3+
using MediatR;
4+
using Microsoft.Extensions.Caching.Distributed;
5+
using Microsoft.Extensions.Caching.Memory;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Options;
8+
using Moq;
9+
using NArchitecture.Core.Application.Pipelines.Caching;
10+
using Shouldly;
11+
12+
namespace NArchitecture.Core.Application.Tests.Pipelines.Caching;
13+
14+
public class MockCacheRemoverRequest : IRequest<int>, ICacheRemoverRequest
15+
{
16+
public string? CacheKey { get; set; }
17+
public string[]? CacheGroupKey { get; set; }
18+
public bool BypassCache { get; set; }
19+
}
20+
21+
public class CacheRemovingBehaviorTests
22+
{
23+
private readonly IDistributedCache _cache;
24+
private readonly Mock<ILogger<CacheRemovingBehavior<MockCacheRemoverRequest, int>>> _loggerMock;
25+
private readonly CacheRemovingBehavior<MockCacheRemoverRequest, int> _behavior;
26+
private readonly RequestHandlerDelegate<int> _nextDelegate;
27+
28+
public CacheRemovingBehaviorTests()
29+
{
30+
var options = new MemoryDistributedCacheOptions();
31+
_cache = new MemoryDistributedCache(Options.Create(options));
32+
_loggerMock = new Mock<ILogger<CacheRemovingBehavior<MockCacheRemoverRequest, int>>>();
33+
_behavior = new CacheRemovingBehavior<MockCacheRemoverRequest, int>(_cache, _loggerMock.Object);
34+
_nextDelegate = () => Task.FromResult(42);
35+
}
36+
37+
/// <summary>
38+
/// Verifies that a specific cache key is successfully removed when provided in the request.
39+
/// This test ensures the basic cache removal functionality works for single keys.
40+
/// </summary>
41+
[Fact]
42+
public async Task Handle_WhenCacheKeyProvided_ShouldRemoveFromCache()
43+
{
44+
// Arrange
45+
var request = new MockCacheRemoverRequest { CacheKey = "test-key" };
46+
await _cache.SetAsync("test-key", Encoding.UTF8.GetBytes("test-value"));
47+
48+
// Act
49+
await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
50+
51+
// Assert
52+
var result = await _cache.GetAsync("test-key");
53+
result.ShouldBeNull("Cache key should be removed from cache");
54+
}
55+
56+
/// <summary>
57+
/// Validates that when a group key is provided, all associated cache entries are removed,
58+
/// including the group metadata and sliding expiration information.
59+
/// This test ensures proper cleanup of group-based caching.
60+
/// </summary>
61+
[Fact]
62+
public async Task Handle_WhenGroupKeyProvided_ShouldRemoveEntireGroup()
63+
{
64+
// Arrange
65+
var groupKey = "group1";
66+
var cachedKeys = new HashSet<string> { "key1", "key2" };
67+
var serializedKeys = JsonSerializer.Serialize(cachedKeys);
68+
69+
await _cache.SetAsync(groupKey, Encoding.UTF8.GetBytes(serializedKeys));
70+
foreach (var key in cachedKeys)
71+
{
72+
await _cache.SetAsync(key, Encoding.UTF8.GetBytes($"value-{key}"));
73+
}
74+
await _cache.SetAsync($"{groupKey}SlidingExpiration", Encoding.UTF8.GetBytes("30"));
75+
76+
var request = new MockCacheRemoverRequest { CacheGroupKey = [groupKey] };
77+
78+
// Act
79+
await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
80+
81+
// Assert
82+
foreach (var key in cachedKeys)
83+
{
84+
var value = await _cache.GetAsync(key);
85+
value.ShouldBeNull($"Cache key '{key}' should be removed");
86+
}
87+
88+
var groupValue = await _cache.GetAsync(groupKey);
89+
groupValue.ShouldBeNull("Group key should be removed");
90+
91+
var slidingValue = await _cache.GetAsync($"{groupKey}SlidingExpiration");
92+
slidingValue.ShouldBeNull("Sliding expiration key should be removed");
93+
}
94+
95+
/// <summary>
96+
/// Tests the behavior when multiple cache groups need to be removed.
97+
/// Verifies that all cache entries, group metadata, and sliding expiration data
98+
/// are properly removed for each specified group.
99+
/// </summary>
100+
[Fact]
101+
public async Task Handle_WhenMultipleGroupKeysProvided_ShouldRemoveAllGroups()
102+
{
103+
// Arrange
104+
var groupKeys = new[] { "group1", "group2" };
105+
var cachedKeys1 = new HashSet<string> { "key1", "key2" };
106+
var cachedKeys2 = new HashSet<string> { "key3", "key4" };
107+
108+
// Setup first group
109+
await _cache.SetAsync("group1", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cachedKeys1)));
110+
foreach (var key in cachedKeys1)
111+
{
112+
await _cache.SetAsync(key, Encoding.UTF8.GetBytes($"value-{key}"));
113+
}
114+
await _cache.SetAsync("group1SlidingExpiration", Encoding.UTF8.GetBytes("30"));
115+
116+
// Setup second group
117+
await _cache.SetAsync("group2", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cachedKeys2)));
118+
foreach (var key in cachedKeys2)
119+
{
120+
await _cache.SetAsync(key, Encoding.UTF8.GetBytes($"value-{key}"));
121+
}
122+
await _cache.SetAsync("group2SlidingExpiration", Encoding.UTF8.GetBytes("30"));
123+
124+
var request = new MockCacheRemoverRequest { CacheGroupKey = groupKeys };
125+
126+
// Act
127+
await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
128+
129+
// Assert
130+
foreach (var key in cachedKeys1.Concat(cachedKeys2))
131+
{
132+
var value = await _cache.GetAsync(key);
133+
value.ShouldBeNull($"Cache key '{key}' should be removed");
134+
}
135+
136+
foreach (var groupKey in groupKeys)
137+
{
138+
var groupValue = await _cache.GetAsync(groupKey);
139+
groupValue.ShouldBeNull($"Group key '{groupKey}' should be removed");
140+
141+
var slidingValue = await _cache.GetAsync($"{groupKey}SlidingExpiration");
142+
slidingValue.ShouldBeNull($"Sliding expiration key for '{groupKey}' should be removed");
143+
}
144+
}
145+
146+
/// <summary>
147+
/// Ensures the behavior handles non-existent group keys gracefully without affecting
148+
/// other cache entries. This test verifies the system's resilience when dealing
149+
/// with missing cache groups.
150+
/// </summary>
151+
[Fact]
152+
public async Task Handle_WhenGroupKeyDoesNotExist_ShouldHandleGracefully()
153+
{
154+
// Arrange
155+
var existingKey = "existing-key";
156+
await _cache.SetAsync(existingKey, Encoding.UTF8.GetBytes("test-value"));
157+
158+
var request = new MockCacheRemoverRequest { CacheGroupKey = ["non-existent-group"] };
159+
160+
// Act
161+
await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
162+
163+
// Assert
164+
var existingValue = await _cache.GetAsync(existingKey);
165+
existingValue.ShouldNotBeNull("Existing cache entries should not be affected");
166+
}
167+
168+
/// <summary>
169+
/// Verifies that when BypassCache is set to true, no cache operations are performed
170+
/// regardless of the provided cache keys or group keys. This test ensures the bypass
171+
/// functionality works as expected.
172+
/// </summary>
173+
[Fact]
174+
public async Task Handle_WhenBypassCacheIsTrue_ShouldSkipCacheOperations()
175+
{
176+
// Arrange
177+
var testKey = "test-key";
178+
var groupKey = "group1";
179+
await _cache.SetAsync(testKey, Encoding.UTF8.GetBytes("test-value"));
180+
await _cache.SetAsync(groupKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new[] { testKey })));
181+
182+
var request = new MockCacheRemoverRequest
183+
{
184+
BypassCache = true,
185+
CacheKey = testKey,
186+
CacheGroupKey = [groupKey],
187+
};
188+
189+
// Act
190+
await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
191+
192+
// Assert
193+
var value = await _cache.GetAsync(testKey);
194+
value.ShouldNotBeNull("Cache should not be modified when bypassing cache");
195+
196+
var groupValue = await _cache.GetAsync(groupKey);
197+
groupValue.ShouldNotBeNull("Group cache should not be modified when bypassing cache");
198+
}
199+
200+
/// <summary>
201+
/// Confirms that the next delegate in the pipeline is always called regardless
202+
/// of cache operations. This ensures the behavior doesn't break the request pipeline.
203+
/// </summary>
204+
[Fact]
205+
public async Task Handle_ShouldAlwaysCallNextDelegate()
206+
{
207+
// Arrange
208+
bool nextDelegateCalled = false;
209+
var request = new MockCacheRemoverRequest();
210+
RequestHandlerDelegate<int> next = () =>
211+
{
212+
nextDelegateCalled = true;
213+
return Task.FromResult(42);
214+
};
215+
216+
// Act
217+
await _behavior.Handle(request, next, CancellationToken.None);
218+
219+
// Assert
220+
nextDelegateCalled.ShouldBeTrue("Next delegate should always be called");
221+
}
222+
223+
/// <summary>
224+
/// Tests various combinations of cache configurations to ensure proper behavior
225+
/// in different scenarios. This parameterized test covers multiple use cases:
226+
/// - No cache key or group keys
227+
/// - Only cache key
228+
/// - Only group keys
229+
/// - Both cache key and group keys
230+
/// - Bypass cache with both types of keys
231+
/// </summary>
232+
[Theory]
233+
[InlineData(null, null, false)]
234+
[InlineData("test-key", null, false)]
235+
[InlineData(null, new[] { "group1" }, false)]
236+
[InlineData("test-key", new[] { "group1" }, false)]
237+
[InlineData("test-key", new[] { "group1" }, true)]
238+
public async Task Handle_WithDifferentRequestConfigurations_ShouldBehaveCorrectly(
239+
string? cacheKey,
240+
string[]? groupKeys,
241+
bool bypassCache
242+
)
243+
{
244+
// Arrange
245+
var request = new MockCacheRemoverRequest
246+
{
247+
CacheKey = cacheKey,
248+
CacheGroupKey = groupKeys,
249+
BypassCache = bypassCache,
250+
};
251+
252+
if (cacheKey != null)
253+
await _cache.SetAsync(cacheKey, Encoding.UTF8.GetBytes("test-value"));
254+
255+
if (groupKeys != null)
256+
foreach (var groupKey in groupKeys)
257+
{
258+
var groupData = new HashSet<string> { "key1" };
259+
await _cache.SetAsync(groupKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(groupData)));
260+
await _cache.SetAsync("key1", Encoding.UTF8.GetBytes("value1"));
261+
}
262+
263+
// Act
264+
var result = await _behavior.Handle(request, _nextDelegate, CancellationToken.None);
265+
266+
// Assert
267+
result.ShouldBe(42);
268+
269+
if (!bypassCache)
270+
{
271+
if (cacheKey != null)
272+
{
273+
var cachedValue = await _cache.GetAsync(cacheKey);
274+
cachedValue.ShouldBeNull();
275+
}
276+
277+
if (groupKeys != null)
278+
{
279+
foreach (var groupKey in groupKeys)
280+
{
281+
var groupValue = await _cache.GetAsync(groupKey);
282+
groupValue.ShouldBeNull();
283+
}
284+
}
285+
}
286+
}
287+
288+
/// <summary>
289+
/// Verifies that the behavior handles empty group key arrays without throwing exceptions.
290+
/// This edge case test ensures system stability when provided with empty collections.
291+
/// </summary>
292+
[Fact]
293+
public async Task Handle_WithEmptyGroupKey_ShouldNotThrowException()
294+
{
295+
// Arrange
296+
var request = new MockCacheRemoverRequest { CacheGroupKey = Array.Empty<string>() };
297+
298+
// Act
299+
var exception = await Record.ExceptionAsync(() => _behavior.Handle(request, _nextDelegate, CancellationToken.None));
300+
301+
// Assert
302+
exception.ShouldBeNull();
303+
}
304+
305+
/// <summary>
306+
/// Tests the behavior's handling of corrupted or invalid JSON data in group cache entries.
307+
/// Ensures the system fails gracefully and throws appropriate exceptions when
308+
/// encountering malformed cache data.
309+
/// </summary>
310+
[Fact]
311+
public async Task Handle_WithInvalidGroupKeyData_ShouldHandleGracefully()
312+
{
313+
// Arrange
314+
const string groupKey = "invalid-group";
315+
await _cache.SetAsync(groupKey, Encoding.UTF8.GetBytes("invalid-json"));
316+
317+
var request = new MockCacheRemoverRequest { CacheGroupKey = new[] { groupKey } };
318+
319+
// Act
320+
var exception = await Record.ExceptionAsync(() => _behavior.Handle(request, _nextDelegate, CancellationToken.None));
321+
322+
// Assert
323+
exception.ShouldNotBeNull();
324+
exception.ShouldBeOfType<JsonException>();
325+
}
326+
327+
/// <summary>
328+
/// Verifies that the behavior properly respects cancellation tokens and
329+
/// terminates operations when cancellation is requested. This ensures
330+
/// the system remains responsive and can be interrupted when needed.
331+
/// </summary>
332+
[Fact]
333+
public async Task Handle_WithCancellation_ShouldRespectCancellationToken()
334+
{
335+
// Arrange
336+
var cts = new CancellationTokenSource();
337+
var request = new MockCacheRemoverRequest { CacheKey = "test-key", CacheGroupKey = new[] { "test-group" } };
338+
339+
// Set up some cache data to ensure we hit the cache operations
340+
await _cache.SetAsync("test-group", Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new HashSet<string> { "test-key" })));
341+
await _cache.SetAsync("test-key", Encoding.UTF8.GetBytes("test-value"));
342+
343+
cts.Cancel(); // Cancel before execution
344+
345+
// Act & Assert
346+
await Should.ThrowAsync<OperationCanceledException>(async () =>
347+
{
348+
await _behavior.Handle(request, _nextDelegate, cts.Token);
349+
});
350+
}
351+
}

0 commit comments

Comments
 (0)