Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 33 additions & 53 deletions EssentialCSharp.Web.Tests/ResponseIdValidationServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
using EssentialCSharp.Web.Services;
using Microsoft.Extensions.Caching.Memory;

namespace EssentialCSharp.Web.Tests;

public class ResponseIdValidationServiceTests
public class ResponseIdValidationServiceTests : IDisposable
{
// Match production SizeLimit so SetSize(1) is exercised in tests, not silently ignored.
private static MemoryCache CreateCache() => new(new MemoryCacheOptions { SizeLimit = 10_000 });
private readonly ResponseIdValidationService _service = new();

private static ResponseIdValidationService CreateService(MemoryCache cache) => new(cache);
public void Dispose()
{
_service.Dispose();
GC.SuppressFinalize(this);
}
Comment thread
BenjaminMichaelis marked this conversation as resolved.

[Test]
[Arguments(null)]
[Arguments("")]
public async Task ValidateResponseId_BlankResponseId_AllowsNewConversation(string? responseId)
{
using var cache = CreateCache();
var service = CreateService(cache);

bool result = service.ValidateResponseId("user1", responseId);
bool result = _service.ValidateResponseId("user1", responseId);

await Assert.That(result).IsTrue();
}
Expand All @@ -28,103 +28,83 @@ public async Task ValidateResponseId_BlankResponseId_AllowsNewConversation(strin
[Arguments("")]
public async Task ValidateResponseId_BlankUserId_Rejects(string? userId)
{
using var cache = CreateCache();
var service = CreateService(cache);

bool result = service.ValidateResponseId(userId, "resp_123");
bool result = _service.ValidateResponseId(userId, "resp_123");

await Assert.That(result).IsFalse();
}

[Test]
public async Task ValidateResponseId_CacheMiss_AllowsGracefulDegradation()
{
using var cache = CreateCache();
var service = CreateService(cache);
// No RecordResponseId call — simulate server restart / different instance

bool result = service.ValidateResponseId("user1", "resp_unknown");
bool result = _service.ValidateResponseId("user1", "resp_unknown");

await Assert.That(result).IsTrue();
}

[Test]
public async Task ValidateResponseId_RecordedByOwner_Validates()
{
using var cache = CreateCache();
var service = CreateService(cache);
service.RecordResponseId("user1", "resp_abc");
_service.RecordResponseId("user1", "resp_abc");

bool result = service.ValidateResponseId("user1", "resp_abc");
bool result = _service.ValidateResponseId("user1", "resp_abc");

await Assert.That(result).IsTrue();
}

[Test]
public async Task ValidateResponseId_RecordedByDifferentUser_Rejects()
{
using var cache = CreateCache();
var service = CreateService(cache);
service.RecordResponseId("user1", "resp_abc");
_service.RecordResponseId("user1", "resp_abc");

bool result = service.ValidateResponseId("user2", "resp_abc");
bool result = _service.ValidateResponseId("user2", "resp_abc");

await Assert.That(result).IsFalse();
}

[Test]
public async Task RecordResponseId_NullInputs_DoesNotThrow()
{
using var cache = CreateCache();
var service = CreateService(cache);

service.RecordResponseId(null, "resp_abc");
service.RecordResponseId("user1", null);
service.RecordResponseId(null, null);
_service.RecordResponseId(null, "resp_abc");
_service.RecordResponseId("user1", null);
_service.RecordResponseId(null, null);

// Verify the service is still functional after no-op calls
service.RecordResponseId("user1", "resp_abc");
await Assert.That(service.ValidateResponseId("user1", "resp_abc")).IsTrue();
_service.RecordResponseId("user1", "resp_abc");
await Assert.That(_service.ValidateResponseId("user1", "resp_abc")).IsTrue();
}

[Test]
public async Task ValidateResponseId_MultipleResponseIds_EachValidatedIndependently()
{
using var cache = CreateCache();
var service = CreateService(cache);
service.RecordResponseId("user1", "resp_001");
service.RecordResponseId("user1", "resp_002");
_service.RecordResponseId("user1", "resp_001");
_service.RecordResponseId("user1", "resp_002");

await Assert.That(service.ValidateResponseId("user1", "resp_001")).IsTrue();
await Assert.That(service.ValidateResponseId("user1", "resp_002")).IsTrue();
await Assert.That(_service.ValidateResponseId("user1", "resp_001")).IsTrue();
await Assert.That(_service.ValidateResponseId("user1", "resp_002")).IsTrue();
// Unrecorded ID for same user → cache miss → allow
await Assert.That(service.ValidateResponseId("user1", "resp_003")).IsTrue();
await Assert.That(_service.ValidateResponseId("user1", "resp_003")).IsTrue();
}

[Test]
public async Task ValidateResponseId_TwoUsers_IsolatedFromEachOther()
{
using var cache = CreateCache();
var service = CreateService(cache);
service.RecordResponseId("user1", "resp_A");
service.RecordResponseId("user2", "resp_B");

await Assert.That(service.ValidateResponseId("user1", "resp_A")).IsTrue();
await Assert.That(service.ValidateResponseId("user2", "resp_B")).IsTrue();
await Assert.That(service.ValidateResponseId("user2", "resp_A")).IsFalse();
await Assert.That(service.ValidateResponseId("user1", "resp_B")).IsFalse();
_service.RecordResponseId("user1", "resp_A");
_service.RecordResponseId("user2", "resp_B");

await Assert.That(_service.ValidateResponseId("user1", "resp_A")).IsTrue();
await Assert.That(_service.ValidateResponseId("user2", "resp_B")).IsTrue();
await Assert.That(_service.ValidateResponseId("user2", "resp_A")).IsFalse();
await Assert.That(_service.ValidateResponseId("user1", "resp_B")).IsFalse();
}

[Test]
public async Task RecordResponseId_SizeLimitEnforced_EntryCountedInCache()
{
using var cache = CreateCache();
var service = CreateService(cache);

// Record an entry — with SizeLimit set, SetSize(1) should count toward the cache size.
service.RecordResponseId("user1", "resp_size_test");
_service.RecordResponseId("user1", "resp_size_test");

// Verify it was recorded (i.e., not silently evicted due to misconfiguration).
await Assert.That(service.ValidateResponseId("user1", "resp_size_test")).IsTrue();
await Assert.That(_service.ValidateResponseId("user1", "resp_size_test")).IsTrue();
}
}
9 changes: 9 additions & 0 deletions EssentialCSharp.Web/Services/ResponseIdValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public sealed class ResponseIdValidationService : IDisposable

private readonly IMemoryCache _cache;
private readonly bool _ownsCache;
private bool _disposed;

/// <summary>
/// Production constructor. Creates and owns a dedicated <see cref="MemoryCache"/> with a bounded
Expand All @@ -54,8 +55,16 @@ private ResponseIdValidationService(IMemoryCache cache, bool ownsCache)
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;

if (_ownsCache && _cache is IDisposable disposable)
{
disposable.Dispose();
}
}

/// <summary>
Expand Down
Loading