using Elternbeirat.Web.Shared; namespace Elternbeirat.Web.Tests; /// /// Unit tests for the health cache behind the maintenance gate. The probe is a fake /// that counts its calls, so no PocketBase is needed. /// public class PocketBaseHealthCacheTests { private readonly SteppingClock _clock = new(new DateTimeOffset(2026, 10, 1, 10, 0, 0, TimeSpan.Zero)); private int _probes; private Func> Probe(bool healthy) => _ => { _probes++; return Task.FromResult(healthy); }; [Fact] public async Task Result_is_reused_within_the_lifetime() { var cache = new PocketBaseHealthCache(_clock); await cache.IsHealthyAsync(Probe(healthy: true), CancellationToken.None); _clock.Now += PocketBaseHealthCache.Lifetime - TimeSpan.FromSeconds(1); var healthy = await cache.IsHealthyAsync(Probe(healthy: false), CancellationToken.None); healthy.ShouldBeTrue(); _probes.ShouldBe(1); } [Fact] public async Task Stale_result_is_probed_again() { // The way back after an outage: once the lifetime is over, the new answer // replaces the old one. var cache = new PocketBaseHealthCache(_clock); await cache.IsHealthyAsync(Probe(healthy: false), CancellationToken.None); _clock.Now += PocketBaseHealthCache.Lifetime; var healthy = await cache.IsHealthyAsync(Probe(healthy: true), CancellationToken.None); healthy.ShouldBeTrue(); _probes.ShouldBe(2); } [Fact] public async Task Unhealthy_result_is_cached_too() { // While PocketBase is down, not every request should wait for a failing probe. var cache = new PocketBaseHealthCache(_clock); await cache.IsHealthyAsync(Probe(healthy: false), CancellationToken.None); var healthy = await cache.IsHealthyAsync(Probe(healthy: true), CancellationToken.None); healthy.ShouldBeFalse(); _probes.ShouldBe(1); } /// /// A clock the test moves forward by hand, to step past the cache lifetime without /// waiting. /// /// The moment the clock shows at first. private sealed class SteppingClock(DateTimeOffset start) : TimeProvider { public DateTimeOffset Now { get; set; } = start; public override DateTimeOffset GetUtcNow() => Now; } }