198 lines
8.8 KiB
C#
198 lines
8.8 KiB
C#
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");
|
|
});
|
|
}
|
|
}
|