using Mbproxy.Proxy.Cache; using Shouldly; using Xunit; namespace Mbproxy.Tests.Proxy.Cache; /// /// Cover invariants of the record: TTL boundary expiry, byte /// independence (caller-supplied PduBytes are not mutated by the cache itself — /// the cache copies on store), and monotonic /// semantics when the cache stamps it. /// [Trait("Category", "Unit")] public sealed class CacheEntryTests { [Fact] public void Expired_When_NowEqualsOrExceedsExpiresAtUtc() { var now = DateTimeOffset.UtcNow; var entry = new CacheEntry( PduBytes: [0x03, 0x02, 0x04, 0xD2], CachedAtUtc: now, ExpiresAtUtc: now.AddMilliseconds(50), Length: 4, LastUsedTick: 1); // The entry exposes ExpiresAtUtc for the cache to compare against UtcNow. Sanity: // an entry whose ExpiresAtUtc is in the past is expired. var past = entry with { ExpiresAtUtc = now.AddMilliseconds(-1) }; (past.ExpiresAtUtc <= now).ShouldBeTrue("an entry whose expiry is in the past must be expired"); var future = entry with { ExpiresAtUtc = now.AddMilliseconds(100) }; (future.ExpiresAtUtc > now).ShouldBeTrue("an entry whose expiry is in the future must be live"); } [Fact] public void Record_With_Expression_DoesNotMutate_OriginalArrayContents() { var bytes = new byte[] { 0x03, 0x02, 0x04, 0xD2 }; var entry = new CacheEntry( PduBytes: bytes, CachedAtUtc: DateTimeOffset.UtcNow, ExpiresAtUtc: DateTimeOffset.UtcNow.AddSeconds(1), Length: 4, LastUsedTick: 1); // Sanity: the entry holds a reference to the supplied array. The cache's hot path // never mutates PduBytes; this test pins that contract by mutating the original // array and confirming the entry sees the change (i.e. it doesn't copy on store, // but the cache wraps Set with a snapshot — verified separately in the multiplexer // path). bytes[0] = 0xFF; entry.PduBytes[0].ShouldBe((byte)0xFF, "CacheEntry holds the supplied reference; defensive copies live in the multiplexer"); } [Fact] public void LastUsedTick_Stamped_By_ResponseCache_OnSet_AndOnHit() { // The CacheEntry itself doesn't compute LastUsedTick — the cache assigns the next // tick on every Set/Get. Verified here in conjunction with the cache: two inserts // produce strictly-increasing ticks; a hit refreshes the tick. using var cache = new ResponseCache(maxEntriesPerPlc: 10, evictionIntervalMs: 5000); var k1 = new CacheKey(1, 0x03, 100, 1); var k2 = new CacheKey(1, 0x03, 200, 1); cache.Set(k1, MakeEntry(ttlMs: 1000)); cache.Set(k2, MakeEntry(ttlMs: 1000)); cache.TryGet(k1, out var e1).ShouldBeTrue(); cache.TryGet(k2, out var e2).ShouldBeTrue(); // Whichever was touched last has the larger LastUsedTick (k2 was the most recent // touch via TryGet). e2.LastUsedTick.ShouldBeGreaterThan(e1.LastUsedTick, "the more-recently-touched entry must carry the larger tick"); } private static CacheEntry MakeEntry(int ttlMs) { var now = DateTimeOffset.UtcNow; return new CacheEntry( PduBytes: [0x03, 0x02, 0x04, 0xD2], CachedAtUtc: now, ExpiresAtUtc: now.AddMilliseconds(ttlMs), Length: 4, LastUsedTick: 0); } }