Eerste commit
This commit is contained in:
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user