74 lines
2.4 KiB
C#
74 lines
2.4 KiB
C#
using Elternbeirat.Web.Shared;
|
|
|
|
namespace Elternbeirat.Web.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the health cache behind the maintenance gate. The probe is a fake
|
|
/// that counts its calls, so no PocketBase is needed.
|
|
/// </summary>
|
|
public class PocketBaseHealthCacheTests
|
|
{
|
|
private readonly SteppingClock _clock = new(new DateTimeOffset(2026, 10, 1, 10, 0, 0, TimeSpan.Zero));
|
|
private int _probes;
|
|
|
|
private Func<CancellationToken, Task<bool>> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A clock the test moves forward by hand, to step past the cache lifetime without
|
|
/// waiting.
|
|
/// </summary>
|
|
/// <param name="start">The moment the clock shows at first.</param>
|
|
private sealed class SteppingClock(DateTimeOffset start) : TimeProvider
|
|
{
|
|
public DateTimeOffset Now { get; set; } = start;
|
|
|
|
public override DateTimeOffset GetUtcNow() => Now;
|
|
}
|
|
}
|