Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
+197
View File
@@ -0,0 +1,197 @@
using System.Collections.Concurrent;
using MijnBesluit.Server;
namespace MijnBesluit.SelfCheck;
/// <summary>
/// De sessies van de SocketServer: het aangemeld blijven van de app.
///
/// Waarom dit een eigen hoofdstukje heeft: hier hangt aan vast of iemand met zijn vingerafdruk
/// de app in komt of weer een code uit zijn mail moet halen. Dat ging mis doordat de sessies
/// alleen in het geheugen stonden - elke herstart van de service loste iedereen op.
/// </summary>
public static class SessieChecks
{
/// <summary>Een bewaring zonder database: precies genoeg om een herstart na te doen.</summary>
private sealed class Proefbewaring : ISessiebewaring
{
private readonly ConcurrentDictionary<string, Sessiegegevens> _rijen = new();
/// <summary>Alles wat er ooit is binnengekomen, om te kunnen kijken wat er wordt opgeslagen.</summary>
public List<string> Sleutels { get; } = new();
private static string Sleutel(byte[] hash) => Convert.ToHexString(hash);
public Task BewaarAsync(byte[] hash, Sessiegegevens sessie, DateTime nu, CancellationToken ct = default)
{
Sleutels.Add(Sleutel(hash));
_rijen[Sleutel(hash)] = sessie;
return Task.CompletedTask;
}
public Task<Sessiegegevens?> ZoekAsync(byte[] hash, CancellationToken ct = default) =>
Task.FromResult(_rijen.TryGetValue(Sleutel(hash), out var s) ? s : null);
public Task SchuifOpAsync(byte[] hash, DateTime verlooptOp, DateTime nu, CancellationToken ct = default)
{
if (_rijen.TryGetValue(Sleutel(hash), out var s))
_rijen[Sleutel(hash)] = s with { VerlooptOp = verlooptOp };
return Task.CompletedTask;
}
public Task VerwijderAsync(byte[] hash, CancellationToken ct = default)
{
_rijen.TryRemove(Sleutel(hash), out _);
return Task.CompletedTask;
}
public Task VerwijderAllesVanAsync(long userId, CancellationToken ct = default)
{
foreach (var kv in _rijen.Where(kv => kv.Value.UserId == userId))
_rijen.TryRemove(kv.Key, out _);
return Task.CompletedTask;
}
public Task<int> RuimVerlopenOpAsync(DateTime nu, CancellationToken ct = default)
{
var weg = _rijen.Where(kv => kv.Value.VerlooptOp <= nu).ToList();
foreach (var kv in weg) _rijen.TryRemove(kv.Key, out _);
return Task.FromResult(weg.Count);
}
public int Aantal => _rijen.Count;
}
public static void Register(Runner runner)
{
runner.TestAsync("Een sessie overleeft een herstart van de SocketServer", async () =>
{
// Dit is waar het om begonnen was. De app blijft aangemeld en gaat open met een
// vingerafdruk; stonden de sessies alleen in het geheugen, dan was elke uitrol of
// reboot genoeg om iedereen weer een code per e-mail te laten opvragen.
var klok = new TestClock();
var bewaring = new Proefbewaring();
var voor = new SessionStore(klok, null, bewaring);
var sessie = await voor.CreateAsync(7, 9, "Galaxy S26");
// De service gaat uit en komt terug: een verse kast, dezelfde database.
var na = new SessionStore(klok, null, bewaring);
var terug = await na.ValidateAsync(sessie.Token);
Assert.True(terug is not null, "het token van de app werkt nog");
Assert.True(terug!.UserId == 7 && terug.PersonId == 9,
"en de server weet weer wie erbij hoort");
Assert.True(terug.Device == "Galaxy S26", "inclusief het toestel waar hij vandaan komt");
});
runner.TestAsync("Het token zelf staat niet in de opslag, alleen de hash", () =>
{
// Dezelfde afspraak als bij de inlogcodes: wie de database in handen krijgt, krijgt
// daarmee geen werkende sessies mee.
var klok = new TestClock();
var bewaring = new Proefbewaring();
var kast = new SessionStore(klok, null, bewaring);
return kast.CreateAsync(1, 1, null).ContinueWith(t =>
{
var sessie = t.Result;
Assert.True(bewaring.Sleutels.Count == 1, "er is één sessie bewaard");
Assert.True(!bewaring.Sleutels[0].Contains(sessie.Token, StringComparison.OrdinalIgnoreCase),
"en het token staat er niet in");
Assert.True(bewaring.Sleutels[0] == Convert.ToHexString(SessionStore.Hash(sessie.Token)),
"wat er staat is de hash ervan");
});
});
runner.TestAsync("Uitloggen haalt de sessie ook echt weg", async () =>
{
var klok = new TestClock();
var bewaring = new Proefbewaring();
var kast = new SessionStore(klok, null, bewaring);
var sessie = await kast.CreateAsync(3, 4, null);
await kast.RemoveAsync(sessie.Token);
Assert.True(bewaring.Aantal == 0, "de sessie is uit de opslag verdwenen");
// En niet alleen uit het geheugen: een verse kast mag hem ook niet meer vinden.
var na = new SessionStore(klok, null, bewaring);
Assert.True(await na.ValidateAsync(sessie.Token) is null,
"ook na een herstart kom je er niet meer mee binnen");
});
runner.TestAsync("Een account opheffen zet alle toestellen buiten", async () =>
{
var klok = new TestClock();
var bewaring = new Proefbewaring();
var kast = new SessionStore(klok, null, bewaring);
var telefoon = await kast.CreateAsync(5, 6, "telefoon");
var tablet = await kast.CreateAsync(5, 6, "tablet");
var iemandAnders = await kast.CreateAsync(8, 9, "telefoon van een ander");
await kast.RemoveAllForAsync(5);
var na = new SessionStore(klok, null, bewaring);
Assert.True(await na.ValidateAsync(telefoon.Token) is null, "de telefoon is eruit");
Assert.True(await na.ValidateAsync(tablet.Token) is null, "de tablet ook");
Assert.True(await na.ValidateAsync(iemandAnders.Token) is not null,
"maar een ander account blijft gewoon aangemeld");
});
runner.TestAsync("Een verlopen sessie komt ook uit de opslag niet meer terug", async () =>
{
var klok = new TestClock();
var bewaring = new Proefbewaring();
var kast = new SessionStore(klok, TimeSpan.FromHours(1), bewaring);
var sessie = await kast.CreateAsync(2, 2, null);
klok.Advance(TimeSpan.FromHours(2));
var na = new SessionStore(klok, TimeSpan.FromHours(1), bewaring);
Assert.True(await na.ValidateAsync(sessie.Token) is null, "de termijn is voorbij");
Assert.True(bewaring.Aantal == 0, "en de rij is opgeruimd in plaats van blijven staan");
});
runner.TestAsync("De vervaltijd wordt niet bij elk verzoek weggeschreven", async () =>
{
// Elk verzoek schuift de termijn op. Dat ook elke keer opslaan zou een schrijfactie
// per aanroep betekenen, voor iets waar bij veertien dagen een uur speling niets
// uitmaakt.
var klok = new TestClock();
var bewaring = new Proefbewaring();
var kast = new SessionStore(klok, null, bewaring);
var sessie = await kast.CreateAsync(1, 1, null);
var opgeslagen = (await bewaring.ZoekAsync(SessionStore.Hash(sessie.Token)))!.VerlooptOp;
klok.Advance(TimeSpan.FromMinutes(10));
await kast.ValidateAsync(sessie.Token);
Assert.True((await bewaring.ZoekAsync(SessionStore.Hash(sessie.Token)))!.VerlooptOp == opgeslagen,
"tien minuten later is er niets geschreven");
klok.Advance(TimeSpan.FromHours(3));
await kast.ValidateAsync(sessie.Token);
Assert.True((await bewaring.ZoekAsync(SessionStore.Hash(sessie.Token)))!.VerlooptOp > opgeslagen,
"maar na een paar uur schuift hij wel op");
});
runner.TestAsync("Zonder opslag werkt alles, alleen niet na een herstart", async () =>
{
// De zelftests en de proefopstellingen draaien zonder database. Dat hoort gewoon te
// werken; wat je dan mist is precies het ene ding waar de tabel voor is.
var klok = new TestClock();
var kast = new SessionStore(klok);
var sessie = await kast.CreateAsync(1, 1, null);
Assert.True(await kast.ValidateAsync(sessie.Token) is not null, "binnen dezelfde start werkt hij");
var na = new SessionStore(klok);
Assert.True(await na.ValidateAsync(sessie.Token) is null, "na een herstart niet meer");
});
}
}