Eerste commit
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
using System.Globalization;
|
||||
using MijnBesluit.Application.Services;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Adverteren: de rekensom, de weg van indienen naar draaien, en wie er wat mag.
|
||||
///
|
||||
/// Zie docs/Adverteren.md. De twee dingen die hier echt bewaakt worden: dat er niets draait
|
||||
/// waarvoor niet is betaald én naar gekeken, en dat alleen een beheerder die knoppen heeft.
|
||||
/// </summary>
|
||||
public static class AdvertentieChecks
|
||||
{
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("Adverteren: een code voor adverteerders is geen inlogcode", async () =>
|
||||
{
|
||||
// De belangrijkste controle van het hele adverteerdersgedeelte. Een adverteerder is
|
||||
// geen gebruiker van MijnBesluit: zijn code hoort nooit toegang tot een kluis te
|
||||
// geven, en de inlogcode van een gebruiker hoort niet in het adverteerdersgedeelte
|
||||
// te werken. Ze staan in dezelfde tabel, dus het verschil zit in het doel - en
|
||||
// precies zo'n verschil is er een om vast te leggen.
|
||||
var w = new World();
|
||||
await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
||||
|
||||
// Een code voor adverteren, aangevraagd op het adres van een echte gebruiker.
|
||||
Assert.True((await w.Accounts.VraagAdverteerderscodeAanAsync("jan@example.nl", "Kwik-Bit", "Jan")).Succeeded,
|
||||
"de code voor adverteren gaat de deur uit");
|
||||
var reclamecode = w.LaatsteCodeUitMail("jan@example.nl");
|
||||
|
||||
var binnen = await w.Accounts.LogInMetCodeAsync("jan@example.nl", reclamecode);
|
||||
Assert.True(!binnen.Succeeded,
|
||||
"met een adverteerderscode kom je niet in de kluis van die gebruiker");
|
||||
|
||||
// En andersom.
|
||||
w.Clock.Advance(TimeSpan.FromSeconds(31));
|
||||
Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "nu een gewone inlogcode");
|
||||
var inlogcode = w.LaatsteCodeUitMail("jan@example.nl");
|
||||
|
||||
var reclame = await w.Accounts.ControleerAdverteerderscodeAsync("jan@example.nl", inlogcode);
|
||||
Assert.True(!reclame.Succeeded, "en met een inlogcode kom je niet in het adverteerdersgedeelte");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: de bedrijfsnaam reist mee met de code", async () =>
|
||||
{
|
||||
// Wie nieuw is vult zijn bedrijfsnaam in vóórdat hij de code krijgt. Die naam wordt
|
||||
// bij de code bewaard en pas gebruikt zodra de code klopt - zo kan niemand zich een
|
||||
// bedrijfsnaam aanmeten zonder eerst bij dat e-mailadres te kunnen.
|
||||
var w = new World();
|
||||
|
||||
Assert.True((await w.Accounts.VraagAdverteerderscodeAanAsync(
|
||||
"info@kwik-bit.nl", "Kwik-Bit", "Jonathan")).Succeeded, "de code gaat de deur uit");
|
||||
|
||||
var code = w.LaatsteCodeUitMail("info@kwik-bit.nl");
|
||||
var controle = await w.Accounts.ControleerAdverteerderscodeAsync("info@kwik-bit.nl", code);
|
||||
|
||||
Assert.True(controle.Succeeded, $"de code klopt ({controle.Error})");
|
||||
Assert.True(controle.Value!.Bedrijfsnaam == "Kwik-Bit", "met de bedrijfsnaam erbij");
|
||||
Assert.True(controle.Value.Contactpersoon == "Jonathan", "en de contactpersoon");
|
||||
|
||||
var account = await w.Adverteren.MeldAanAsync(controle.Value.Bedrijfsnaam,
|
||||
controle.Value.Contactpersoon, "info@kwik-bit.nl");
|
||||
Assert.True(account.Succeeded, $"en het account wordt aangemaakt ({account.Error})");
|
||||
|
||||
// Dezelfde code werkt geen tweede keer.
|
||||
var nogmaals = await w.Accounts.ControleerAdverteerderscodeAsync("info@kwik-bit.nl", code);
|
||||
Assert.True(!nogmaals.Succeeded, "een code werkt maar één keer");
|
||||
});
|
||||
|
||||
runner.Test("Adverteren: de betaallink klopt, ook op een Nederlandse server", () =>
|
||||
{
|
||||
var w = new World();
|
||||
w.Adverteren.BunqMe = "https://bunq.me/KwikBit";
|
||||
|
||||
var vorige = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
// Precies de server waar dit op draait: een taalinstelling die 30,00 met een
|
||||
// komma schrijft. In de link hoort een punt te staan - bunq leest in een komma
|
||||
// een ander bedrag, of helemaal geen.
|
||||
//
|
||||
// Een zelfgemaakte instelling en niet new CultureInfo("nl-NL"): niet elke
|
||||
// omgeving heeft de landgegevens bij zich (globalization-invariant mode), en dan
|
||||
// valt deze controle om op iets wat er niet toe doet. Waar het om gaat is de
|
||||
// komma, en die zetten we hier zelf.
|
||||
var metKomma = (CultureInfo)CultureInfo.InvariantCulture.Clone();
|
||||
metKomma.NumberFormat.NumberDecimalSeparator = ",";
|
||||
CultureInfo.CurrentCulture = metKomma;
|
||||
|
||||
Assert.True((30.00m).ToString("0.00") == "30,00",
|
||||
"de instelling van deze controle schrijft zelf wél een komma");
|
||||
|
||||
var link = w.Adverteren.Betaallink(3000, "MB-00012");
|
||||
Assert.True(link == "https://bunq.me/KwikBit?amount=30.00&description=MB-00012",
|
||||
$"bedrag met een punt en het kenmerk erin ({link})");
|
||||
}
|
||||
finally { CultureInfo.CurrentCulture = vorige; }
|
||||
|
||||
// Een afsluitende schuine streep in de instelling hoort geen dubbele op te leveren.
|
||||
w.Adverteren.BunqMe = "https://bunq.me/KwikBit/";
|
||||
var netjes = w.Adverteren.Betaallink(1000, "MB-00001");
|
||||
Assert.True(netjes == "https://bunq.me/KwikBit?amount=10.00&description=MB-00001",
|
||||
$"een streep te veel wordt opgeruimd ({netjes})");
|
||||
|
||||
// Op het scherm hoort juist een komma te staan; daar leest een mens het. Er stond
|
||||
// "€ 10.00" op de adverteerderspagina, en dat is niet hoe je hier een bedrag schrijft.
|
||||
Assert.True(AdvertentieService.Euro(3000) == "\u20ac 30,00",
|
||||
$"een bedrag op het scherm heeft een komma ({AdvertentieService.Euro(3000)})");
|
||||
Assert.True(AdvertentieService.Euro(1000) == "\u20ac 10,00", "ook een rond bedrag");
|
||||
|
||||
// Niets ingesteld: dan liever geen knop dan een knop naar de verkeerde rekening.
|
||||
w.Adverteren.BunqMe = "";
|
||||
Assert.True(w.Adverteren.Betaallink(1000, "MB-00001") is null,
|
||||
"zonder ingesteld adres komt er geen betaallink");
|
||||
});
|
||||
|
||||
runner.Test("Adverteren: de prijs is dagen maal vertoningen per uur maal tarief", () =>
|
||||
{
|
||||
var w = new World();
|
||||
|
||||
// Vaste getallen, zodat deze controle blijft kloppen als het tarief in de instellingen
|
||||
// wijzigt. Dat is immers de bedoeling van dat tarief.
|
||||
w.Adverteren.Tarieven.TariefCenten = 20;
|
||||
w.Adverteren.Tarieven.MinimumCenten = 1000;
|
||||
|
||||
// Dertig dagen, vijf per uur: 150 eenheden, 3000 cent.
|
||||
Assert.True(w.Adverteren.BerekenBedrag(30, 5) == 3000, "dertig dagen, vijf per uur is dertig euro");
|
||||
|
||||
// Begin- en einddag tellen allebei mee: van de 1e tot en met de 30e is dertig dagen.
|
||||
var eerste = new DateOnly(2026, 10, 1);
|
||||
Assert.True(w.Adverteren.BerekenBedrag(eerste, eerste.AddDays(29), 5) == 3000,
|
||||
"met datums gerekend komt er hetzelfde uit");
|
||||
Assert.True(w.Adverteren.BerekenBedrag(eerste, eerste, 1) == w.Adverteren.BerekenBedrag(1, 1),
|
||||
"van de eerste tot en met de eerste is één dag");
|
||||
|
||||
// Onder de bodem betaal je de bodem: een kleine boeking kost aan kijken, afvinken en
|
||||
// terugmailen meer dan hij opbrengt.
|
||||
Assert.True(w.Adverteren.BerekenBedrag(1, 1) == 1000, "het kleinste bedrag is de bodem");
|
||||
Assert.True(w.Adverteren.BerekenBedrag(7, 2) == 1000, "zeven dagen, twee per uur haalt de bodem niet");
|
||||
|
||||
Assert.True(w.Adverteren.BerekenBedrag(0, 5) == 0 && w.Adverteren.BerekenBedrag(30, 0) == 0,
|
||||
"een campagne zonder dagen of zonder vertoningen kost niets");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: een campagne wordt pas getoond na betalen én goedkeuren", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer");
|
||||
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
|
||||
|
||||
var bedrijf = (await w.Adverteren.MeldAanAsync("Uitvaartzorg Noord", "Piet", "piet@uitvaart.nl")).Value!;
|
||||
|
||||
var vanaf = DateOnly.FromDateTime(w.Clock.UtcNow);
|
||||
var tot = vanaf.AddDays(29);
|
||||
|
||||
var ingediend = await w.Adverteren.DienInAsync(bedrijf.Id, "Persoonlijk afscheid, dichtbij",
|
||||
"https://uitvaartnoord.nl", vanaf, tot, 5);
|
||||
Assert.True(ingediend.Succeeded, $"indienen lukt ({ingediend.Error})");
|
||||
|
||||
var campagne = ingediend.Value!;
|
||||
Assert.True(campagne.Stand == CampagneStand.WachtOpBetaling, "hij wacht op de betaling");
|
||||
Assert.True(campagne.BedragCenten == w.Adverteren.BerekenBedrag(30, 5), "met het berekende bedrag");
|
||||
Assert.True(campagne.Betaalkenmerk is { Length: > 0 }, "en een kenmerk om de betaling mee terug te vinden");
|
||||
|
||||
// De beheerder hoort het te weten, want vanaf nu ligt het bij hem.
|
||||
Assert.True(w.Email.Sent.Any(m => m.To == "beheer@example.nl"), "de beheerder krijgt bericht");
|
||||
|
||||
// Nog niets te zien: niet betaald, niet gekeurd.
|
||||
Assert.True((await w.Adverteren.WachtOpBeheerderAsync()).Count == 1, "hij staat op de lijst van de beheerder");
|
||||
Assert.True(!campagne.DraaitOp(vanaf), "en draait nog niet");
|
||||
|
||||
// Goedkeuren vóór betalen mag niet. Anders draait er iets waarvoor niet betaald is,
|
||||
// en dat merk je pas als het voorbij is.
|
||||
var tevroeg = await w.Adverteren.GoedkeurenAsync(campagne.Id, beheerder.Id);
|
||||
Assert.True(!tevroeg.Succeeded, "goedkeuren kan niet zolang er niet betaald is");
|
||||
|
||||
Assert.True((await w.Adverteren.BetaaldAsync(campagne.Id, beheerder.Id)).Succeeded, "betaling afvinken");
|
||||
Assert.True(campagne.Stand == CampagneStand.WachtOpGoedkeuring, "nu wacht hij op de inhoudelijke blik");
|
||||
Assert.True(!campagne.DraaitOp(vanaf), "betaald is nog niet goedgekeurd");
|
||||
|
||||
Assert.True((await w.Adverteren.GoedkeurenAsync(campagne.Id, beheerder.Id)).Succeeded, "goedkeuren");
|
||||
Assert.True(campagne.Stand == CampagneStand.Loopt, "hij loopt");
|
||||
Assert.True(campagne.DraaitOp(vanaf) && campagne.DraaitOp(tot), "op de eerste en de laatste dag");
|
||||
Assert.True(!campagne.DraaitOp(tot.AddDays(1)), "en een dag later niet meer");
|
||||
|
||||
Assert.True((await w.Adverteren.WachtOpBeheerderAsync()).Count == 0, "er wacht niets meer");
|
||||
Assert.True(w.Email.Sent.Any(m => m.To == "piet@uitvaart.nl"), "de adverteerder hoort dat hij is goedgekeurd");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: wie het verst achterloopt is aan de beurt", async () =>
|
||||
{
|
||||
// Twee campagnes tegelijk, met een verschillend aantal per uur. Wie meer boekt hoort
|
||||
// meer te krijgen, zonder dat de een de ander verdringt en zonder dat iemand over
|
||||
// zijn ingekochte aantal heen gaat.
|
||||
var w = new World();
|
||||
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer");
|
||||
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
|
||||
|
||||
var groot = await LopendAsync(w, beheerder, "Groot BV", "groot@example.nl", perUur: 3);
|
||||
var klein = await LopendAsync(w, beheerder, "Klein BV", "klein@example.nl", perUur: 1);
|
||||
|
||||
// Vier keer kiezen binnen hetzelfde klokuur. Groot heeft er drie gekocht, Klein één,
|
||||
// dus de verdeling hoort 3 om 1 te zijn.
|
||||
var nu = w.Clock.UtcNow;
|
||||
var beurten = new List<long>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var gekozen = await w.Adverteren.KiesAsync(nu);
|
||||
Assert.True(gekozen is not null, $"er is een advertentie bij beurt {i + 1}");
|
||||
beurten.Add(gekozen!.Id);
|
||||
await w.Adverteren.TelVertoningAsync(gekozen.Id, nu);
|
||||
}
|
||||
|
||||
Assert.True(beurten.Count(id => id == groot.Id) == 3, "Groot komt drie keer aan bod");
|
||||
Assert.True(beurten.Count(id => id == klein.Id) == 1, "Klein één keer");
|
||||
|
||||
// Allebei op hun aantal: dan verschijnt er niets. Geen opvulling, geen herhaling.
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu) is null, "daarna is het uur op");
|
||||
|
||||
// Het volgende klokuur begint het opnieuw.
|
||||
var volgendUur = nu.AddHours(1);
|
||||
Assert.True(await w.Adverteren.KiesAsync(volgendUur) is not null, "een uur later mag het weer");
|
||||
|
||||
// En wat er werkelijk is getoond staat klaar voor de adverteerder.
|
||||
var totalen = await w.Adverteren.TotalenAsync(groot.Id);
|
||||
Assert.True(totalen.Vertoningen == 3 && totalen.Kliks == 0, "de tellers kloppen");
|
||||
|
||||
await w.Adverteren.TelKlikAsync(groot.Id, nu);
|
||||
Assert.True((await w.Adverteren.TotalenAsync(groot.Id)).Kliks == 1, "en een klik telt apart");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: buiten de periode of zonder goedkeuring komt er niets", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer");
|
||||
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
|
||||
|
||||
var nu = w.Clock.UtcNow;
|
||||
var vandaag = DateOnly.FromDateTime(nu);
|
||||
|
||||
// Betaald en goedgekeurd, maar hij begint pas morgen.
|
||||
var straks = (await w.Adverteren.MeldAanAsync("Later BV", "L", "later@example.nl")).Value!;
|
||||
var morgen = (await w.Adverteren.DienInAsync(straks.Id, "Binnenkort", "https://later.nl",
|
||||
vandaag.AddDays(1), vandaag.AddDays(8), 5)).Value!;
|
||||
await w.Adverteren.BetaaldAsync(morgen.Id, beheerder.Id);
|
||||
await w.Adverteren.GoedkeurenAsync(morgen.Id, beheerder.Id);
|
||||
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu) is null, "een campagne die morgen begint draait vandaag niet");
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu.AddDays(1)) is not null, "morgen wel");
|
||||
|
||||
// Betaald maar niet goedgekeurd: dat hoort niets te tonen. Dit is de belangrijkste
|
||||
// van allemaal - hier zou een niet-gekeurde advertentie langs de beheerder glippen.
|
||||
var wacht = (await w.Adverteren.MeldAanAsync("Wacht BV", "W", "wacht@example.nl")).Value!;
|
||||
var ongekeurd = (await w.Adverteren.DienInAsync(wacht.Id, "Nog niet gezien", "https://wacht.nl",
|
||||
vandaag, vandaag.AddDays(8), 5)).Value!;
|
||||
await w.Adverteren.BetaaldAsync(ongekeurd.Id, beheerder.Id);
|
||||
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu) is null, "betaald zonder goedkeuring komt er niet in");
|
||||
|
||||
// En een stopgezette campagne verdwijnt meteen.
|
||||
await w.Adverteren.GoedkeurenAsync(ongekeurd.Id, beheerder.Id);
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu) is not null, "na goedkeuren wel");
|
||||
|
||||
await w.Adverteren.StopzettenAsync(ongekeurd.Id, beheerder.Id);
|
||||
Assert.True(await w.Adverteren.KiesAsync(nu) is null, "stopzetten haalt hem er meteen uit");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: alleen een beheerder mag afvinken en goedkeuren", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var gewoon = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
||||
var bedrijf = (await w.Adverteren.MeldAanAsync("Bloemen Zo", "Riet", "riet@bloemen.nl")).Value!;
|
||||
|
||||
var vanaf = DateOnly.FromDateTime(w.Clock.UtcNow);
|
||||
var campagne = (await w.Adverteren.DienInAsync(bedrijf.Id, "Rouwbloemen",
|
||||
"https://bloemenzo.nl", vanaf, vanaf.AddDays(6), 2)).Value!;
|
||||
|
||||
var betalen = await w.Adverteren.BetaaldAsync(campagne.Id, gewoon.Id);
|
||||
Assert.True(!betalen.Succeeded, "een gewone gebruiker vinkt niets af");
|
||||
|
||||
// Dezelfde melding als "bestaat niet": wie geen beheerder is, hoort niet te weten dat
|
||||
// hier iets te beheren valt.
|
||||
Assert.True(betalen.Error == "Niet gevonden.", "en krijgt niet te horen dat het aan zijn rechten ligt");
|
||||
|
||||
Assert.True(!(await w.Adverteren.GoedkeurenAsync(campagne.Id, gewoon.Id)).Succeeded, "en keurt niets goed");
|
||||
Assert.True(campagne.Stand == CampagneStand.WachtOpBetaling, "er is niets veranderd");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: afwijzen kan niet zonder reden", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer");
|
||||
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
|
||||
|
||||
var bedrijf = (await w.Adverteren.MeldAanAsync("Iets Anders", "Kees", "kees@anders.nl")).Value!;
|
||||
var vanaf = DateOnly.FromDateTime(w.Clock.UtcNow);
|
||||
var campagne = (await w.Adverteren.DienInAsync(bedrijf.Id, "Koop nu",
|
||||
"https://anders.nl", vanaf, vanaf.AddDays(6), 2)).Value!;
|
||||
|
||||
Assert.True(!(await w.Adverteren.AfwijzenAsync(campagne.Id, beheerder.Id, " ")).Succeeded,
|
||||
"zonder reden wijs je niets af");
|
||||
|
||||
Assert.True((await w.Adverteren.AfwijzenAsync(campagne.Id, beheerder.Id, "Past niet bij dit product")).Succeeded,
|
||||
"met een reden wel");
|
||||
Assert.True(campagne.Stand == CampagneStand.Afgewezen, "hij staat als afgewezen");
|
||||
Assert.True(campagne.AfwijzingReden == "Past niet bij dit product", "met de reden erbij");
|
||||
|
||||
// De adverteerder hoort te weten waaróm. Anders dient hij hetzelfde nog een keer in.
|
||||
var bericht = w.Email.Sent.Last(m => m.To == "kees@anders.nl");
|
||||
Assert.True(bericht.Body.Contains("Past niet bij dit product"), "en krijgt die reden te lezen");
|
||||
});
|
||||
|
||||
runner.TestAsync("Adverteren: wat er niet in mag", async () =>
|
||||
{
|
||||
var w = new World();
|
||||
var bedrijf = (await w.Adverteren.MeldAanAsync("Test BV", "Tester", "test@example.nl")).Value!;
|
||||
var vanaf = new DateOnly(2026, 10, 1);
|
||||
|
||||
// Een link die code uitvoert bij de lezer. Dit is de reden dat er op het schema wordt
|
||||
// gecontroleerd en niet alleen op "is het gevuld".
|
||||
var script = await w.Adverteren.DienInAsync(bedrijf.Id, "Klik hier",
|
||||
"javascript:alert(1)", vanaf, vanaf.AddDays(6), 2);
|
||||
Assert.True(!script.Succeeded, "een javascript-link komt er niet in");
|
||||
|
||||
var geenAdres = await w.Adverteren.DienInAsync(bedrijf.Id, "Klik hier",
|
||||
"www.voorbeeld.nl", vanaf, vanaf.AddDays(6), 2);
|
||||
Assert.True(!geenAdres.Succeeded, "en een adres zonder http:// ook niet");
|
||||
|
||||
var omgekeerd = await w.Adverteren.DienInAsync(bedrijf.Id, "Klik hier",
|
||||
"https://voorbeeld.nl", vanaf.AddDays(6), vanaf, 2);
|
||||
Assert.True(!omgekeerd.Succeeded, "de einddatum mag niet voor de begindatum liggen");
|
||||
|
||||
var teveel = await w.Adverteren.DienInAsync(bedrijf.Id, "Klik hier",
|
||||
"https://voorbeeld.nl", vanaf, vanaf.AddDays(6), w.Adverteren.Tarieven.MaxPerUur + 1);
|
||||
Assert.True(!teveel.Succeeded, "en niemand koopt alle ruimte van een uur op");
|
||||
|
||||
Assert.True(w.Store.Campagnes.Count == 0, "er is niets van dit alles opgeslagen");
|
||||
|
||||
// Hetzelfde e-mailadres kan geen tweede account: daarmee logt hij in.
|
||||
var nogeens = await w.Adverteren.MeldAanAsync("Test BV 2", "Tester", "test@example.nl");
|
||||
Assert.True(!nogeens.Succeeded, "eenzelfde e-mailadres kan geen tweede account");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Een adverteerder met één campagne die vanaf vandaag draait: betaald en goedgekeurd.</summary>
|
||||
private static async Task<MijnBesluit.Domain.Entities.Campagne> LopendAsync(
|
||||
World w, MijnBesluit.Domain.Entities.User beheerder, string bedrijf, string email, int perUur)
|
||||
{
|
||||
var adverteerder = (await w.Adverteren.MeldAanAsync(bedrijf, "Contact", email)).Value!;
|
||||
var vandaag = DateOnly.FromDateTime(w.Clock.UtcNow);
|
||||
|
||||
var campagne = (await w.Adverteren.DienInAsync(adverteerder.Id, $"Advertentie van {bedrijf}",
|
||||
"https://voorbeeld.nl", vandaag, vandaag.AddDays(30), perUur)).Value!;
|
||||
|
||||
await w.Adverteren.BetaaldAsync(campagne.Id, beheerder.Id);
|
||||
await w.Adverteren.GoedkeurenAsync(campagne.Id, beheerder.Id);
|
||||
return campagne;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,820 @@
|
||||
using System.Net;
|
||||
using MijnBesluit.Client;
|
||||
using MijnBesluit.Protocol;
|
||||
using MijnBesluit.Server;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Controleert de laag waar de Android- en iOS-app op draait: MijnBesluitClient, over een echte
|
||||
/// socketverbinding naar een echte dispatcher. Alleen de database is vervangen door geheugen.
|
||||
///
|
||||
/// Dit is bewust geen test met een nagebootste server: juist de vertaalslag van JSON naar de
|
||||
/// modellen van de app is waar het misgaat, en die wordt hier echt gedaan.
|
||||
/// </summary>
|
||||
public static class ClientChecks
|
||||
{
|
||||
/// <summary>Een draaiende SocketServer met opslag in het geheugen, zoals de app hem ziet.</summary>
|
||||
private sealed class Proef : IAsyncDisposable
|
||||
{
|
||||
public World Wereld { get; } = new();
|
||||
public SocketListener Server { get; }
|
||||
|
||||
/// <summary>De sessies van de server, om een herstart mee na te bootsen.</summary>
|
||||
public SessionStore Sessies { get; }
|
||||
|
||||
public Proef()
|
||||
{
|
||||
var sessies = Sessies = new SessionStore(Wereld.Clock);
|
||||
var dispatcher = new SocketDispatcher(sessies, work => work(new RequestScope
|
||||
{
|
||||
Accounts = Wereld.Accounts,
|
||||
Families = Wereld.Families,
|
||||
Vault = Wereld.Vault,
|
||||
Messages = Wereld.Messages,
|
||||
Legacy = Wereld.Legacy,
|
||||
Overzicht = Wereld.Overzicht,
|
||||
Samenvoegen = Wereld.Samenvoegen
|
||||
}), new Bestandsoverdracht(Wereld.Clock));
|
||||
|
||||
Server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
dispatcher.HandleAsync);
|
||||
Server.Start();
|
||||
}
|
||||
|
||||
public MijnBesluitClient App(ITokenopslag? opslag = null) =>
|
||||
new("127.0.0.1", Server.Port, tls: false, opslag);
|
||||
|
||||
/// <summary>Registreert een verse gebruiker en geeft een aangemelde app terug.</summary>
|
||||
public async Task<MijnBesluitClient> AangemeldeAppAsync(
|
||||
string email, string voornaam = "Jan", string achternaam = "Jansen", ITokenopslag? opslag = null)
|
||||
{
|
||||
var app = App(opslag);
|
||||
var aanvraag = await app.VraagRegistratiecodeAanAsync(email, voornaam, achternaam);
|
||||
Assert.True(aanvraag.Gelukt, $"registratiecode aanvragen lukt ({aanvraag.Melding})");
|
||||
|
||||
var registratie = await app.RegistreerAsync(email, Wereld.LaatsteCodeUitMail(email));
|
||||
Assert.True(registratie.Gelukt, $"registreren lukt ({registratie.Melding})");
|
||||
return app;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync() => await Server.DisposeAsync();
|
||||
}
|
||||
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("App: registreren met een code en de sessie bewaren", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
await using var app = proef.App(opslag);
|
||||
|
||||
var aanvraag = await app.VraagRegistratiecodeAanAsync("jan@example.nl", "Jan", "Jansen");
|
||||
Assert.True(aanvraag.Gelukt, "de code wordt verstuurd");
|
||||
Assert.True(!aanvraag.Waarde!.VerstuurdNaar.Contains("jan@example.nl"),
|
||||
"het volledige adres komt niet terug naar de app");
|
||||
Assert.True(aanvraag.Waarde.GeldigTot > proef.Wereld.Clock.UtcNow,
|
||||
"er komt een geldigheidsduur mee");
|
||||
|
||||
var registratie = await app.RegistreerAsync("jan@example.nl",
|
||||
proef.Wereld.LaatsteCodeUitMail("jan@example.nl"));
|
||||
Assert.True(registratie.Gelukt, "registreren lukt met de code uit de e-mail");
|
||||
Assert.True(app.IsAangemeld, "de app is daarna aangemeld");
|
||||
Assert.True(app.PersoonId > 0, "de app kent zijn eigen persoonId");
|
||||
Assert.True(!string.IsNullOrEmpty(await opslag.LeesAsync()),
|
||||
"het token is bewaard, zodat de app niet elke start opnieuw hoeft in te loggen");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een verkeerde code geeft een melding, geen uitzondering", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await proef.Wereld.MaakGebruikerAsync("piet@example.nl", "Piet");
|
||||
await using var app = proef.App();
|
||||
|
||||
Assert.True((await app.VraagInlogcodeAanAsync("piet@example.nl")).Gelukt, "de inlogcode wordt verstuurd");
|
||||
|
||||
var mis = await app.LogInAsync("piet@example.nl", "000000");
|
||||
Assert.True(!mis.Gelukt, "een verkeerde code wordt geweigerd");
|
||||
Assert.True(!string.IsNullOrWhiteSpace(mis.Melding), "er komt een melding mee die op het scherm kan");
|
||||
Assert.True(!app.IsAangemeld, "de app is niet aangemeld");
|
||||
|
||||
var goed = await app.LogInAsync("piet@example.nl", proef.Wereld.LaatsteCodeUitMail("piet@example.nl"));
|
||||
Assert.True(goed.Gelukt, "met de juiste code lukt het wel");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: de eigen familie komt binnen via family.list", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var families = await app.FamiliesAsync();
|
||||
Assert.True(families.Gelukt, $"de familielijst komt terug ({families.Melding})");
|
||||
Assert.True(families.Waarde!.Count == 1, "een nieuwe gebruiker heeft precies één familie");
|
||||
Assert.True(families.Waarde[0].Eigenaar, "en daar is hij eigenaar van");
|
||||
Assert.True(!string.IsNullOrWhiteSpace(families.Waarde[0].Name), "de familie heeft een naam");
|
||||
|
||||
var leden = await app.FamilieledenAsync(families.Waarde[0].Id);
|
||||
Assert.True(leden.Gelukt, $"de leden komen terug ({leden.Melding})");
|
||||
var ikzelf = leden.Waarde!.Single(l => l.PersonId == app.PersoonId);
|
||||
Assert.True(ikzelf.State == ProfielStand.Geclaimd, "het eigen profiel staat als geclaimd");
|
||||
Assert.True(ikzelf.CanEdit, "en is te bewerken");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: familielid toevoegen en uitnodigen", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
|
||||
var toegevoegd = await app.VoegFamilielidToeAsync(familieId, "Marie", "Jansen",
|
||||
"marie@example.nl", relatie: 1);
|
||||
Assert.True(toegevoegd.Gelukt, $"een partner toevoegen lukt ({toegevoegd.Melding})");
|
||||
var marieId = toegevoegd.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var leden = await app.FamilieledenAsync(familieId);
|
||||
var marie = leden.Waarde!.Single(l => l.PersonId == marieId);
|
||||
Assert.True(marie.State == ProfielStand.Beheerd, "het profiel is nog van de aanmaker");
|
||||
Assert.True(marie.CanEdit, "en hij mag het dus wijzigen");
|
||||
Assert.True(!marie.IsGeclaimd, "het is nog niet geclaimd");
|
||||
|
||||
var uitnodiging = await app.NodigUitAsync(familieId, marieId);
|
||||
Assert.True(uitnodiging.Gelukt, $"uitnodigen lukt ({uitnodiging.Melding})");
|
||||
|
||||
var naUitnodiging = await app.FamilieledenAsync(familieId);
|
||||
Assert.True(naUitnodiging.Waarde!.Single(l => l.PersonId == marieId).State == ProfielStand.Uitgenodigd,
|
||||
"de stand staat daarna op uitgenodigd");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een familielid wijzigen, ook na een uitnodiging", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
|
||||
// Zoals het in het echt gaat: een tikfout in het adres.
|
||||
var toegevoegd = await app.VoegFamilielidToeAsync(familieId, "Jarron", "Flietstra",
|
||||
"jarron@gmial.com", relatie: 2);
|
||||
var jarronId = toegevoegd.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var gewijzigd = await app.WijzigFamilielidAsync(familieId, jarronId, "Jarron", "Flietstra",
|
||||
"jarron@gmail.com", relatie: 11);
|
||||
Assert.True(gewijzigd.Gelukt, $"wijzigen lukt ({gewijzigd.Melding})");
|
||||
|
||||
var leden = await app.FamilieledenAsync(familieId);
|
||||
var jarron = leden.Waarde!.Single(l => l.PersonId == jarronId);
|
||||
Assert.True(jarron.Email == "jarron@gmail.com", "het gecorrigeerde adres komt terug");
|
||||
Assert.True(jarron.Relation == 11, "en de relatie staat nu op zoon");
|
||||
Assert.True(jarron.FirstName == "Jarron", "de voornaam komt mee, zodat het scherm in te vullen is");
|
||||
|
||||
var bezet = await app.WijzigFamilielidAsync(familieId, jarronId, "Jarron", "Flietstra",
|
||||
"jan@example.nl", relatie: 11);
|
||||
Assert.True(!bezet.Gelukt, "het adres van een bestaand profiel wordt geweigerd");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: kluisitem opslaan, terugzien en openen", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var opslaan = await app.KluisOpslaanAsync(
|
||||
categorie: 3, titel: "Bank",
|
||||
inhoud: new KluisInhoud { Website = "bank.nl", Username = "jan", Secret = "zeergeheim" },
|
||||
vrijgave: 1);
|
||||
Assert.True(opslaan.Gelukt, $"opslaan lukt ({opslaan.Melding})");
|
||||
var itemId = opslaan.Waarde.GetProperty("itemId").GetInt64();
|
||||
|
||||
var lijst = await app.KluisAsync();
|
||||
Assert.True(lijst.Gelukt, $"de kluislijst komt terug ({lijst.Melding})");
|
||||
Assert.True(lijst.Waarde!.Single().Title == "Bank", "met de titel erbij");
|
||||
Assert.True(lijst.Waarde!.Single().Category == 3, "en de categorie");
|
||||
|
||||
var geopend = await app.KluisOpenenAsync(itemId);
|
||||
Assert.True(geopend.Gelukt, $"openen lukt ({geopend.Melding})");
|
||||
Assert.True(geopend.Waarde!.Secret == "zeergeheim", "het geheim komt ongeschonden terug");
|
||||
Assert.True(geopend.Waarde.Website == "bank.nl", "en de rest van de inhoud ook");
|
||||
|
||||
var alleenPolissen = await app.KluisAsync(categorie: 2);
|
||||
Assert.True(alleenPolissen.Waarde!.Count == 0, "filteren op categorie werkt");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: kluisitem wijzigen en verwijderen over de socket", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var opslaan = await app.KluisOpslaanAsync(3, "Bank",
|
||||
new KluisInhoud { Website = "bank.nl", Secret = "eerstegeheim" }, vrijgave: 1);
|
||||
var itemId = opslaan.Waarde.GetProperty("itemId").GetInt64();
|
||||
|
||||
var gewijzigd = await app.KluisWijzigenAsync(itemId, 3, "Bank en sparen",
|
||||
new KluisInhoud { Website = "bank.nl", Secret = "tweedegeheim" }, vrijgave: 1);
|
||||
Assert.True(gewijzigd.Gelukt, $"wijzigen lukt ({gewijzigd.Melding})");
|
||||
|
||||
var geopend = await app.KluisOpenenAsync(itemId);
|
||||
Assert.True(geopend.Waarde!.Secret == "tweedegeheim", "de nieuwe inhoud komt terug");
|
||||
Assert.True((await app.KluisAsync()).Waarde!.Single().Title == "Bank en sparen", "en de nieuwe titel");
|
||||
|
||||
var verwijderd = await app.KluisVerwijderenAsync(itemId);
|
||||
Assert.True(verwijderd.Gelukt, $"verwijderen lukt ({verwijderd.Melding})");
|
||||
Assert.True((await app.KluisAsync()).Waarde!.Count == 0, "de kluis is weer leeg");
|
||||
Assert.True(!(await app.KluisOpenenAsync(itemId)).Gelukt, "het item is niet meer te openen");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: bericht wijzigen en terugtrekken over de socket", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await app.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", null, relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var klaargezet = await app.BerichtKlaarzettenAsync(evaId, "Voor jou", "Eerste versie", null, true);
|
||||
var berichtId = klaargezet.Waarde.GetProperty("berichtId").GetInt64();
|
||||
|
||||
var gewijzigd = await app.BerichtWijzigenAsync(berichtId, "Voor jou, Eva", "Tweede versie", null, true);
|
||||
Assert.True(gewijzigd.Gelukt, $"wijzigen lukt ({gewijzigd.Melding})");
|
||||
|
||||
var uitgaand = await app.UitgaandAsync();
|
||||
Assert.True(uitgaand.Waarde!.Single().Subject == "Voor jou, Eva", "het nieuwe onderwerp staat er");
|
||||
|
||||
var tekst = await app.BerichtConceptAsync(berichtId);
|
||||
Assert.True(tekst.Gelukt, $"je eigen concept is terug te lezen ({tekst.Melding})");
|
||||
Assert.True(tekst.Waarde == "Tweede versie", "en de nieuwe tekst staat erin");
|
||||
|
||||
Assert.True(!(await app.BerichtLezenAsync(berichtId)).Gelukt,
|
||||
"lezen als ontvanger kan niet: het bericht is niet voor mij en nog niet bezorgd");
|
||||
|
||||
var ingetrokken = await app.BerichtIntrekkenAsync(berichtId);
|
||||
Assert.True(ingetrokken.Gelukt, $"terugtrekken lukt ({ingetrokken.Melding})");
|
||||
Assert.True((await app.UitgaandAsync()).Waarde!.Count == 0, "het staat niet meer in uitgaande post");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: familielid verwijderen over de socket", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await app.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", "eva@example.nl", relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
Assert.True((await app.FamilieledenAsync(familieId)).Waarde!.Any(l => l.PersonId == evaId),
|
||||
"Eva staat erin");
|
||||
|
||||
var weg = await app.VerwijderFamilielidAsync(familieId, evaId);
|
||||
Assert.True(weg.Gelukt, $"verwijderen lukt ({weg.Melding})");
|
||||
Assert.True((await app.FamilieledenAsync(familieId)).Waarde!.All(l => l.PersonId != evaId),
|
||||
"en daarna staat ze er niet meer in");
|
||||
|
||||
var zichzelf = await app.VerwijderFamilielidAsync(familieId, app.PersoonId);
|
||||
Assert.True(!zichzelf.Gelukt, "jezelf verwijderen kan niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een bijlage in stukken versturen en weer ophalen", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var itemId = (await app.KluisOpslaanAsync(2, "Uitvaartverzekering",
|
||||
new KluisInhoud { Insurer = "Verzekeraar" }, vrijgave: 1)).Waarde.GetProperty("itemId").GetInt64();
|
||||
|
||||
// Ruim groter dan één stuk, zodat het opknippen echt gebeurt: 1,3 MB.
|
||||
var inhoud = new byte[1_300_000];
|
||||
Random.Shared.NextBytes(inhoud);
|
||||
|
||||
var voortgang = new List<long>();
|
||||
var verstuurd = await app.BestandVersturenAsync("polis.pdf", "application/pdf", inhoud, itemId,
|
||||
(gedaan, _) => voortgang.Add(gedaan));
|
||||
Assert.True(verstuurd.Gelukt, $"versturen lukt ({verstuurd.Melding})");
|
||||
Assert.True(voortgang.Count >= 3, $"het ging in stukken ({voortgang.Count} stuks)");
|
||||
Assert.True(voortgang[^1] == inhoud.LongLength, "de voortgang eindigt op de volledige grootte");
|
||||
|
||||
var lijst = await app.BijlagenAsync(itemId);
|
||||
Assert.True(lijst.Gelukt, $"de bijlagen komen terug ({lijst.Melding})");
|
||||
var bijlage = lijst.Waarde!.Single();
|
||||
Assert.True(bijlage.FileName == "polis.pdf", "met de juiste naam");
|
||||
Assert.True(bijlage.SizeBytes == inhoud.LongLength, "en de juiste grootte");
|
||||
Assert.True(bijlage.Grootte == "1,2 MB" || bijlage.Grootte.EndsWith("MB"), "leesbaar getoond");
|
||||
|
||||
var terug = await app.BestandOphalenAsync(verstuurd.Waarde);
|
||||
Assert.True(terug.Gelukt, $"ophalen lukt ({terug.Melding})");
|
||||
Assert.True(terug.Waarde!.Inhoud.SequenceEqual(inhoud), "byte voor byte hetzelfde als wat erin ging");
|
||||
Assert.True(terug.Waarde.Soort == "application/pdf", "en het soort klopt");
|
||||
|
||||
var weg = await app.BijlageVerwijderenAsync(bijlage.Id);
|
||||
Assert.True(weg.Gelukt, $"verwijderen lukt ({weg.Melding})");
|
||||
Assert.True((await app.BijlagenAsync(itemId)).Waarde!.Count == 0, "de bijlage is weg");
|
||||
Assert.True(!(await app.BestandOphalenAsync(bijlage.Id)).Gelukt, "en niet meer op te halen");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een bijlage van iemand anders is niet te bereiken", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var jan = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var itemId = (await jan.KluisOpslaanAsync(3, "Bank", new KluisInhoud { Secret = "geheim" }, 1))
|
||||
.Waarde.GetProperty("itemId").GetInt64();
|
||||
var bestandId = (await jan.BestandVersturenAsync("geheim.txt", "text/plain",
|
||||
System.Text.Encoding.UTF8.GetBytes("staat hier iets geheims"), itemId)).Waarde;
|
||||
|
||||
await using var piet = await proef.AangemeldeAppAsync("piet@example.nl", "Piet", "Pietersen");
|
||||
|
||||
Assert.True(!(await piet.BestandOphalenAsync(bestandId)).Gelukt, "Piet kan het bestand niet openen");
|
||||
Assert.True(!(await piet.BijlagenAsync(itemId)).Gelukt, "en ziet de bijlagenlijst niet");
|
||||
Assert.True(!(await piet.BijlageVerwijderenAsync(bestandId)).Gelukt, "en kan het niet verwijderen");
|
||||
|
||||
// Het bestand is er voor Jan nog gewoon.
|
||||
Assert.True((await jan.BestandOphalenAsync(bestandId)).Gelukt, "voor Jan werkt het wel");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: je eigen profiel lezen en wijzigen", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var profiel = await app.EigenProfielAsync();
|
||||
Assert.True(profiel.Gelukt, $"je eigen profiel komt terug ({profiel.Melding})");
|
||||
Assert.True(profiel.Waarde!.Email == "jan@example.nl", "met je inlogadres erbij");
|
||||
Assert.True(profiel.Waarde.PersoonId == app.PersoonId, "en het is jouw persoon");
|
||||
Assert.True(profiel.Waarde.Geboortedatum is null, "nog zonder geboortedatum");
|
||||
|
||||
var opgeslagen = await app.EigenProfielOpslaanAsync("Johannes", "Jansen",
|
||||
new DateOnly(1967, 3, 12));
|
||||
Assert.True(opgeslagen.Gelukt, $"wijzigen lukt ({opgeslagen.Melding})");
|
||||
|
||||
profiel = await app.EigenProfielAsync();
|
||||
Assert.True(profiel.Waarde!.Voornaam == "Johannes", "de nieuwe voornaam staat er");
|
||||
Assert.True(profiel.Waarde.Geboortedatum == new DateOnly(1967, 3, 12), "en de geboortedatum");
|
||||
Assert.True(profiel.Waarde.Naam == "Johannes Jansen", "de volledige naam klopt");
|
||||
|
||||
var leeg = await app.EigenProfielOpslaanAsync("", "", null);
|
||||
Assert.True(!leeg.Gelukt, "zonder naam gaat het niet");
|
||||
|
||||
var toekomst = await app.EigenProfielOpslaanAsync(
|
||||
"Jan", "Jansen", DateOnly.FromDateTime(proef.Wereld.Clock.UtcNow.AddYears(1).Date));
|
||||
Assert.True(!toekomst.Gelukt, "en een geboortedatum in de toekomst ook niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: je e-mailadres wijzigen gaat via een code naar het nieuwe adres", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var gevraagd = await app.AdreswijzigingAanvragenAsync("jan@kwik-bit.nl");
|
||||
Assert.True(gevraagd.Gelukt, $"de code wordt aangevraagd ({gevraagd.Melding})");
|
||||
Assert.True(!gevraagd.Waarde!.VerstuurdNaar.Contains("jan@kwik-bit.nl"),
|
||||
"het volledige adres komt niet terug naar de app");
|
||||
|
||||
// Zolang de code niet is ingevuld verandert er niets: dat is de hele bedoeling.
|
||||
Assert.True((await app.EigenProfielAsync()).Waarde!.Email == "jan@example.nl",
|
||||
"het oude adres geldt nog");
|
||||
|
||||
var mis = await app.AdreswijzigingBevestigenAsync("jan@kwik-bit.nl", "000000");
|
||||
Assert.True(!mis.Gelukt, "een verkeerde code wordt geweigerd");
|
||||
Assert.True((await app.EigenProfielAsync()).Waarde!.Email == "jan@example.nl",
|
||||
"en ook dan verandert er niets");
|
||||
|
||||
var code = proef.Wereld.LaatsteCodeUitMail("jan@kwik-bit.nl");
|
||||
var goed = await app.AdreswijzigingBevestigenAsync("jan@kwik-bit.nl", code);
|
||||
Assert.True(goed.Gelukt, $"met de juiste code lukt het wel ({goed.Melding})");
|
||||
Assert.True((await app.EigenProfielAsync()).Waarde!.Email == "jan@kwik-bit.nl",
|
||||
"en nu staat het nieuwe adres er");
|
||||
|
||||
// Inloggen gaat vanaf nu op het nieuwe adres, en niet meer op het oude.
|
||||
await using var opnieuw = proef.App();
|
||||
Assert.True(!(await opnieuw.VraagInlogcodeAanAsync("jan@example.nl")).Gelukt,
|
||||
"het oude adres kent de server niet meer");
|
||||
Assert.True((await opnieuw.VraagInlogcodeAanAsync("jan@kwik-bit.nl")).Gelukt,
|
||||
"en op het nieuwe adres kun je een code aanvragen");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een e-mailadres dat al bezet is wordt geweigerd", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var jan = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
await using var piet = await proef.AangemeldeAppAsync("piet@example.nl", "Piet", "Pietersen");
|
||||
|
||||
var bezet = await jan.AdreswijzigingAanvragenAsync("piet@example.nl");
|
||||
Assert.True(!bezet.Gelukt, "het adres van Piet is niet over te nemen");
|
||||
|
||||
var zelfde = await jan.AdreswijzigingAanvragenAsync("jan@example.nl");
|
||||
Assert.True(!zelfde.Gelukt, "en je eigen adres opnieuw instellen heeft geen zin");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: foto's bij een persoon, met een profielfoto erbij", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await app.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", null, relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var eerste = await app.BestandVersturenAsync("eva.jpg", "image/jpeg",
|
||||
System.Text.Encoding.UTF8.GetBytes("doet alsof het een foto is"),
|
||||
persoonId: evaId);
|
||||
Assert.True(eerste.Gelukt, $"een foto bij Eva opslaan lukt ({eerste.Melding})");
|
||||
|
||||
var tweede = await app.BestandVersturenAsync("eva-2.jpg", "image/jpeg",
|
||||
System.Text.Encoding.UTF8.GetBytes("en dit is de tweede"),
|
||||
persoonId: evaId);
|
||||
Assert.True(tweede.Gelukt, $"en een tweede ook ({tweede.Melding})");
|
||||
|
||||
var fotos = await app.PersoonsfotosAsync(evaId);
|
||||
Assert.True(fotos.Gelukt, $"de foto's komen terug ({fotos.Melding})");
|
||||
Assert.True(fotos.Waarde!.Count == 2, "allebei");
|
||||
Assert.True(fotos.Waarde.All(f => !f.IsProfilePhoto), "nog geen van beide is profielfoto");
|
||||
|
||||
Assert.True((await app.ProfielfotoAsync(eerste.Waarde)).Gelukt, "de eerste aanwijzen lukt");
|
||||
fotos = await app.PersoonsfotosAsync(evaId);
|
||||
Assert.True(fotos.Waarde!.Single(f => f.Id == eerste.Waarde).IsProfilePhoto, "die is het nu");
|
||||
|
||||
// Een andere aanwijzen laat de vorige los: er kan er maar één zijn.
|
||||
Assert.True((await app.ProfielfotoAsync(tweede.Waarde)).Gelukt, "de tweede aanwijzen lukt ook");
|
||||
fotos = await app.PersoonsfotosAsync(evaId);
|
||||
Assert.True(fotos.Waarde!.Count(f => f.IsProfilePhoto) == 1, "er is er precies één profielfoto");
|
||||
Assert.True(fotos.Waarde.Single(f => f.Id == tweede.Waarde).IsProfilePhoto, "en dat is de tweede");
|
||||
|
||||
// De bijlagen bij Eva horen niet in de lijst van een kluisitem thuis en andersom.
|
||||
var itemId = (await app.KluisOpslaanAsync(3, "Bank", new KluisInhoud { Secret = "x" }, 1))
|
||||
.Waarde.GetProperty("itemId").GetInt64();
|
||||
Assert.True((await app.BijlagenAsync(itemId)).Waarde!.Count == 0,
|
||||
"het kluisitem heeft zijn eigen, lege lijst");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: de profielfoto komt mee in de lijst met mensen", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await app.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", null, relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
Familielid Eva() => app.FamilieledenAsync(familieId).GetAwaiter().GetResult()
|
||||
.Waarde!.Single(l => l.PersonId == evaId);
|
||||
|
||||
Assert.True(Eva().ProfielfotoId is null, "zonder foto komt er niets mee");
|
||||
Assert.True(Eva().Profielfotobron is null, "en is er niets te tonen");
|
||||
|
||||
var foto = await app.BestandVersturenAsync("eva.jpg", "image/jpeg",
|
||||
System.Text.Encoding.UTF8.GetBytes("doet alsof het een foto is"),
|
||||
persoonId: evaId);
|
||||
Assert.True(foto.Gelukt, $"een foto opslaan lukt ({foto.Melding})");
|
||||
|
||||
// Nog niet aangewezen: dan hoort hij ook niet bij de lijst te zitten. Anders zou de
|
||||
// eerste de beste bijlage ineens het portret van iemand worden.
|
||||
Assert.True(Eva().ProfielfotoId is null, "een gewone bijlage is nog geen profielfoto");
|
||||
|
||||
Assert.True((await app.ProfielfotoAsync(foto.Waarde)).Gelukt, "aanwijzen lukt");
|
||||
Assert.True(Eva().ProfielfotoId == foto.Waarde, "en nu staat het nummer erbij");
|
||||
|
||||
// De kleine versie blijft hier leeg: in de zelfcontroles draait geen verkleiner, en
|
||||
// "doet alsof het een foto is" is ook geen echte afbeelding. Het scherm hoort daar
|
||||
// niet op te blijven hangen maar gewoon de beginletters te tonen.
|
||||
Assert.True(Eva().Profielfotobron is null, "zonder kleine versie toont het scherm de letters");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een foto bij iemand die zijn profiel zelf beheert kan niet", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var jan = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await jan.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await jan.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", "eva@example.nl", relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
// Piet hoort er helemaal niet bij: hij mag er niet bij, ook niet om te kijken.
|
||||
await using var piet = await proef.AangemeldeAppAsync("piet@example.nl", "Piet", "Pietersen");
|
||||
|
||||
Assert.True(!(await piet.PersoonsfotosAsync(evaId)).Gelukt, "Piet ziet Eva's foto's niet");
|
||||
Assert.True(!(await piet.BestandVersturenAsync("stiekem.jpg", "image/jpeg",
|
||||
System.Text.Encoding.UTF8.GetBytes("nee"), persoonId: evaId)).Gelukt,
|
||||
"en kan er niets bij zetten");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: bijlagen bij een bericht dat nog klaarstaat", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var evaId = (await app.VoegFamilielidToeAsync(familieId, "Eva", "Jansen", null, relatie: 12))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var berichtId = (await app.BerichtKlaarzettenAsync(evaId, "Voor jou", "Lees dit later", null, true))
|
||||
.Waarde.GetProperty("berichtId").GetInt64();
|
||||
|
||||
var bijlage = await app.BestandVersturenAsync("kaartje.jpg", "image/jpeg",
|
||||
System.Text.Encoding.UTF8.GetBytes("een foto bij de brief"),
|
||||
berichtId: berichtId);
|
||||
Assert.True(bijlage.Gelukt, $"een bijlage bij het bericht lukt ({bijlage.Melding})");
|
||||
|
||||
var lijst = await app.BerichtbijlagenAsync(berichtId);
|
||||
Assert.True(lijst.Gelukt, $"de bijlagen komen terug ({lijst.Melding})");
|
||||
Assert.True(lijst.Waarde!.Single().FileName == "kaartje.jpg", "met de juiste naam");
|
||||
|
||||
// Een bijlage hoort bij één ding; dat is geen keuze maar een regel.
|
||||
Assert.True(!(await app.ProfielfotoAsync(bijlage.Waarde)).Gelukt,
|
||||
"een berichtbijlage is geen profielfoto");
|
||||
|
||||
await using var piet = await proef.AangemeldeAppAsync("piet@example.nl", "Piet", "Pietersen");
|
||||
Assert.True(!(await piet.BerichtbijlagenAsync(berichtId)).Gelukt,
|
||||
"een ander ziet de bijlagen van jouw bericht niet");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: bericht klaarzetten voor na het overlijden", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var familieId = (await app.FamiliesAsync()).Waarde![0].Id;
|
||||
var marieId = (await app.VoegFamilielidToeAsync(familieId, "Marie", "Jansen", null, relatie: 1))
|
||||
.Waarde.GetProperty("persoonId").GetInt64();
|
||||
|
||||
var klaargezet = await app.BerichtKlaarzettenAsync(
|
||||
marieId, "Voor jou", "Dag lieve Marie", bezorgOp: null, naOverlijden: true);
|
||||
Assert.True(klaargezet.Gelukt, $"het bericht wordt klaargezet ({klaargezet.Melding})");
|
||||
|
||||
var uitgaand = await app.UitgaandAsync();
|
||||
Assert.True(uitgaand.Gelukt, $"uitgaande post komt terug ({uitgaand.Melding})");
|
||||
var bericht = uitgaand.Waarde!.Single();
|
||||
Assert.True(bericht.Subject == "Voor jou", "met het onderwerp");
|
||||
Assert.True(bericht.ToPersonId == marieId, "en de juiste ontvanger");
|
||||
Assert.True(bericht.DeliverAfterDeath, "gemarkeerd als 'pas na mijn overlijden'");
|
||||
|
||||
var inkomend = await app.InkomendAsync();
|
||||
Assert.True(inkomend.Waarde!.Count == 0, "bij de afzender staat het niet in de inbox");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: stand van de nalatenschap", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
|
||||
|
||||
var stand = await app.NalatenschapStandAsync();
|
||||
Assert.True(stand.Gelukt, $"de stand komt terug ({stand.Melding})");
|
||||
Assert.True(!stand.Waarde!.LooptEenProcedure, "er loopt niets");
|
||||
Assert.True(!stand.Waarde.IsBevestigd, "en er is niets bevestigd");
|
||||
Assert.True(stand.Waarde.RequiredDeclarations == 3, "standaard zijn er drie meldingen nodig");
|
||||
Assert.True(stand.Waarde.Missing == 3, "er ontbreken er dus nog drie");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: sessie hervatten bij een volgende start", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
|
||||
await using (var eerste = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag)) { }
|
||||
|
||||
// Zo start de app de tweede keer: alleen het bewaarde token, geen code.
|
||||
await using (var tweede = proef.App(opslag))
|
||||
{
|
||||
Assert.True(await tweede.HervatSessieAsync(), "met het bewaarde token hoeft niemand opnieuw in te loggen");
|
||||
Assert.True((await tweede.FamiliesAsync()).Gelukt, "en de app kan meteen gegevens ophalen");
|
||||
|
||||
// En hij weet wie hij is. Dit werd alleen gevuld bij het inloggen met een code,
|
||||
// en dan weet de app na een herstart niet welk familielid jijzelf bent: de kaart
|
||||
// bovenaan Mijn mensen viel terug op initialen én je stond een tweede keer in de
|
||||
// lijst, want die lijst filtert op precies dit nummer.
|
||||
Assert.True(tweede.PersoonId != 0, "en weet wie hij is");
|
||||
|
||||
var ik = await tweede.EigenProfielAsync();
|
||||
Assert.True(ik.Gelukt && ik.Waarde!.PersoonId == tweede.PersoonId,
|
||||
"hetzelfde nummer als in het eigen profiel");
|
||||
|
||||
// Het profiel wordt onthouden, zodat elk scherm je naam en je kleine foto heeft
|
||||
// zonder er zelf om te vragen. De kluis gebruikt die foto als tekentje bij
|
||||
// "Alleen voor mijzelf".
|
||||
Assert.True(tweede.MijnProfiel is not null, "het profiel wordt onthouden");
|
||||
|
||||
// De server stelt dit antwoord met de hand samen, omdat de profielfoto er los
|
||||
// bij komt. Vergeet iemand daar een veld, dan is het stil weg; vandaar deze
|
||||
// controle op de velden zelf en niet alleen op het nummer.
|
||||
Assert.True(tweede.MijnProfiel!.Email == "jan@example.nl",
|
||||
"met alle velden erin, niet alleen het nummer");
|
||||
}
|
||||
|
||||
// En zo als het token niet meer geldig is.
|
||||
await opslag.SchrijfAsync("dit-is-geen-geldig-token");
|
||||
await using (var derde = proef.App(opslag))
|
||||
{
|
||||
Assert.True(!await derde.HervatSessieAsync(), "een verlopen token levert geen sessie op");
|
||||
Assert.True(await opslag.LeesAsync() is null, "en wordt opgeruimd in plaats van eeuwig geprobeerd");
|
||||
}
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een sessie die de server niet meer kent wordt meteen opgeruimd", async () =>
|
||||
{
|
||||
// Dit gebeurt na elke herstart van de SocketServer: die bewaart zijn sessies in het
|
||||
// geheugen. Zonder dit bleef de app staan op een rode balk met "opnieuw proberen",
|
||||
// en dat lukt nooit meer - het token bestaat niet meer.
|
||||
await using var proef = new Proef();
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag);
|
||||
|
||||
Assert.True((await app.FamiliesAsync()).Gelukt, "eerst werkt alles gewoon");
|
||||
|
||||
var gemeld = 0;
|
||||
app.SessieVerlopen += () => gemeld++;
|
||||
|
||||
// Alsof de service opnieuw is opgestart: de server is alle sessies kwijt.
|
||||
await proef.Sessies.RemoveAllForAsync(proef.Wereld.Store.Users.Single().Id);
|
||||
|
||||
var uitkomst = await app.FamiliesAsync();
|
||||
Assert.True(!uitkomst.Gelukt, "het verzoek mislukt");
|
||||
Assert.True(uitkomst.Melding!.Contains("opnieuw in"), "met de melding dat je opnieuw moet inloggen");
|
||||
|
||||
Assert.True(gemeld == 1, "de app krijgt het één keer te horen");
|
||||
Assert.True(!app.IsAangemeld, "en beschouwt zichzelf als uitgelogd");
|
||||
Assert.True(await opslag.LeesAsync() is null, "het bewaarde token is weg");
|
||||
|
||||
// Een tweede verzoek meldt het niet nog een keer: één keer naar het inlogscherm is genoeg.
|
||||
await app.FamiliesAsync();
|
||||
Assert.True(gemeld == 1, "en blijft het bij één melding");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: het slot gaat op wat er op het toestel ligt, niet op wat de server zegt", async () =>
|
||||
{
|
||||
// De afspraak: na de eerste keer inloggen met een code kom je erin met je
|
||||
// vingerafdruk, tot je zelf uitlogt. Daarvoor moet de app bij het openen kunnen
|
||||
// weten dat er een sessie op dit toestel ligt - zonder de server iets te vragen.
|
||||
//
|
||||
// Het ging eerder mis omdat het slot pas op de app ging nadat de server het token
|
||||
// had goedgekeurd. Een telefoon die net wakker wordt heeft nog geen netwerk, en na
|
||||
// een herstart van de service kende de server de sessie niet meer. Je kwam dan op
|
||||
// het inlogscherm terecht met een vraag om je e-mailadres, terwijl je telefoon
|
||||
// prima wist wie je was.
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
|
||||
await using (var proef = new Proef())
|
||||
{
|
||||
await using var eerst = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag);
|
||||
}
|
||||
// De server is nu weg - net als een telefoon zonder bereik, of een service die
|
||||
// net opnieuw start.
|
||||
|
||||
await using var app = new MijnBesluitClient("127.0.0.1", VrijePoort(), tls: false, opslag);
|
||||
|
||||
Assert.True(await app.HeeftBewaardeSessieAsync(),
|
||||
"de app weet zonder server dat hier iemand is aangemeld");
|
||||
Assert.True(app.IsAangemeld, "en beschouwt zichzelf als aangemeld");
|
||||
|
||||
// En het omgekeerde: op een toestel waar niets ligt, valt er niets te vergrendelen.
|
||||
await using var vers = new MijnBesluitClient("127.0.0.1", VrijePoort(), tls: false,
|
||||
new GeheugenTokenopslag());
|
||||
Assert.True(!await vers.HeeftBewaardeSessieAsync(),
|
||||
"een schoon toestel gaat gewoon naar het inlogscherm");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: uitloggen wist het bewaarde token", async () =>
|
||||
{
|
||||
await using var proef = new Proef();
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
await using var app = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag);
|
||||
|
||||
await app.UitloggenAsync();
|
||||
|
||||
Assert.True(!app.IsAangemeld, "de app is niet meer aangemeld");
|
||||
Assert.True(await opslag.LeesAsync() is null, "het token is van het toestel verdwenen");
|
||||
Assert.True(!(await app.FamiliesAsync()).Gelukt, "en gegevens ophalen lukt niet meer");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: uitloggen lukt ook als de server onbereikbaar is", async () =>
|
||||
{
|
||||
// Anders blijft iemand met een dood scherm zitten terwijl hij juist weg wil:
|
||||
// de knop wacht dan op een verbinding die er niet is.
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
MijnBesluitClient app;
|
||||
|
||||
await using (var proef = new Proef())
|
||||
{
|
||||
app = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag);
|
||||
Assert.True(!string.IsNullOrEmpty(await opslag.LeesAsync()), "er is een sessie");
|
||||
}
|
||||
// De server is nu weg - net als een telefoon zonder bereik.
|
||||
|
||||
var klaar = app.UitloggenAsync();
|
||||
Assert.True(await Task.WhenAny(klaar, Task.Delay(5000)) == klaar, "uitloggen blijft niet hangen");
|
||||
|
||||
Assert.True(!app.IsAangemeld, "de app is uitgelogd");
|
||||
Assert.True(await opslag.LeesAsync() is null, "en het token staat niet meer op het toestel");
|
||||
await app.DisposeAsync();
|
||||
});
|
||||
|
||||
runner.TestAsync("App: de melding 'geen verbinding' verdwijnt weer zodra het lukt", async () =>
|
||||
{
|
||||
// Precies wat er op een telefoon gebeurt: de app gaat naar de achtergrond terwijl
|
||||
// iemand zijn e-mail leest, Android verbreekt de verbinding, en daarna moet de app
|
||||
// gewoon weer verder kunnen zonder dat de melding blijft staan.
|
||||
var poort = VrijePoort();
|
||||
var standen = new List<bool>();
|
||||
|
||||
await using var app = new MijnBesluitClient("127.0.0.1", poort, tls: false);
|
||||
app.VerbindingVeranderd += nu => standen.Add(nu);
|
||||
|
||||
Assert.True(!(await app.FamiliesAsync()).Gelukt, "zonder server mislukt het verzoek");
|
||||
Assert.True(!app.IsVerbonden, "de app weet dat er geen verbinding is");
|
||||
|
||||
// Nu komt de server (of het netwerk) terug.
|
||||
var wereld = new World();
|
||||
var sessies = new SessionStore(wereld.Clock);
|
||||
var dispatcher = new SocketDispatcher(sessies, work => work(new RequestScope
|
||||
{
|
||||
Accounts = wereld.Accounts, Families = wereld.Families, Vault = wereld.Vault,
|
||||
Messages = wereld.Messages, Legacy = wereld.Legacy, Overzicht = wereld.Overzicht
|
||||
}));
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = poort },
|
||||
dispatcher.HandleAsync);
|
||||
server.Start();
|
||||
|
||||
// Een verzoek zonder sessie wordt geweigerd, maar de verbinding staat er wel weer.
|
||||
await app.FamiliesAsync();
|
||||
Assert.True(app.IsVerbonden, "na een geslaagde verbinding staat de melding weer uit");
|
||||
Assert.True(standen.Contains(false) && standen.Last(), "de app is over beide veranderingen bericht");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: een actie die de server niet kent wordt als zodanig gemeld", async () =>
|
||||
{
|
||||
// Dit is precies het geval "app is nieuwer dan de server". Zonder deze volgorde
|
||||
// krijg je "log opnieuw in" te zien, en zoek je het in de verkeerde hoek.
|
||||
await using var proef = new Proef();
|
||||
await using var rauw = new MijnBesluitSocketClient("127.0.0.1", proef.Server.Port, useTls: false);
|
||||
|
||||
var onbekend = await rauw.SendAsync("vault.ditbestaatniet");
|
||||
Assert.True(onbekend.Status == "Error", "een onbekende actie wordt geweigerd");
|
||||
Assert.True(onbekend.Message!.Contains("Onbekende actie"),
|
||||
$"met de juiste melding, ook zonder sessie ({onbekend.Message})");
|
||||
|
||||
// Een actie die hij wél kent maar waarvoor je moet inloggen, geeft de sessiemelding.
|
||||
var welBekend = await rauw.SendAsync(SocketActions.VaultList);
|
||||
Assert.True(welBekend.Message!.Contains("Log opnieuw in"), "en een bekende actie vraagt om inloggen");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: zonder bereikbare server komt er een melding in plaats van een crash", async () =>
|
||||
{
|
||||
// Een poort waar niets luistert: precies wat er gebeurt in een lift of trein.
|
||||
await using var app = new MijnBesluitClient("127.0.0.1", VrijePoort(), tls: false);
|
||||
|
||||
var uitkomst = await app.FamiliesAsync();
|
||||
Assert.True(!uitkomst.Gelukt, "de aanroep mislukt netjes");
|
||||
Assert.True(!string.IsNullOrWhiteSpace(uitkomst.Melding), "met een melding die op het scherm kan");
|
||||
Assert.True(!await app.HervatSessieAsync(), "sessie hervatten loopt ook niet stuk");
|
||||
});
|
||||
|
||||
runner.TestAsync("App: geen verbinding gooit het bewaarde token niet weg", async () =>
|
||||
{
|
||||
// Het verschil tussen "de server zegt nee" en "ik kan de server niet bereiken".
|
||||
// Alleen het eerste is een reden om iemand opnieuw te laten inloggen.
|
||||
//
|
||||
// Ging dit mis, dan moest je een code per e-mail aanvragen terwijl er niets mis was
|
||||
// met je sessie - en juist op het vervelendste moment: een app die op slot stond is
|
||||
// vaak door Android afgesloten, dus start hij koud op, en dan is het netwerk van de
|
||||
// telefoon nog niet wakker.
|
||||
var opslag = new GeheugenTokenopslag();
|
||||
|
||||
// Eerst een echte sessie, zodat er een geldig token op het toestel staat.
|
||||
string token;
|
||||
await using (var proef = new Proef())
|
||||
{
|
||||
await using var eerste = await proef.AangemeldeAppAsync("jan@example.nl", opslag: opslag);
|
||||
token = (await opslag.LeesAsync())!;
|
||||
Assert.True(!string.IsNullOrEmpty(token), "er staat een token op het toestel");
|
||||
}
|
||||
|
||||
// En nu starten terwijl de server onbereikbaar is.
|
||||
await using (var koudeStart = new MijnBesluitClient("127.0.0.1", VrijePoort(), tls: false, opslag: opslag))
|
||||
{
|
||||
Assert.True(!await koudeStart.HervatSessieAsync(), "de sessie komt er niet doorheen");
|
||||
Assert.True(await opslag.LeesAsync() == token,
|
||||
"maar het token blijft staan: dit is geen reden om opnieuw te laten inloggen");
|
||||
}
|
||||
|
||||
// Zegt de server wél dat hij de sessie niet kent, dan hoort het token juist weg.
|
||||
await using (var proef = new Proef())
|
||||
{
|
||||
await opslag.SchrijfAsync("dit-is-geen-geldig-token");
|
||||
await using var app = proef.App(opslag);
|
||||
|
||||
Assert.True(!await app.HervatSessieAsync(), "een geweigerde sessie levert niets op");
|
||||
Assert.True(await opslag.LeesAsync() is null, "en dan wordt het token wel opgeruimd");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Een poortnummer waar gegarandeerd niets luistert.</summary>
|
||||
private static int VrijePoort()
|
||||
{
|
||||
var luisteraar = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
|
||||
luisteraar.Start();
|
||||
var poort = ((IPEndPoint)luisteraar.LocalEndpoint).Port;
|
||||
luisteraar.Stop();
|
||||
return poort;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using MijnBesluit.Protocol;
|
||||
using MijnBesluit.Server;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Loopt de hele keten door: app -> socket -> dispatcher -> Application-laag -> opslag en terug.
|
||||
/// Alleen de database is hier vervangen door opslag in het geheugen.
|
||||
/// </summary>
|
||||
public static class EndToEndChecks
|
||||
{
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("Van app tot kluis: registreren, familie, uitnodigen, bericht", async () =>
|
||||
{
|
||||
var wereld = new World();
|
||||
var sessies = new SessionStore(wereld.Clock);
|
||||
var dispatcher = new SocketDispatcher(sessies, work => work(new RequestScope
|
||||
{
|
||||
Accounts = wereld.Accounts,
|
||||
Families = wereld.Families,
|
||||
Vault = wereld.Vault,
|
||||
Messages = wereld.Messages,
|
||||
Legacy = wereld.Legacy,
|
||||
Overzicht = wereld.Overzicht,
|
||||
Samenvoegen = wereld.Samenvoegen
|
||||
}));
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
dispatcher.HandleAsync);
|
||||
server.Start();
|
||||
|
||||
await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
|
||||
// Zonder token mag er niets.
|
||||
var zonderToken = await app.SendAsync(SocketActions.VaultList);
|
||||
Assert.True(zonderToken.Status == "Error" && zonderToken.Message!.Contains("Log opnieuw in"),
|
||||
"zonder geldige sessie wordt een verzoek geweigerd");
|
||||
|
||||
// Registreren gaat in twee stappen: code aanvragen, code invullen.
|
||||
var codeAanvraag = await app.SendAsync(SocketActions.RequestRegisterCode, new
|
||||
{
|
||||
email = "jan@example.nl",
|
||||
firstName = "Jan",
|
||||
lastName = "Jansen"
|
||||
});
|
||||
Assert.True(codeAanvraag.Status == "Ok", "de registratiecode wordt verstuurd");
|
||||
Assert.True(!JsonSerializer.Serialize(codeAanvraag.Payload, SocketFraming.Json).Contains("jan@example.nl"),
|
||||
"het antwoord toont het adres alleen afgeschermd");
|
||||
|
||||
var registratie = await app.SendAsync(SocketActions.Register, new
|
||||
{
|
||||
email = "jan@example.nl",
|
||||
code = wereld.LaatsteCodeUitMail("jan@example.nl")
|
||||
});
|
||||
Assert.True(registratie.Status == "Ok", "registreren lukt met de code uit de e-mail");
|
||||
app.Token = Field(registratie, "token");
|
||||
Assert.True(!string.IsNullOrEmpty(app.Token), "er komt een token terug");
|
||||
|
||||
// Kluisitem opslaan en weer openen, helemaal via de socket.
|
||||
var opslaan = await app.SendAsync(SocketActions.VaultSave, new
|
||||
{
|
||||
category = 3, // Wachtwoord
|
||||
title = "Bank",
|
||||
payload = new { website = "bank.nl", username = "jan", secret = "zeergeheim" },
|
||||
releasePolicy = 1, // Na overlijden
|
||||
releaseAt = (DateTime?)null
|
||||
});
|
||||
Assert.True(opslaan.Status == "Ok", "kluisitem opslaan lukt");
|
||||
|
||||
var lijst = await app.SendAsync(SocketActions.VaultList);
|
||||
Assert.True(lijst.Status == "Ok", "de kluislijst komt terug");
|
||||
Assert.True(!JsonSerializer.Serialize(lijst.Payload, SocketFraming.Json).Contains("zeergeheim"),
|
||||
"de lijst bevat geen geheimen, alleen titels");
|
||||
|
||||
var itemId = long.Parse(Field(opslaan, "itemId")!);
|
||||
var openen = await app.SendAsync(SocketActions.VaultOpen, new { id = itemId });
|
||||
Assert.True(JsonSerializer.Serialize(openen.Payload, SocketFraming.Json).Contains("zeergeheim"),
|
||||
"openen geeft de inhoud terug aan de eigenaar");
|
||||
|
||||
// Familie uitbreiden en uitnodigen.
|
||||
var familie = wereld.Store.Families.Single();
|
||||
var toevoegen = await app.SendAsync(SocketActions.AddMember, new
|
||||
{
|
||||
familyId = familie.Id,
|
||||
firstName = "Eva",
|
||||
lastName = "Jansen",
|
||||
email = "eva@example.nl",
|
||||
relation = 2, // Kind
|
||||
relationLabel = (string?)null
|
||||
});
|
||||
Assert.True(toevoegen.Status == "Ok", "familielid toevoegen lukt");
|
||||
var evaId = long.Parse(Field(toevoegen, "persoonId")!);
|
||||
|
||||
var uitnodigen = await app.SendAsync(SocketActions.Invite, new { familyId = familie.Id, personId = evaId });
|
||||
Assert.True(uitnodigen.Status == "Ok", "uitnodigen lukt");
|
||||
Assert.True(!JsonSerializer.Serialize(uitnodigen.Payload, SocketFraming.Json)
|
||||
.Contains(wereld.Store.Invitations.Single().Email),
|
||||
"het antwoord bevat geen uitnodigingstoken");
|
||||
|
||||
// Eva neemt de uitnodiging aan vanaf haar eigen toestel.
|
||||
var mail = wereld.Email.Sent.Last(m => m.To == "eva@example.nl");
|
||||
var token = mail.Body.Split("/uitnodiging/")[1].Split('"')[0];
|
||||
|
||||
await using var appVanEva = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
var accepteren = await appVanEva.SendAsync(SocketActions.AcceptInvitation, new { token });
|
||||
Assert.True(accepteren.Status == "Ok", "Eva kan de uitnodiging aannemen via de app");
|
||||
appVanEva.Token = Field(accepteren, "token");
|
||||
|
||||
// Jan ziet nu dat het profiel geclaimd is.
|
||||
var leden = await app.SendAsync(SocketActions.ListMembers, new { familyId = familie.Id });
|
||||
var ledenJson = JsonSerializer.Serialize(leden.Payload, SocketFraming.Json);
|
||||
Assert.True(ledenJson.Contains("\"state\":2"), "Jan ziet het profiel van Eva als geclaimd");
|
||||
|
||||
// Jan zet een bericht klaar voor Eva, te bezorgen na zijn overlijden.
|
||||
var bericht = await app.SendAsync(SocketActions.MessageCompose, new
|
||||
{
|
||||
toPersonId = evaId,
|
||||
subject = "Voor later",
|
||||
text = "Ik ben trots op je.",
|
||||
deliverAt = (DateTime?)null,
|
||||
afterDeath = true,
|
||||
kind = 0,
|
||||
vaultFileId = (long?)null
|
||||
});
|
||||
Assert.True(bericht.Status == "Ok", "bericht klaarzetten lukt");
|
||||
|
||||
var inboxVanEva = await appVanEva.SendAsync(SocketActions.Inbox);
|
||||
Assert.True(JsonSerializer.Serialize(inboxVanEva.Payload, SocketFraming.Json) == "[]",
|
||||
"Eva ziet het bericht nog niet");
|
||||
|
||||
// Uitloggen maakt het token onbruikbaar.
|
||||
await app.SendAsync(SocketActions.SignOut);
|
||||
var naUitloggen = await app.SendAsync(SocketActions.VaultList);
|
||||
Assert.True(naUitloggen.Status == "Error", "na uitloggen werkt het token niet meer");
|
||||
});
|
||||
|
||||
runner.TestAsync("Verlopen sessie wordt geweigerd", async () =>
|
||||
{
|
||||
var wereld = new World();
|
||||
var sessies = new SessionStore(wereld.Clock, TimeSpan.FromHours(1));
|
||||
var dispatcher = new SocketDispatcher(sessies, work => work(new RequestScope
|
||||
{
|
||||
Accounts = wereld.Accounts, Families = wereld.Families, Vault = wereld.Vault,
|
||||
Messages = wereld.Messages, Legacy = wereld.Legacy, Overzicht = wereld.Overzicht
|
||||
}));
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 }, dispatcher.HandleAsync);
|
||||
server.Start();
|
||||
|
||||
await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
await app.SendAsync(SocketActions.RequestRegisterCode,
|
||||
new { email = "a@example.nl", firstName = "A", lastName = "A" });
|
||||
var reg = await app.SendAsync(SocketActions.Register,
|
||||
new { email = "a@example.nl", code = wereld.LaatsteCodeUitMail("a@example.nl") });
|
||||
app.Token = Field(reg, "token");
|
||||
Assert.True((await app.SendAsync(SocketActions.VaultList)).Status == "Ok", "meteen na inloggen werkt het");
|
||||
|
||||
wereld.Clock.Advance(TimeSpan.FromHours(2));
|
||||
Assert.True((await app.SendAsync(SocketActions.VaultList)).Status == "Error", "na twee uur is de sessie verlopen");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Haalt één veld uit de payload van een antwoord.</summary>
|
||||
private static string? Field(SocketResponse response, string name)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(response.Payload, SocketFraming.Json);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.TryGetProperty(name, out var value)
|
||||
? (value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString())
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
using MijnBesluit.Application.Abstractions;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>Eenvoudige opslag in het geheugen, zodat de regels zonder database te controleren zijn.</summary>
|
||||
public class Store
|
||||
{
|
||||
public List<User> Users = new();
|
||||
public List<Person> People = new();
|
||||
public List<Family> Families = new();
|
||||
public List<FamilyMember> Members = new();
|
||||
public List<Invitation> Invitations = new();
|
||||
public List<VaultItem> VaultItems = new();
|
||||
public List<VaultFile> VaultFiles = new();
|
||||
public List<ContentGrant> Grants = new();
|
||||
public List<Message> Messages = new();
|
||||
public List<DeathDeclaration> Declarations = new();
|
||||
public List<LegacyManager> Managers = new();
|
||||
public List<AuditLogEntry> Audit = new();
|
||||
public List<LoginCode> Codes = new();
|
||||
public List<Toestel> Toestellen = new();
|
||||
public List<Samenvoeging> Samenvoegingen = new();
|
||||
public List<Adverteerder> Adverteerders = new();
|
||||
public List<Campagne> Campagnes = new();
|
||||
public List<CampagneUur> CampagneUren = new();
|
||||
|
||||
private long _seq;
|
||||
public long NextId() => ++_seq;
|
||||
}
|
||||
|
||||
public class TestClock : IClock
|
||||
{
|
||||
public DateTime UtcNow { get; set; } = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
public void Advance(TimeSpan by) => UtcNow = UtcNow.Add(by);
|
||||
}
|
||||
|
||||
/// <summary>Mastersleutel in het geheugen. In productie komt deze uit een sleutelbestand of sleutelkluis.</summary>
|
||||
/// <summary>
|
||||
/// Een verkleiner die je per test aanzet. Standaard uit, zodat de andere controles blijven
|
||||
/// werken zoals ze waren: zonder verkleiner is er geen kleine versie en wordt er niets omgezet,
|
||||
/// precies zoals op een server waar het verkleinen niet beschikbaar is.
|
||||
/// </summary>
|
||||
public class TestVerkleiner : IAfbeeldingVerkleiner
|
||||
{
|
||||
public bool Aan { get; set; }
|
||||
|
||||
/// <summary>Wat er teruggegeven wordt als hij aanstaat. Klein, zodat het verschil duidelijk is.</summary>
|
||||
public byte[] Uitkomst { get; set; } = { 1, 2, 3, 4, 5, 6, 7, 8 };
|
||||
|
||||
/// <summary>Waarmee hij is aangeroepen; zo kan een test nakijken of de maat klopt.</summary>
|
||||
public List<(string Soort, int MaxZijde)> Aanroepen { get; } = new();
|
||||
|
||||
public byte[]? Verklein(byte[] inhoud, string soort, int maxZijde)
|
||||
{
|
||||
if (!Aan) return null;
|
||||
if (!soort.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) return null;
|
||||
|
||||
Aanroepen.Add((soort, maxZijde));
|
||||
return Uitkomst;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestKeyProtector : IMasterKeyProtector
|
||||
{
|
||||
private readonly byte[] _master = MijnBesluit.Application.Security.EnvelopeCrypto.NewKey();
|
||||
public int CurrentKeyVersion => 1;
|
||||
public byte[] Protect(byte[] dataKey) => MijnBesluit.Application.Security.EnvelopeCrypto.WrapKey(dataKey, _master);
|
||||
public byte[] Unprotect(byte[] p) => MijnBesluit.Application.Security.EnvelopeCrypto.UnwrapKey(p, _master);
|
||||
}
|
||||
|
||||
public class TestEmail : IEmailSender
|
||||
{
|
||||
public List<(string To, string Subject, string Body)> Sent = new();
|
||||
|
||||
/// <summary>Zet dit aan om te doen alsof de mailserver onbereikbaar is.</summary>
|
||||
public bool Mislukt { get; set; }
|
||||
|
||||
public Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
|
||||
{
|
||||
if (Mislukt) throw new InvalidOperationException("Failure sending mail.");
|
||||
Sent.Add((to, subject, body));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFiles : IFileStore
|
||||
{
|
||||
private readonly Dictionary<string, byte[]> _files = new();
|
||||
public Task<string> WriteAsync(byte[] bytes, string name, CancellationToken ct = default)
|
||||
{ var key = Guid.NewGuid().ToString("N"); _files[key] = bytes; return Task.FromResult(key); }
|
||||
public Task<byte[]> ReadAsync(string key, CancellationToken ct = default) => Task.FromResult(_files[key]);
|
||||
public Task DeleteAsync(string key, CancellationToken ct = default) { _files.Remove(key); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
public class Uow : IUnitOfWork
|
||||
{
|
||||
private readonly Store _s;
|
||||
public Uow(Store s) => _s = s;
|
||||
|
||||
/// <summary>Deelt id's uit aan nieuwe records, net zoals de database dat bij opslaan doet.</summary>
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default)
|
||||
{
|
||||
foreach (var p in _s.People.Where(x => x.Id == 0)) p.Id = _s.NextId();
|
||||
foreach (var u in _s.Users.Where(x => x.Id == 0))
|
||||
{
|
||||
u.Id = _s.NextId();
|
||||
if (u.PersonId == 0 && u.Person is not null) u.PersonId = u.Person.Id;
|
||||
}
|
||||
foreach (var f in _s.Families.Where(x => x.Id == 0)) f.Id = _s.NextId();
|
||||
foreach (var m in _s.Members.Where(x => x.Id == 0)) m.Id = _s.NextId();
|
||||
foreach (var i in _s.Invitations.Where(x => x.Id == 0)) i.Id = _s.NextId();
|
||||
foreach (var v in _s.VaultItems.Where(x => x.Id == 0)) v.Id = _s.NextId();
|
||||
foreach (var v in _s.VaultFiles.Where(x => x.Id == 0)) v.Id = _s.NextId();
|
||||
foreach (var g in _s.Grants.Where(x => x.Id == 0)) g.Id = _s.NextId();
|
||||
foreach (var m in _s.Messages.Where(x => x.Id == 0)) m.Id = _s.NextId();
|
||||
foreach (var d in _s.Declarations.Where(x => x.Id == 0)) d.Id = _s.NextId();
|
||||
foreach (var m in _s.Managers.Where(x => x.Id == 0)) m.Id = _s.NextId();
|
||||
foreach (var a in _s.Audit.Where(x => x.Id == 0)) a.Id = _s.NextId();
|
||||
foreach (var c in _s.Codes.Where(x => x.Id == 0)) c.Id = _s.NextId();
|
||||
foreach (var v in _s.Samenvoegingen.Where(x => x.Id == 0)) v.Id = _s.NextId();
|
||||
foreach (var a in _s.Adverteerders.Where(x => x.Id == 0)) a.Id = _s.NextId();
|
||||
foreach (var c in _s.Campagnes.Where(x => x.Id == 0)) c.Id = _s.NextId();
|
||||
foreach (var u in _s.CampagneUren.Where(x => x.Id == 0)) u.Id = _s.NextId();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class UserRepo : IUserRepository
|
||||
{
|
||||
private readonly Store _s; public UserRepo(Store s) => _s = s;
|
||||
|
||||
/// <summary>
|
||||
/// Vult de persoon in, net als de echte opslag met Include doet. Zonder dit gedraagt de
|
||||
/// zelftest zich anders dan de server en merk je een ontbrekende Include hier niet.
|
||||
/// </summary>
|
||||
private User? MetPersoon(User? u)
|
||||
{
|
||||
if (u is not null) u.Person ??= _s.People.FirstOrDefault(p => p.Id == u.PersonId);
|
||||
return u;
|
||||
}
|
||||
|
||||
public Task<User?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.Id == id)));
|
||||
public Task<User?> FindByEmailAsync(string e, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.Email == e)));
|
||||
public Task<User?> FindByPersonIdAsync(long pid, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.PersonId == pid)));
|
||||
public Task<bool> EmailExistsAsync(string e, CancellationToken ct = default) => Task.FromResult(_s.Users.Any(u => u.Email == e));
|
||||
public Task<IReadOnlyList<User>> ListBeheerdersAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<User>>(_s.Users.Where(u => u.IsBeheerder).OrderBy(u => u.Id).ToList());
|
||||
public void Add(User u) => _s.Users.Add(u);
|
||||
public void Remove(User u) => _s.Users.Remove(u);
|
||||
}
|
||||
|
||||
public class LoginCodeRepo : ILoginCodeRepository
|
||||
{
|
||||
private readonly Store _s; public LoginCodeRepo(Store s) => _s = s;
|
||||
|
||||
public Task<LoginCode?> FindOpenAsync(string email, LoginCodePurpose doel, DateTime nu, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Codes.Where(c => c.Email == email && c.Purpose == doel && c.IsOpen(nu))
|
||||
.OrderByDescending(c => c.CreatedAt).FirstOrDefault());
|
||||
|
||||
public Task<IReadOnlyList<LoginCode>> ListOpenAsync(string email, LoginCodePurpose doel, DateTime nu, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<LoginCode>>(
|
||||
_s.Codes.Where(c => c.Email == email && c.Purpose == doel && c.IsOpen(nu)).ToList());
|
||||
|
||||
public Task<int> CountSinceAsync(string email, DateTime sinds, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Codes.Count(c => c.Email == email && c.CreatedAt >= sinds && c.SignedInAt is null));
|
||||
|
||||
public Task<int> RemoveExpiredAsync(DateTime ouderDan, CancellationToken ct = default)
|
||||
{
|
||||
var aantal = _s.Codes.RemoveAll(c => c.ExpiresAt < ouderDan);
|
||||
return Task.FromResult(aantal);
|
||||
}
|
||||
|
||||
public Task<int> RemoveForEmailAsync(string email, CancellationToken ct = default)
|
||||
{
|
||||
var aantal = _s.Codes.RemoveAll(c => c.Email == email);
|
||||
return Task.FromResult(aantal);
|
||||
}
|
||||
|
||||
public void Add(LoginCode code) => _s.Codes.Add(code);
|
||||
}
|
||||
|
||||
public class PersonRepo : IPersonRepository
|
||||
{
|
||||
private readonly Store _s; public PersonRepo(Store s) => _s = s;
|
||||
public Task<Person?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.People.FirstOrDefault(p => p.Id == id));
|
||||
// Dezelfde volgorde als in de database: geclaimde profielen eerst. Er kunnen er meerdere
|
||||
// zijn met hetzelfde adres; zie docs/Identiteit.md.
|
||||
public Task<Person?> FindByEmailAsync(string e, CancellationToken ct = default) =>
|
||||
Task.FromResult(_s.People.Where(p => p.Email == e)
|
||||
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id)
|
||||
.FirstOrDefault());
|
||||
|
||||
public Task<IReadOnlyList<Person>> ListByEmailAsync(string e, CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<Person>>(
|
||||
_s.People.Where(p => p.Email == e)
|
||||
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id).ToList());
|
||||
public Task<IReadOnlyList<Person>> ListAantekeningenOverAdresAsync(string e, long gebruikerId,
|
||||
CancellationToken ct = default) =>
|
||||
Task.FromResult<IReadOnlyList<Person>>(
|
||||
_s.People.Where(p => p.Email == e
|
||||
&& p.ClaimedByUserId == null
|
||||
&& p.MergedIntoPersonId == null
|
||||
&& p.ManagedByUserId != null
|
||||
&& p.ManagedByUserId != gebruikerId
|
||||
&& p.LinkDeclinedByUserId != gebruikerId)
|
||||
.OrderBy(p => p.CreatedAt).ToList());
|
||||
public Task<IReadOnlyList<Person>> ListByIdsAsync(IEnumerable<long> ids, CancellationToken ct = default)
|
||||
{ var set = ids.ToHashSet(); return Task.FromResult<IReadOnlyList<Person>>(_s.People.Where(p => set.Contains(p.Id)).ToList()); }
|
||||
public Task<Person?> FindByObjectionTokenAsync(byte[] hash, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.People.FirstOrDefault(
|
||||
p => p.ObjectionTokenHash is not null && p.ObjectionTokenHash.SequenceEqual(hash)));
|
||||
public void Add(Person p) => _s.People.Add(p);
|
||||
public void Remove(Person p) => _s.People.Remove(p);
|
||||
}
|
||||
|
||||
public class FamilyRepo : IFamilyRepository
|
||||
{
|
||||
private readonly Store _s; public FamilyRepo(Store s) => _s = s;
|
||||
public Task<Family?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Families.FirstOrDefault(f => f.Id == id));
|
||||
public Task<IReadOnlyList<Family>> ListOwnedByPersonAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => f.OwnerPersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<FamilyMember>> ListMembersAsync(long fid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<FamilyMember>>(_s.Members.Where(m => m.FamilyId == fid).ToList());
|
||||
public Task<FamilyMember?> FindMemberAsync(long fid, long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Members.FirstOrDefault(m => m.FamilyId == fid && m.PersonId == pid));
|
||||
public Task<IReadOnlyList<Family>> ListFamiliesContainingAsync(long pid, CancellationToken ct = default)
|
||||
{ var ids = _s.Members.Where(m => m.PersonId == pid).Select(m => m.FamilyId).ToHashSet();
|
||||
return Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => ids.Contains(f.Id)).ToList()); }
|
||||
public Task<IReadOnlyList<long>> ListCircleAsync(long pid, CancellationToken ct = default)
|
||||
{
|
||||
var families = _s.Members.Where(m => m.PersonId == pid).Select(m => m.FamilyId)
|
||||
.Concat(_s.Families.Where(f => f.OwnerPersonId == pid).Select(f => f.Id))
|
||||
.ToHashSet();
|
||||
|
||||
var kring = _s.Members.Where(m => families.Contains(m.FamilyId)).Select(m => m.PersonId).ToHashSet();
|
||||
kring.Add(pid);
|
||||
return Task.FromResult<IReadOnlyList<long>>(kring.ToList());
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<FamilyMember>> ListMembershipsOfPersonAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<FamilyMember>>(_s.Members.Where(m => m.PersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<Family>> ListOwnedAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => f.OwnerPersonId == pid).ToList());
|
||||
public void Add(Family f) => _s.Families.Add(f);
|
||||
public void Add(FamilyMember m) => _s.Members.Add(m);
|
||||
public void Remove(FamilyMember m) => _s.Members.Remove(m);
|
||||
public void Remove(Family f) => _s.Families.Remove(f);
|
||||
}
|
||||
|
||||
public class InvitationRepo : IInvitationRepository
|
||||
{
|
||||
private readonly Store _s; public InvitationRepo(Store s) => _s = s;
|
||||
public Task<Invitation?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Invitations.FirstOrDefault(i => i.Id == id));
|
||||
public Task<Invitation?> FindByTokenHashAsync(byte[] h, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Invitations.FirstOrDefault(i => i.TokenHash.SequenceEqual(h)));
|
||||
public Task<IReadOnlyList<Invitation>> ListOpenForPersonAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.PersonId == pid).ToList());
|
||||
public Task<bool> HasOpenInvitationAsync(long pid, DateTime now, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Invitations.Any(i => i.PersonId == pid && i.IsOpen(now)));
|
||||
public Task<IReadOnlyList<Invitation>> ListLatestForPeopleAsync(IEnumerable<long> personIds, CancellationToken ct = default)
|
||||
{
|
||||
var ids = personIds.Distinct().ToHashSet();
|
||||
return Task.FromResult<IReadOnlyList<Invitation>>(
|
||||
_s.Invitations.Where(i => ids.Contains(i.PersonId))
|
||||
.GroupBy(i => i.PersonId)
|
||||
.Select(g => g.OrderByDescending(i => i.SentAt ?? i.CreatedAt).First())
|
||||
.ToList());
|
||||
}
|
||||
public Task<IReadOnlyList<Invitation>> ListAllForPersonAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.PersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<Invitation>> ListSentByUserAsync(long userId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.CreatedByUserId == userId).ToList());
|
||||
|
||||
public void Add(Invitation i) => _s.Invitations.Add(i);
|
||||
public void Remove(Invitation i) => _s.Invitations.Remove(i);
|
||||
}
|
||||
|
||||
public class VaultRepo : IVaultRepository
|
||||
{
|
||||
private readonly Store _s; public VaultRepo(Store s) => _s = s;
|
||||
public Task<VaultItem?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.VaultItems.FirstOrDefault(v => v.Id == id));
|
||||
public Task<IReadOnlyList<VaultItem>> ListForPersonAsync(long pid, VaultCategory? cat = null, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<VaultItem>>(_s.VaultItems.Where(v => v.OwnerPersonId == pid && v.DeletedAt == null
|
||||
&& (cat == null || v.Category == cat)).ToList());
|
||||
public Task<VaultFile?> FindFileAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.VaultFiles.FirstOrDefault(f => f.Id == id));
|
||||
public Task<IReadOnlyList<VaultFile>> ListFilesForItemAsync(long itemId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
|
||||
.Where(f => f.VaultItemId == itemId && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
|
||||
public Task<IReadOnlyList<VaultFile>> ListFilesForPersonAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
|
||||
.Where(f => f.SubjectPersonId == pid && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
|
||||
public Task<IReadOnlyList<VaultFile>> ListFilesForMessageAsync(long mid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
|
||||
.Where(f => f.MessageId == mid && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
|
||||
public Task<IReadOnlyList<VaultFile>> ListProfilePhotosAsync(IEnumerable<long> pids, CancellationToken ct = default)
|
||||
{
|
||||
var ids = pids.Distinct().ToHashSet();
|
||||
return Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
|
||||
.Where(f => f.SubjectPersonId is { } p && ids.Contains(p) && f.IsProfilePhoto && f.DeletedAt == null)
|
||||
.ToList());
|
||||
}
|
||||
public Task<ContentGrant?> FindGrantAsync(string type, long rid, long uid, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Grants.FirstOrDefault(g => g.ResourceType == type && g.ResourceId == rid && g.UserId == uid));
|
||||
public Task<IReadOnlyList<VaultItem>> ListSharedWithAsync(long userId, CancellationToken ct = default)
|
||||
{
|
||||
var ids = _s.Grants.Where(g => g.ResourceType == nameof(VaultItem) && g.UserId == userId && g.RevokedAt == null)
|
||||
.Select(g => g.ResourceId).ToHashSet();
|
||||
return Task.FromResult<IReadOnlyList<VaultItem>>(
|
||||
_s.VaultItems.Where(v => ids.Contains(v.Id) && v.DeletedAt == null).ToList());
|
||||
}
|
||||
public Task<IReadOnlyList<VaultFile>> ListFilesOwnedByAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles.Where(f => f.OwnerPersonId == pid).ToList());
|
||||
public void Add(VaultItem i) => _s.VaultItems.Add(i);
|
||||
public void Add(VaultFile f) => _s.VaultFiles.Add(f);
|
||||
public void Add(ContentGrant g) => _s.Grants.Add(g);
|
||||
public void Remove(VaultItem i) => _s.VaultItems.Remove(i);
|
||||
public void Remove(VaultFile f) => _s.VaultFiles.Remove(f);
|
||||
|
||||
public Task RemoveGrantsForUserAsync(long userId, CancellationToken ct = default)
|
||||
{
|
||||
_s.Grants.RemoveAll(g => g.UserId == userId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public Task<int> RevokeGrantsForOwnerAsync(long ownerPersonId, string reason, DateTime nowUtc,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var items = _s.VaultItems.Where(i => i.OwnerPersonId == ownerPersonId).Select(i => i.Id).ToHashSet();
|
||||
var verleend = _s.Grants.Where(g => g.ResourceType == nameof(VaultItem) && items.Contains(g.ResourceId)
|
||||
&& g.Reason == reason && g.RevokedAt == null).ToList();
|
||||
foreach (var t in verleend) t.RevokedAt = nowUtc;
|
||||
return Task.FromResult(verleend.Count);
|
||||
}
|
||||
|
||||
public Task RemoveGrantsAsync(long itemId, CancellationToken ct = default)
|
||||
{
|
||||
_s.Grants.RemoveAll(g => g.ResourceType == nameof(VaultItem) && g.ResourceId == itemId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageRepo : IMessageRepository
|
||||
{
|
||||
private readonly Store _s; public MessageRepo(Store s) => _s = s;
|
||||
public Task<Message?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Messages.FirstOrDefault(m => m.Id == id));
|
||||
public Task<IReadOnlyList<Message>> ListOutboxAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.FromPersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<Message>> ListInboxAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.ToPersonId == pid && m.Status >= MessageStatus.Bezorgd).ToList());
|
||||
public Task<IReadOnlyList<Message>> ListDueAsync(DateTime now, int max, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.Status == MessageStatus.Gepland).Take(max).ToList());
|
||||
public Task<IReadOnlyList<Message>> ListPlannedForAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages
|
||||
.Where(m => m.ToPersonId == pid && m.Status == MessageStatus.Gepland).ToList());
|
||||
public Task<IReadOnlyList<Message>> ListAwaitingDeathOfAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.FromPersonId == pid && m.DeliverAfterDeath).ToList());
|
||||
public Task<IReadOnlyList<Message>> ListAllTouchingAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Message>>(
|
||||
_s.Messages.Where(m => m.FromPersonId == pid || m.ToPersonId == pid).ToList());
|
||||
public Task<Message?> FindByViewTokenHashAsync(byte[] tokenHash, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Messages.FirstOrDefault(m => m.ViewTokenHash is not null && m.ViewTokenHash.SequenceEqual(tokenHash)));
|
||||
public void Add(Message m) => _s.Messages.Add(m);
|
||||
public void Remove(Message m) => _s.Messages.Remove(m);
|
||||
}
|
||||
|
||||
public class LegacyRepo : ILegacyRepository
|
||||
{
|
||||
private readonly Store _s; public LegacyRepo(Store s) => _s = s;
|
||||
public Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(_s.Declarations.Where(d => d.PersonId == pid).ToList());
|
||||
public Task<DeathDeclaration?> FindDeclarationByUserAsync(long pid, long uid, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Declarations.FirstOrDefault(d => d.PersonId == pid && d.DeclaredByUserId == uid));
|
||||
public Task<IReadOnlyList<LegacyManager>> ListManagersForAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<LegacyManager>>(_s.Managers.Where(m => m.PersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<LegacyManager>> ListAssignmentsOfAsync(long mpid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<LegacyManager>>(_s.Managers.Where(m => m.ManagerPersonId == mpid).ToList());
|
||||
public Task<IReadOnlyList<DeathDeclaration>> ListAllDeclarationsAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(_s.Declarations.Where(d => d.PersonId == pid).ToList());
|
||||
public Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsByUserAsync(long userId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(
|
||||
_s.Declarations.Where(d => d.DeclaredByUserId == userId).ToList());
|
||||
|
||||
public void Remove(DeathDeclaration d) => _s.Declarations.Remove(d);
|
||||
public void Add(DeathDeclaration d) => _s.Declarations.Add(d);
|
||||
public void Add(LegacyManager m) => _s.Managers.Add(m);
|
||||
public void Remove(LegacyManager m) => _s.Managers.Remove(m);
|
||||
}
|
||||
|
||||
public class SamenvoegRepo : ISamenvoegRepository
|
||||
{
|
||||
private readonly Store _s; public SamenvoegRepo(Store s) => _s = s;
|
||||
public Task<Samenvoeging?> FindByIdAsync(long id, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Samenvoegingen.FirstOrDefault(x => x.Id == id));
|
||||
public Task<IReadOnlyList<Samenvoeging>> ListVoorDuplicaatAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Samenvoeging>>(
|
||||
_s.Samenvoegingen.Where(x => x.DuplicaatPersonId == pid)
|
||||
.OrderByDescending(x => x.SamengevoegdOp).ToList());
|
||||
public Task<IReadOnlyList<Samenvoeging>> ListVoorDoelAsync(long pid, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Samenvoeging>>(
|
||||
_s.Samenvoegingen.Where(x => x.DoelPersonId == pid)
|
||||
.OrderByDescending(x => x.SamengevoegdOp).ToList());
|
||||
public void Add(Samenvoeging s) => _s.Samenvoegingen.Add(s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adverteerders, campagnes en tellers. Vult de adverteerder in bij een campagne, net als de
|
||||
/// echte opslag met Include doet - zonder dat gedraagt de zelftest zich anders dan de server.
|
||||
/// </summary>
|
||||
public class AdvertentieRepo : IAdvertentieRepository
|
||||
{
|
||||
private readonly Store _s; public AdvertentieRepo(Store s) => _s = s;
|
||||
|
||||
private Campagne? Met(Campagne? c)
|
||||
{
|
||||
if (c is not null) c.Adverteerder ??= _s.Adverteerders.FirstOrDefault(a => a.Id == c.AdverteerderId);
|
||||
return c;
|
||||
}
|
||||
|
||||
public Task<Adverteerder?> FindAdverteerderAsync(long id, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Adverteerders.FirstOrDefault(a => a.Id == id));
|
||||
|
||||
public Task<Adverteerder?> FindAdverteerderByEmailAsync(string email, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Adverteerders.FirstOrDefault(a => a.Email == email));
|
||||
|
||||
public Task<IReadOnlyList<Adverteerder>> ListAdverteerdersAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Adverteerder>>(_s.Adverteerders.OrderBy(a => a.Bedrijfsnaam).ToList());
|
||||
|
||||
public void Add(Adverteerder a) => _s.Adverteerders.Add(a);
|
||||
|
||||
public Task<Campagne?> FindCampagneAsync(long id, CancellationToken ct = default)
|
||||
=> Task.FromResult(Met(_s.Campagnes.FirstOrDefault(c => c.Id == id)));
|
||||
|
||||
public Task<IReadOnlyList<Campagne>> ListCampagnesAsync(long adverteerderId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Campagne>>(
|
||||
_s.Campagnes.Where(c => c.AdverteerderId == adverteerderId)
|
||||
.OrderByDescending(c => c.AangemaaktOp).Select(c => Met(c)!).ToList());
|
||||
|
||||
public Task<IReadOnlyList<Campagne>> ListLopendAsync(DateOnly dag, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Campagne>>(
|
||||
_s.Campagnes.Where(c => c.DraaitOp(dag)).Select(c => Met(c)!).ToList());
|
||||
|
||||
public Task<IReadOnlyList<Campagne>> ListWachtendAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Campagne>>(
|
||||
_s.Campagnes.Where(c => c.Stand == CampagneStand.WachtOpBetaling ||
|
||||
c.Stand == CampagneStand.WachtOpGoedkeuring)
|
||||
.OrderBy(c => c.AangemaaktOp).Select(c => Met(c)!).ToList());
|
||||
|
||||
public void Add(Campagne c) => _s.Campagnes.Add(c);
|
||||
|
||||
public Task<CampagneUur?> FindUurAsync(long campagneId, DateTime uurUtc, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.CampagneUren.FirstOrDefault(u => u.CampagneId == campagneId && u.UurUtc == uurUtc));
|
||||
|
||||
public Task<IReadOnlyList<CampagneUur>> ListUrenAsync(long campagneId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<CampagneUur>>(
|
||||
_s.CampagneUren.Where(u => u.CampagneId == campagneId).OrderBy(u => u.UurUtc).ToList());
|
||||
|
||||
public Task<IReadOnlyDictionary<long, int>> VertoningenInUurAsync(
|
||||
DateTime uurUtc, IEnumerable<long> campagneIds, CancellationToken ct = default)
|
||||
{
|
||||
var lijst = campagneIds.ToHashSet();
|
||||
return Task.FromResult<IReadOnlyDictionary<long, int>>(
|
||||
_s.CampagneUren.Where(u => u.UurUtc == uurUtc && lijst.Contains(u.CampagneId))
|
||||
.ToDictionary(u => u.CampagneId, u => u.Vertoningen));
|
||||
}
|
||||
|
||||
/// <summary>Doet hetzelfde als de INSERT ... ON DUPLICATE KEY UPDATE van de echte opslag.</summary>
|
||||
public Task TelAsync(long campagneId, DateTime uurUtc, int vertoningen, int kliks,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var regel = _s.CampagneUren.FirstOrDefault(u => u.CampagneId == campagneId && u.UurUtc == uurUtc);
|
||||
if (regel is null)
|
||||
{
|
||||
regel = new CampagneUur { Id = _s.NextId(), CampagneId = campagneId, UurUtc = uurUtc };
|
||||
_s.CampagneUren.Add(regel);
|
||||
}
|
||||
|
||||
regel.Vertoningen += vertoningen;
|
||||
regel.Kliks += kliks;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<(int Vertoningen, int Kliks)> TotalenAsync(long campagneId, CancellationToken ct = default)
|
||||
{
|
||||
var uren = _s.CampagneUren.Where(u => u.CampagneId == campagneId).ToList();
|
||||
return Task.FromResult((uren.Sum(u => u.Vertoningen), uren.Sum(u => u.Kliks)));
|
||||
}
|
||||
|
||||
public void Add(CampagneUur u) => _s.CampagneUren.Add(u);
|
||||
}
|
||||
|
||||
public class Audit : IAuditLog
|
||||
{
|
||||
private readonly Store _s; private readonly IClock _c;
|
||||
public Audit(Store s, IClock c) { _s = s; _c = c; }
|
||||
public Task WriteAsync(AuditAction a, long? uid, string? et = null, long? eid = null, string? d = null, CancellationToken ct = default)
|
||||
{ _s.Audit.Add(new AuditLogEntry { Action = a, UserId = uid, EntityType = et, EntityId = eid, Details = d, CreatedAt = _c.UtcNow }); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
public class ToestelRepo : IToestelRepository
|
||||
{
|
||||
private readonly Store _s; public ToestelRepo(Store s) => _s = s;
|
||||
public Task<Toestel?> FindByKenmerkAsync(string kenmerk, CancellationToken ct = default)
|
||||
=> Task.FromResult(_s.Toestellen.FirstOrDefault(t => t.Kenmerk == kenmerk));
|
||||
public Task<IReadOnlyList<Toestel>> ListForUserAsync(long userId, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<Toestel>>(_s.Toestellen
|
||||
.Where(t => t.UserId == userId && t.AfgemeldOp == null).ToList());
|
||||
public void Add(Toestel t) => _s.Toestellen.Add(t);
|
||||
public void Remove(Toestel t) => _s.Toestellen.Remove(t);
|
||||
}
|
||||
|
||||
/// <summary>Onthoudt wat er verstuurd zou zijn, en kan doen alsof een toestel niet meer bestaat.</summary>
|
||||
public class TestPush : IPushSender
|
||||
{
|
||||
public List<(string Kenmerk, string Titel, string Tekst)> Verstuurd = new();
|
||||
|
||||
/// <summary>Kenmerken die Google niet meer kent.</summary>
|
||||
public HashSet<string> Onbekend = new();
|
||||
|
||||
public bool IsIngesteld { get; set; } = true;
|
||||
|
||||
public Task<bool> StuurAsync(string kenmerk, string titel, string tekst,
|
||||
IReadOnlyDictionary<string, string>? gegevens = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (Onbekend.Contains(kenmerk)) return Task.FromResult(false);
|
||||
Verstuurd.Add((kenmerk, titel, tekst));
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Deze ene bron wordt meegecompileerd in plaats van via een projectverwijzing. Hij hoort
|
||||
bij Infrastructure, en dat project heeft EF Core nodig; daarmee zou de zelftest niet
|
||||
meer zonder internet te bouwen zijn. De handtekening voor Google is te belangrijk om
|
||||
ongetest te laten, dus halen we alleen dat bestand erbij. -->
|
||||
<Compile Include="..\..\src\MijnBesluit.Infrastructure\Meldingen\Servicesleutel.cs"
|
||||
Link="Meegecompileerd\Servicesleutel.cs" />
|
||||
|
||||
<!-- Om dezelfde reden: dit hoort bij AppUi, en dat project heeft de Razor-SDK nodig. De
|
||||
volgorde waarin de terugknop wordt afgehandeld bepaalt of iemand per ongeluk de app
|
||||
afsluit terwijl hij een formulier invult, dus die willen we wel gecontroleerd hebben. -->
|
||||
<Compile Include="..\..\src\MijnBesluit.AppUi\Terugknop.cs"
|
||||
Link="Meegecompileerd\Terugknop.cs" />
|
||||
|
||||
<!-- En om dezelfde reden de uitlogklok: als die verkeerd rekent logt iemand midden in het
|
||||
invullen uit, of juist nooit terwijl hij dacht van wel. -->
|
||||
<Compile Include="..\..\src\MijnBesluit.AppUi\Uitlogklok.cs"
|
||||
Link="Meegecompileerd\Uitlogklok.cs" />
|
||||
|
||||
<!-- En het toestelslot, omdat de uitlogklok er een standaardwaarde van krijgt: met een slot
|
||||
gaat de app op slot en staat hij standaard aan, zonder slot logt hij uit en staat hij
|
||||
standaard uit. Draait dat om, dan logt iemand ongevraagd uit. -->
|
||||
<Compile Include="..\..\src\MijnBesluit.AppUi\Toestelslot.cs"
|
||||
Link="Meegecompileerd\Toestelslot.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Application\MijnBesluit.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Client\MijnBesluit.Client.csproj" />
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Protocol\MijnBesluit.Protocol.csproj" />
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Server\MijnBesluit.Server.csproj" />
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.ServiceBeheer\MijnBesluit.ServiceBeheer.csproj" />
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Uitrol\MijnBesluit.Uitrol.csproj" />
|
||||
<!-- Alleen om de instellingen van de agent te kunnen nakijken; de agent zelf wordt hier
|
||||
niet gedraaid. -->
|
||||
<ProjectReference Include="..\..\src\MijnBesluit.Uitrolagent\MijnBesluit.Uitrolagent.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
using MijnBesluit.Application.Abstractions;
|
||||
using MijnBesluit.Application.Services;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>Alles wat één testscenario nodig heeft, klaargezet in het geheugen.</summary>
|
||||
public class World
|
||||
{
|
||||
public Store Store { get; } = new();
|
||||
public TestClock Clock { get; } = new();
|
||||
public TestEmail Email { get; } = new();
|
||||
public TestPush Push { get; } = new();
|
||||
|
||||
/// <summary>Standaard uit; zet hem aan in een test die het verkleinen wil nakijken.</summary>
|
||||
public TestVerkleiner Verkleiner { get; } = new();
|
||||
|
||||
public AccountService Accounts { get; }
|
||||
public FamilyService Families { get; }
|
||||
public VaultService Vault { get; }
|
||||
public MessageService Messages { get; }
|
||||
public LegacyService Legacy { get; }
|
||||
public MeldingService Meldingen { get; }
|
||||
public OverzichtService Overzicht { get; }
|
||||
public SamenvoegService Samenvoegen { get; }
|
||||
public BeheerService Beheer { get; }
|
||||
public AdvertentieService Adverteren { get; }
|
||||
public AccountOpheffenService Opheffen { get; }
|
||||
|
||||
/// <summary>Maakt in één stap een account aan; scheelt in de scenario's het hele codegedoe.</summary>
|
||||
public async Task<MijnBesluit.Domain.Entities.User> MaakGebruikerAsync(string email, string voornaam, string achternaam = "")
|
||||
{
|
||||
var resultaat = await Accounts.MaakAccountAsync(email, voornaam, achternaam);
|
||||
if (!resultaat.Succeeded) throw new AssertionException($"account aanmaken mislukte: {resultaat.Error}");
|
||||
return resultaat.Value!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maakt iemand met een echt account die ook écht in de familie van <paramref name="eigenaar"/>
|
||||
/// staat: toevoegen, uitnodigen, aannemen. Dat laatste is nodig omdat toevoegen alleen een
|
||||
/// aantekening maakt; pas door de uitnodiging aan te nemen wordt het profiel van hemzelf en
|
||||
/// staat hij in de kring van de eigenaar.
|
||||
///
|
||||
/// Die kring bepaalt wie er iets mag zeggen over een overlijden, dus tests die dat nabootsen
|
||||
/// moeten hem ook echt opbouwen.
|
||||
/// </summary>
|
||||
public async Task<MijnBesluit.Domain.Entities.User> MaakFamilielidAsync(
|
||||
MijnBesluit.Domain.Entities.User eigenaar, string voornaam, string achternaam, string email,
|
||||
MijnBesluit.Domain.Enums.RelationType relatie = MijnBesluit.Domain.Enums.RelationType.Kind)
|
||||
{
|
||||
var familie = Store.Families.Single(f => f.OwnerPersonId == eigenaar.PersonId);
|
||||
|
||||
var profiel = await Families.AddMemberAsync(familie.Id, eigenaar.Id, voornaam, achternaam, email, relatie);
|
||||
if (!profiel.Succeeded) throw new AssertionException($"toevoegen mislukte: {profiel.Error}");
|
||||
|
||||
var token = await Families.InviteAsync(familie.Id, profiel.Value!.Id, eigenaar.Id);
|
||||
if (!token.Succeeded) throw new AssertionException($"uitnodigen mislukte: {token.Error}");
|
||||
|
||||
var account = await Families.AcceptInvitationAsync(token.Value!, Accounts);
|
||||
if (!account.Succeeded) throw new AssertionException($"aannemen mislukte: {account.Error}");
|
||||
|
||||
return account.Value!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wijst een of meer mensen aan als nalatenschapsbeheerder van <paramref name="betrokkene"/>.
|
||||
///
|
||||
/// Nodig in elke test die een overlijden meldt: alleen wie is aangewezen mag dat. Ze moeten
|
||||
/// ook echt in zijn familie staan, dus gebruik hiervoor mensen uit MaakFamilielidAsync.
|
||||
/// </summary>
|
||||
public async Task BeheerderAsync(MijnBesluit.Domain.Entities.User betrokkene,
|
||||
params MijnBesluit.Domain.Entities.User[] beheerders)
|
||||
{
|
||||
var volgorde = 1;
|
||||
foreach (var beheerder in beheerders)
|
||||
{
|
||||
var r = await Legacy.AppointManagerAsync(betrokkene.Id, beheerder.PersonId, volgorde++);
|
||||
if (!r.Succeeded) throw new AssertionException($"aanwijzen mislukte: {r.Error}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Haalt de code uit de laatst verstuurde e-mail, zoals een gebruiker hem uit zijn postvak haalt.</summary>
|
||||
public string LaatsteCodeUitMail(string email)
|
||||
{
|
||||
var mail = Email.Sent.Last(m => m.To == email);
|
||||
var match = System.Text.RegularExpressions.Regex.Match(mail.Body, @"letter-spacing:5px;margin:20px 0"">(\d+)<");
|
||||
if (!match.Success) throw new AssertionException("geen code gevonden in de e-mail");
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
public World()
|
||||
{
|
||||
var uow = new Uow(Store);
|
||||
var keys = new TestKeyProtector();
|
||||
var files = new TestFiles();
|
||||
var audit = new Audit(Store, Clock);
|
||||
|
||||
var users = new UserRepo(Store);
|
||||
var people = new PersonRepo(Store);
|
||||
var families = new FamilyRepo(Store);
|
||||
var invitations = new InvitationRepo(Store);
|
||||
var vault = new VaultRepo(Store);
|
||||
var messages = new MessageRepo(Store);
|
||||
var legacy = new LegacyRepo(Store);
|
||||
var codes = new LoginCodeRepo(Store);
|
||||
|
||||
Accounts = new AccountService(users, people, families, codes, keys, Email, audit, uow, Clock, vault);
|
||||
Families = new FamilyService(families, people, users, invitations, messages, legacy,
|
||||
Email, audit, uow, Clock);
|
||||
Vault = new VaultService(vault, people, messages, users, keys, files, audit, uow, Clock, Verkleiner, families, legacy);
|
||||
Meldingen = new MeldingService(new ToestelRepo(Store), users, Push, uow, Clock);
|
||||
Messages = new MessageService(messages, people, users, vault, keys, files, Email, audit, uow, Clock, Meldingen);
|
||||
Legacy = new LegacyService(legacy, people, users, vault, messages, Email, audit, uow, Clock, Vault, families);
|
||||
Overzicht = new OverzichtService(users, people, families, vault, messages, legacy, uow, Clock);
|
||||
Samenvoegen = new SamenvoegService(people, families, invitations, messages, vault, legacy,
|
||||
audit, uow, Clock, new SamenvoegRepo(Store));
|
||||
Beheer = new BeheerService(users, people, legacy, audit, Clock, Legacy, Samenvoegen);
|
||||
Adverteren = new AdvertentieService(new AdvertentieRepo(Store), users, Email, audit, uow,
|
||||
Clock, Verkleiner);
|
||||
Opheffen = new AccountOpheffenService(users, people, families, invitations, vault, messages,
|
||||
legacy, codes, new ToestelRepo(Store), files, Email,
|
||||
audit, uow, Clock);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Assert
|
||||
{
|
||||
public static void True(bool condition, string what)
|
||||
{
|
||||
if (!condition) throw new AssertionException(what);
|
||||
}
|
||||
|
||||
public static void Throws<TException>(Action action, string what) where TException : Exception
|
||||
{
|
||||
try { action(); }
|
||||
catch (TException) { return; }
|
||||
catch (Exception ex) { throw new AssertionException($"{what} (kreeg {ex.GetType().Name})"); }
|
||||
throw new AssertionException($"{what} (er werd geen fout gegooid)");
|
||||
}
|
||||
}
|
||||
|
||||
public class AssertionException : Exception
|
||||
{
|
||||
public AssertionException(string message) : base(message) { }
|
||||
}
|
||||
|
||||
/// <summary>Draait de scenario's en vat de uitkomst samen.</summary>
|
||||
public class Runner
|
||||
{
|
||||
private int _passed, _failed;
|
||||
|
||||
public void Test(string name, Action body) => Run(name, () => { body(); return Task.CompletedTask; });
|
||||
|
||||
public void TestAsync(string name, Func<Task> body) => Run(name, body);
|
||||
|
||||
private void Run(string name, Func<Task> body)
|
||||
{
|
||||
try
|
||||
{
|
||||
body().GetAwaiter().GetResult();
|
||||
_passed++;
|
||||
Console.WriteLine($" OK {name}");
|
||||
}
|
||||
catch (AssertionException ex)
|
||||
{
|
||||
_failed++;
|
||||
Console.WriteLine($" FOUT {name}");
|
||||
Console.WriteLine($" verwacht: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_failed++;
|
||||
Console.WriteLine($" FOUT {name}");
|
||||
Console.WriteLine($" {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public int Summary()
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(_failed == 0
|
||||
? $"Alle {_passed} controles geslaagd."
|
||||
: $"{_passed} geslaagd, {_failed} mislukt.");
|
||||
return _failed == 0 ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using MijnBesluit.ServiceBeheer;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Controleert wat het installatieschermpje doet met het poortnummer. Het aanmaken van de
|
||||
/// service zelf kan alleen op Windows en met beheerdersrechten; dat valt hier buiten.
|
||||
/// </summary>
|
||||
public static class ServiceChecks
|
||||
{
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.Test("Poortnummer: onmogelijke waarden worden geweigerd", () =>
|
||||
{
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(0) is not null, "0 is geen poort");
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(70000) is not null, "70000 bestaat niet");
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(-1) is not null, "negatief kan niet");
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(80)!.Contains("gereserveerd"),
|
||||
"onder 1024 wordt uitgelegd waarom het niet mag");
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(5200) is null, "5200 mag gewoon");
|
||||
Assert.True(ServiceInstellingen.ControleerPoort(65535) is null, "de hoogste poort mag");
|
||||
});
|
||||
|
||||
runner.Test("Poortnummer opslaan laat de rest van appsettings.json met rust", () =>
|
||||
{
|
||||
var map = Path.Combine(Path.GetTempPath(), "mb-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(map);
|
||||
var pad = Path.Combine(map, "appsettings.json");
|
||||
|
||||
File.WriteAllText(pad, """
|
||||
{
|
||||
"ConnectionStrings": { "MijnBesluit": "Server=localhost;Database=mijnbesluit" },
|
||||
"Socket": { "Port": 5200, "CertificateFile": "D:\\cert.pfx" },
|
||||
"Smtp": { "Host": "mail.example.nl" }
|
||||
}
|
||||
""");
|
||||
|
||||
var instellingen = new ServiceInstellingen(pad);
|
||||
Assert.True(instellingen.LeesPoort() == 5200, "de bestaande poort wordt gelezen");
|
||||
|
||||
instellingen.SchrijfPoort(5300);
|
||||
|
||||
var opnieuw = new ServiceInstellingen(pad);
|
||||
Assert.True(opnieuw.LeesPoort() == 5300, "de nieuwe poort staat erin");
|
||||
|
||||
var inhoud = File.ReadAllText(pad);
|
||||
Assert.True(inhoud.Contains("Server=localhost"), "de connectiereeks staat er nog");
|
||||
Assert.True(inhoud.Contains("cert.pfx"), "het certificaatpad staat er nog");
|
||||
Assert.True(inhoud.Contains("mail.example.nl"), "de e-mailinstellingen staan er nog");
|
||||
Assert.True(!File.Exists(pad + ".tmp"), "er blijft geen tijdelijk bestand achter");
|
||||
|
||||
Directory.Delete(map, true);
|
||||
});
|
||||
|
||||
runner.Test("Poortnummer opslaan werkt ook zonder bestaand bestand", () =>
|
||||
{
|
||||
var map = Path.Combine(Path.GetTempPath(), "mb-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(map);
|
||||
var pad = Path.Combine(map, "appsettings.json");
|
||||
|
||||
var instellingen = new ServiceInstellingen(pad);
|
||||
Assert.True(instellingen.LeesPoort() == ServiceInstellingen.StandaardPoort,
|
||||
"zonder bestand geldt de standaardpoort");
|
||||
|
||||
instellingen.SchrijfPoort(5400);
|
||||
Assert.True(new ServiceInstellingen(pad).LeesPoort() == 5400, "en daarna staat het er netjes in");
|
||||
|
||||
Directory.Delete(map, true);
|
||||
});
|
||||
|
||||
runner.Test("Een onmogelijke poort wordt niet weggeschreven", () =>
|
||||
{
|
||||
var map = Path.Combine(Path.GetTempPath(), "mb-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(map);
|
||||
var pad = Path.Combine(map, "appsettings.json");
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => new ServiceInstellingen(pad).SchrijfPoort(80),
|
||||
"poort 80 wordt geweigerd");
|
||||
Assert.True(!File.Exists(pad), "en er wordt niets opgeslagen");
|
||||
|
||||
Directory.Delete(map, true);
|
||||
});
|
||||
|
||||
runner.Test("Een bezette poort wordt herkend", () =>
|
||||
{
|
||||
var bezet = new TcpListener(IPAddress.Any, 0);
|
||||
bezet.Start();
|
||||
var poort = ((IPEndPoint)bezet.LocalEndpoint).Port;
|
||||
|
||||
try
|
||||
{
|
||||
Assert.True(!ServiceInstellingen.PoortIsVrij(poort), "de bezette poort wordt als bezet gezien");
|
||||
}
|
||||
finally { bezet.Stop(); }
|
||||
|
||||
Assert.True(ServiceInstellingen.PoortIsVrij(poort), "en na afloop weer als vrij");
|
||||
});
|
||||
|
||||
runner.Test("Een beschadigd appsettings.json wordt niet stilzwijgend gewist", () =>
|
||||
{
|
||||
var map = Path.Combine(Path.GetTempPath(), "mb-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(map);
|
||||
var pad = Path.Combine(map, "appsettings.json");
|
||||
File.WriteAllText(pad, "{ dit is geen geldige json");
|
||||
|
||||
var instellingen = new ServiceInstellingen(pad);
|
||||
Assert.True(instellingen.LeesPoort() == ServiceInstellingen.StandaardPoort,
|
||||
"onleesbare inhoud levert de standaardpoort op in plaats van een fout");
|
||||
|
||||
Directory.Delete(map, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using System.Net;
|
||||
using MijnBesluit.Protocol;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>Controleert de SocketServer met een echte verbinding over localhost.</summary>
|
||||
public static class SocketChecks
|
||||
{
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.TestAsync("SocketServer: verzoek en antwoord over een echte verbinding", async () =>
|
||||
{
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) => Task.FromResult(SocketResponse.Ok(req.RequestId, new { echo = req.Action })));
|
||||
server.Start();
|
||||
|
||||
await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
var response = await client.SendAsync(SocketActions.Ping);
|
||||
|
||||
Assert.True(response.Status == "Ok", "de server antwoordt met Ok");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: meerdere verzoeken tegelijk komen bij het juiste antwoord terecht", async () =>
|
||||
{
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
async (req, conn, ct) =>
|
||||
{
|
||||
// Antwoorden bewust door elkaar laten lopen.
|
||||
var nummer = req.PayloadAs<Dictionary<string, int>>()!["n"];
|
||||
await Task.Delay(50 - nummer * 5, ct);
|
||||
return SocketResponse.Ok(req.RequestId, new { n = nummer });
|
||||
});
|
||||
server.Start();
|
||||
|
||||
await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
await client.ConnectAsync();
|
||||
|
||||
var taken = Enumerable.Range(1, 8)
|
||||
.Select(n => client.SendAsync(SocketActions.Ping, new { n }))
|
||||
.ToArray();
|
||||
var antwoorden = await Task.WhenAll(taken);
|
||||
|
||||
for (var i = 0; i < antwoorden.Length; i++)
|
||||
{
|
||||
var terug = System.Text.Json.JsonSerializer
|
||||
.Deserialize<Dictionary<string, int>>(
|
||||
System.Text.Json.JsonSerializer.Serialize(antwoorden[i].Payload, SocketFraming.Json),
|
||||
SocketFraming.Json)!["n"];
|
||||
Assert.True(terug == i + 1, $"antwoord {i + 1} hoort bij het juiste verzoek");
|
||||
}
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: inhoud met '**BYE**' erin komt ongeschonden aan", async () =>
|
||||
{
|
||||
// Precies het randgeval waar een markering-gebaseerd protocol op stukloopt.
|
||||
var lastig = new string('A', 200_000) + "**BYE**" + new string('B', 200_000) + "BYE";
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) => Task.FromResult(SocketResponse.Ok(req.RequestId,
|
||||
new { tekst = req.PayloadAs<Dictionary<string, string>>()!["tekst"] })));
|
||||
server.Start();
|
||||
|
||||
await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
var response = await client.SendAsync(SocketActions.Ping, new { tekst = lastig });
|
||||
|
||||
var terug = System.Text.Json.JsonSerializer
|
||||
.Deserialize<Dictionary<string, string>>(
|
||||
System.Text.Json.JsonSerializer.Serialize(response.Payload, SocketFraming.Json),
|
||||
SocketFraming.Json)!["tekst"];
|
||||
|
||||
Assert.True(response.Status == "Ok", "de server antwoordt");
|
||||
Assert.True(terug.Length == lastig.Length, "de lengte klopt nog");
|
||||
Assert.True(terug == lastig, "de inhoud is niet afgekapt op de markering");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: een fout in de afhandeling lekt geen interne details", async () =>
|
||||
{
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) => throw new InvalidOperationException("verbinding met database mislukt op server01"));
|
||||
server.Start();
|
||||
|
||||
await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
var response = await client.SendAsync(SocketActions.Ping);
|
||||
|
||||
Assert.True(response.Status == "Error", "de app krijgt een foutantwoord");
|
||||
Assert.True(!response.Message!.Contains("server01"), "zonder interne details erin");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: de server kan uit zichzelf een melding sturen", async () =>
|
||||
{
|
||||
SocketConnection? verbinding = null;
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) => { verbinding = conn; conn.UserId = 42; return Task.FromResult(SocketResponse.Ok(req.RequestId)); });
|
||||
server.Start();
|
||||
|
||||
await using var client = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
var ontvangen = new TaskCompletionSource<SocketResponse>();
|
||||
client.PushReceived += r => ontvangen.TrySetResult(r);
|
||||
|
||||
await client.SendAsync(SocketActions.Ping);
|
||||
Assert.True(verbinding is not null, "de server kent de verbinding");
|
||||
|
||||
await server.PushToUserAsync(42, new SocketResponse { Status = "Ok", Message = "Er staat een bericht voor je klaar." });
|
||||
|
||||
var melding = await ontvangen.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.True(melding.Message!.Contains("bericht voor je klaar"), "de app ontvangt de melding");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: elke verbinding krijgt een eigen thread", async () =>
|
||||
{
|
||||
var threadsPerVerbinding = new System.Collections.Concurrent.ConcurrentDictionary<string, string>();
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) =>
|
||||
{
|
||||
threadsPerVerbinding[conn.Id] = conn.ThreadName ?? "?";
|
||||
return Task.FromResult(SocketResponse.Ok(req.RequestId, new { thread = conn.ThreadName }));
|
||||
});
|
||||
server.Start();
|
||||
|
||||
await using var app1 = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
await using var app2 = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
|
||||
await app1.SendAsync(SocketActions.Ping);
|
||||
await app2.SendAsync(SocketActions.Ping);
|
||||
|
||||
Assert.True(threadsPerVerbinding.Count == 2, "de server ziet twee aparte verbindingen");
|
||||
Assert.True(threadsPerVerbinding.Values.Distinct().Count() == 2,
|
||||
"elke verbinding wordt door een eigen thread bediend");
|
||||
Assert.True(threadsPerVerbinding.Values.All(n => n.StartsWith("MijnBesluit-verbinding-")),
|
||||
"de thread draagt de naam van de verbinding, handig bij foutzoeken");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: een traag verzoek houdt de volgende niet op", async () =>
|
||||
{
|
||||
// Drie verzoeken van dezelfde verbinding die elk een halve seconde blijven hangen.
|
||||
// Op een gedeelde thread zou dat anderhalve seconde duren; elk op een eigen thread
|
||||
// hooguit iets meer dan een halve.
|
||||
var gebruikteThreads = new System.Collections.Concurrent.ConcurrentBag<int>();
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions { Address = IPAddress.Loopback, Port = 0 },
|
||||
(req, conn, ct) =>
|
||||
{
|
||||
gebruikteThreads.Add(Environment.CurrentManagedThreadId);
|
||||
Thread.Sleep(500);
|
||||
return Task.FromResult(SocketResponse.Ok(req.RequestId));
|
||||
});
|
||||
server.Start();
|
||||
|
||||
await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
await app.ConnectAsync();
|
||||
|
||||
var klok = System.Diagnostics.Stopwatch.StartNew();
|
||||
var antwoorden = await Task.WhenAll(
|
||||
app.SendAsync(SocketActions.Ping),
|
||||
app.SendAsync(SocketActions.Ping),
|
||||
app.SendAsync(SocketActions.Ping));
|
||||
klok.Stop();
|
||||
|
||||
Assert.True(antwoorden.All(a => a.Status == "Ok"), "alle drie de verzoeken worden beantwoord");
|
||||
Assert.True(gebruikteThreads.Distinct().Count() == 3, "elk verzoek liep op zijn eigen thread");
|
||||
Assert.True(klok.ElapsedMilliseconds < 1200,
|
||||
$"ze liepen naast elkaar (duurde {klok.ElapsedMilliseconds} ms, na elkaar zou 1500 ms zijn)");
|
||||
});
|
||||
|
||||
runner.TestAsync("SocketServer: boven de grens wachten verzoeken netjes op hun beurt", async () =>
|
||||
{
|
||||
var tegelijk = 0;
|
||||
var hoogste = 0;
|
||||
var slot = new object();
|
||||
|
||||
await using var server = new SocketListener(
|
||||
new SocketListenerOptions
|
||||
{
|
||||
Address = IPAddress.Loopback,
|
||||
Port = 0,
|
||||
MaxRequestsPerConnection = 2
|
||||
},
|
||||
(req, conn, ct) =>
|
||||
{
|
||||
lock (slot) { tegelijk++; hoogste = Math.Max(hoogste, tegelijk); }
|
||||
Thread.Sleep(150);
|
||||
lock (slot) { tegelijk--; }
|
||||
return Task.FromResult(SocketResponse.Ok(req.RequestId));
|
||||
});
|
||||
server.Start();
|
||||
|
||||
await using var app = new MijnBesluitSocketClient("127.0.0.1", server.Port, useTls: false);
|
||||
await app.ConnectAsync();
|
||||
|
||||
var antwoorden = await Task.WhenAll(Enumerable.Range(0, 6)
|
||||
.Select(_ => app.SendAsync(SocketActions.Ping, timeout: TimeSpan.FromSeconds(20))));
|
||||
|
||||
Assert.True(antwoorden.All(a => a.Status == "Ok"), "alle zes verzoeken worden alsnog beantwoord");
|
||||
Assert.True(hoogste <= 2, $"er liepen er nooit meer dan twee tegelijk (gemeten: {hoogste})");
|
||||
});
|
||||
|
||||
runner.Test("Bestandsoverdracht: stukken moeten op volgorde en binnen de maat blijven", () =>
|
||||
{
|
||||
var klok = new TestClock();
|
||||
var overdracht = new MijnBesluit.Server.Bestandsoverdracht(klok);
|
||||
|
||||
var id = overdracht.Begin(gebruikerId: 1, "foto.jpg", "image/jpeg", null, totaalBytes: 10);
|
||||
overdracht.Stuk(id, 1, 0, new byte[4]);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Stuk(id, 1, 0, new byte[4]),
|
||||
"hetzelfde stuk twee keer wordt geweigerd");
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Stuk(id, 1, 5, new byte[4]),
|
||||
"een stuk dat overslaat wordt geweigerd");
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Stuk(id, 1, 1, new byte[99]),
|
||||
"meer dan aangekondigd wordt geweigerd");
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Klaar(id, 1),
|
||||
"afsluiten met te weinig bytes wordt geweigerd");
|
||||
|
||||
overdracht.Stuk(id, 1, 1, new byte[6]);
|
||||
var (naam, soort, item, inhoud) = overdracht.Klaar(id, 1);
|
||||
Assert.True(naam == "foto.jpg" && soort == "image/jpeg" && item is null, "de gegevens komen terug");
|
||||
Assert.True(inhoud.Length == 10, "en de volledige inhoud");
|
||||
Assert.True(overdracht.Lopend == 0, "de overdracht is opgeruimd");
|
||||
});
|
||||
|
||||
runner.Test("Bestandsoverdracht: van een ander, of te lang blijven liggen", () =>
|
||||
{
|
||||
var klok = new TestClock();
|
||||
var overdracht = new MijnBesluit.Server.Bestandsoverdracht(klok);
|
||||
|
||||
var id = overdracht.Begin(gebruikerId: 1, "polis.pdf", "application/pdf", null, totaalBytes: 100);
|
||||
|
||||
// Gebruiker 2 mag er niet bij, ook niet als hij het kenmerk op de een of andere manier kent.
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Stuk(id, 2, 0, new byte[10]),
|
||||
"de upload van iemand anders bestaat voor jou niet");
|
||||
overdracht.Afbreken(id, 2);
|
||||
Assert.True(overdracht.Lopend == 1, "en is door hem ook niet af te breken");
|
||||
|
||||
// Blijft hij te lang liggen, dan is hij weg.
|
||||
klok.Advance(MijnBesluit.Server.Bestandsoverdracht.Verlooptijd + TimeSpan.FromMinutes(1));
|
||||
Assert.Throws<InvalidOperationException>(() => overdracht.Stuk(id, 1, 0, new byte[10]),
|
||||
"een upload die te lang stillag wordt geweigerd");
|
||||
});
|
||||
|
||||
runner.Test("Bestandsoverdracht: niet eindeloos veel tegelijk", () =>
|
||||
{
|
||||
var overdracht = new MijnBesluit.Server.Bestandsoverdracht(new TestClock());
|
||||
|
||||
for (var i = 0; i < MijnBesluit.Server.Bestandsoverdracht.MaxPerGebruiker; i++)
|
||||
overdracht.Begin(1, $"bestand{i}.bin", "application/octet-stream", null, 100);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => overdracht.Begin(1, "nog een.bin", "application/octet-stream", null, 100),
|
||||
"boven de grens wordt een nieuwe upload geweigerd");
|
||||
|
||||
// Iemand anders heeft daar geen last van.
|
||||
overdracht.Begin(2, "van piet.bin", "application/octet-stream", null, 100);
|
||||
Assert.True(overdracht.Lopend == MijnBesluit.Server.Bestandsoverdracht.MaxPerGebruiker + 1,
|
||||
"de grens geldt per gebruiker");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
using MijnBesluit.Uitrol;
|
||||
|
||||
namespace MijnBesluit.SelfCheck;
|
||||
|
||||
/// <summary>
|
||||
/// De handtekening waarmee de uitrolagent weet dat een opdracht van jou komt.
|
||||
///
|
||||
/// Dit is het stuk dat je maar één keer fout hoeft te doen. De agent staat op de server waar de
|
||||
/// kluizen en de masterkey liggen, en hij installeert wat hij binnenkrijgt. Elke grendel die
|
||||
/// hieronder wordt beproefd is er een die anders stilletjes weg kan vallen.
|
||||
/// </summary>
|
||||
public static class UitrolChecks
|
||||
{
|
||||
/// <summary>Een geheugen dat doet alsof er op een bepaald moment is uitgerold.</summary>
|
||||
private sealed class Proefgeheugen : Uitrolgeheugen
|
||||
{
|
||||
private readonly DateTime _moment;
|
||||
public Proefgeheugen(DateTime moment) => _moment = moment;
|
||||
|
||||
public override DateTime? Laatste(string onderdeel) => _moment;
|
||||
public override void Onthoud(string onderdeel, DateTime nieuwste) { }
|
||||
}
|
||||
|
||||
public static void Register(Runner runner)
|
||||
{
|
||||
runner.Test("Uitrol: een opdracht zonder het juiste geheim komt er niet in", () =>
|
||||
{
|
||||
var geheim = Uitrolsleutel.NieuweSleutel();
|
||||
var nu = DateTime.UtcNow;
|
||||
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var nonce = Uitrolsleutel.NieuweNonce();
|
||||
var doel = "pakket:1:ABCDEF";
|
||||
|
||||
var handtekening = Uitrolsleutel.Teken(geheim, doel, tijd, nonce);
|
||||
|
||||
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, nu, new HashSet<string>()),
|
||||
"met het goede geheim mag het");
|
||||
|
||||
var ander = Uitrolsleutel.NieuweSleutel();
|
||||
Assert.True(!Uitrolsleutel.Klopt(ander, doel, tijd, nonce, handtekening, nu, new HashSet<string>()),
|
||||
"met een ander geheim niet");
|
||||
|
||||
// Een kort geheim is geen geheim. Dit vangt de dag waarop iemand "test" invult om
|
||||
// even iets te proberen en dat laat staan.
|
||||
Assert.True(!Uitrolsleutel.Klopt("kort", doel, tijd, nonce,
|
||||
Uitrolsleutel.Teken("kort", doel, tijd, nonce), nu, new HashSet<string>()),
|
||||
"een te kort geheim wordt geweigerd, ook als de handtekening zelf klopt");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: een ander pakket onder dezelfde handtekening wordt geweigerd", () =>
|
||||
{
|
||||
// Dit is waar de hash voor is. Zonder deze band tussen handtekening en inhoud kan
|
||||
// iemand die een geldige opdracht opvangt er zijn eigen zip achteraan sturen.
|
||||
var geheim = Uitrolsleutel.NieuweSleutel();
|
||||
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var nonce = Uitrolsleutel.NieuweNonce();
|
||||
|
||||
var echt = new Uitrolopdracht(Uitrolsoort.Site, "mijnbesluit.zip", 1000, "AAAA", tijd, nonce, "");
|
||||
var handtekening = Uitrolsleutel.Teken(geheim, echt.Doel, tijd, nonce);
|
||||
|
||||
var vervalst = echt with { Sha256 = "BBBB" };
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, vervalst.Doel, tijd, nonce, handtekening, DateTime.UtcNow,
|
||||
new HashSet<string>()),
|
||||
"een andere inhoud hoort niet onder deze handtekening te passen");
|
||||
|
||||
// Ook de soort zit erin: een pakket voor de site hoort niet als databasescript te
|
||||
// kunnen binnenkomen.
|
||||
var andereSoort = echt with { Soort = Uitrolsoort.Database };
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, andereSoort.Doel, tijd, nonce, handtekening, DateTime.UtcNow,
|
||||
new HashSet<string>()),
|
||||
"en een andere soort ook niet");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: dezelfde opdracht kan geen tweede keer", () =>
|
||||
{
|
||||
// Zonder dit kan wie een opdracht opvangt hem gewoon nog een keer sturen. Bij een
|
||||
// uitrol betekent dat: nog een keer installeren, op een moment dat jij het niet weet.
|
||||
var geheim = Uitrolsleutel.NieuweSleutel();
|
||||
var tijd = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var nonce = Uitrolsleutel.NieuweNonce();
|
||||
var doel = "pakket:1:ABCDEF";
|
||||
var handtekening = Uitrolsleutel.Teken(geheim, doel, tijd, nonce);
|
||||
|
||||
var gezien = new HashSet<string>();
|
||||
|
||||
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, DateTime.UtcNow, gezien),
|
||||
"de eerste keer mag");
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, tijd, nonce, handtekening, DateTime.UtcNow, gezien),
|
||||
"de tweede keer niet");
|
||||
|
||||
// En een mislukte poging mag een geldige opdracht niet in de weg zitten: anders kan
|
||||
// iemand die alleen het nonce kent jouw uitrol tegenhouden door hem vóór te zijn.
|
||||
var vers = Uitrolsleutel.NieuweNonce();
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, tijd, vers, "ONZIN", DateTime.UtcNow, gezien),
|
||||
"een verkeerde handtekening wordt geweigerd");
|
||||
Assert.True(Uitrolsleutel.Klopt(geheim, doel, tijd, vers,
|
||||
Uitrolsleutel.Teken(geheim, doel, tijd, vers), DateTime.UtcNow, gezien),
|
||||
"en datzelfde nonce mag daarna nog wel met de juiste handtekening");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: een oude opdracht verloopt", () =>
|
||||
{
|
||||
var geheim = Uitrolsleutel.NieuweSleutel();
|
||||
var nu = DateTime.UtcNow;
|
||||
var doel = "stand";
|
||||
|
||||
var lang = new DateTimeOffset(nu.AddMinutes(-10)).ToUnixTimeSeconds();
|
||||
var nonce = Uitrolsleutel.NieuweNonce();
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, lang, nonce,
|
||||
Uitrolsleutel.Teken(geheim, doel, lang, nonce), nu, new HashSet<string>()),
|
||||
"tien minuten oud is te oud");
|
||||
|
||||
// En ook niet uit de toekomst: een klok die ver vooruit staat zou anders een opdracht
|
||||
// opleveren die uren geldig blijft.
|
||||
var straks = new DateTimeOffset(nu.AddMinutes(10)).ToUnixTimeSeconds();
|
||||
var nonce2 = Uitrolsleutel.NieuweNonce();
|
||||
Assert.True(!Uitrolsleutel.Klopt(geheim, doel, straks, nonce2,
|
||||
Uitrolsleutel.Teken(geheim, doel, straks, nonce2), nu, new HashSet<string>()),
|
||||
"en tien minuten in de toekomst ook niet");
|
||||
|
||||
// Een halve minuut speling hoort geen probleem te zijn; niet elke klok loopt gelijk.
|
||||
var bijna = new DateTimeOffset(nu.AddSeconds(-30)).ToUnixTimeSeconds();
|
||||
var nonce3 = Uitrolsleutel.NieuweNonce();
|
||||
Assert.True(Uitrolsleutel.Klopt(geheim, doel, bijna, nonce3,
|
||||
Uitrolsleutel.Teken(geheim, doel, bijna, nonce3), nu, new HashSet<string>()),
|
||||
"een halve minuut verschil mag wel");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: de volgorde ligt vast, ook als het briefje anders is opgeschreven", () =>
|
||||
{
|
||||
// De volgorde is geen smaak: een nieuwe kolom moet er zijn voordat de code erom
|
||||
// vraagt, en de SocketServer gaat als laatste omdat het herstarten daarvan alle apps
|
||||
// hun verbinding kost. Het venster zet de vinkjes in willekeurige volgorde in het
|
||||
// briefje; de agent hoort er niet van af te hangen.
|
||||
var brief = new Uitrolbrief("pc", DateTime.UtcNow, new[]
|
||||
{
|
||||
new Uitrolpakket(Uitrolsoort.SocketServer, "socket.zip", "aa"),
|
||||
new Uitrolpakket(Uitrolsoort.Site, "site.zip", "bb"),
|
||||
new Uitrolpakket(Uitrolsoort.Database, "db.zip", "cc")
|
||||
});
|
||||
|
||||
var volgorde = brief.InVolgorde.Select(p => p.Soort).ToList();
|
||||
|
||||
Assert.True(volgorde[0] == Uitrolsoort.Database, "de databasescripts gaan eerst");
|
||||
Assert.True(volgorde[1] == Uitrolsoort.Site, "dan de website");
|
||||
Assert.True(volgorde[2] == Uitrolsoort.SocketServer, "en de SocketServer als laatste");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: een briefje overleeft de reis naar de server", () =>
|
||||
{
|
||||
// Het briefje gaat als JSON over een doorgegeven schijf naar de agent. Als dat aan de
|
||||
// overkant niet meer te lezen is, staat er een uitrol stil op iets doms.
|
||||
var brief = new Uitrolbrief("MIJN-PC", new DateTime(2026, 9, 17, 12, 0, 0, DateTimeKind.Utc),
|
||||
new[] { new Uitrolpakket(Uitrolsoort.Site, "mijnbesluit.zip", "ABCDEF") });
|
||||
|
||||
var terug = System.Text.Json.JsonSerializer.Deserialize<Uitrolbrief>(
|
||||
System.Text.Json.JsonSerializer.Serialize(brief),
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
Assert.True(terug is not null, "het briefje is terug te lezen");
|
||||
Assert.True(terug!.Van == "MIJN-PC", "van welke pc hij komt");
|
||||
Assert.True(terug.Pakketten.Count == 1, "met één pakket erin");
|
||||
Assert.True(terug.Pakketten[0].Soort == Uitrolsoort.Site, "van de goede soort");
|
||||
Assert.True(terug.Pakketten[0].Sha256 == "ABCDEF", "en met de hash erbij");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: de hash van een bestand is dezelfde als die van zijn inhoud", () =>
|
||||
{
|
||||
// De agent rekent de hash van het pakket op schijf uit en legt die naast wat er in
|
||||
// het briefje staat. Rekenen die twee kanten anders, dan wordt elk pakket geweigerd
|
||||
// en is er geen uitrol meer mogelijk.
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes("een pakket dat niet echt een pakket is");
|
||||
var tijdelijk = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}.bin");
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(tijdelijk, bytes);
|
||||
|
||||
Assert.True(Uitrolsleutel.HashVanBestand(tijdelijk) == Uitrolsleutel.Hash(bytes),
|
||||
"van schijf gelezen levert dezelfde hash op");
|
||||
|
||||
// En één byte verschil hoort een andere hash te geven - anders vangt hij geen
|
||||
// half geschreven zip.
|
||||
bytes[0]++;
|
||||
File.WriteAllBytes(tijdelijk, bytes);
|
||||
Assert.True(Uitrolsleutel.HashVanBestand(tijdelijk) != Uitrolsleutel.Hash(
|
||||
System.Text.Encoding.UTF8.GetBytes("een pakket dat niet echt een pakket is")),
|
||||
"een gewijzigd bestand valt door de mand");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(tijdelijk); } catch (Exception) { }
|
||||
}
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: geheimen uit een apart bestand gaan over de instellingen heen", () =>
|
||||
{
|
||||
// Het wachtwoord van het uitrolaccount hoort niet in uitrolagent.json: die map wordt
|
||||
// bij elke publicatie overschreven, en dan ben je het kwijt zonder dat iemand het
|
||||
// zegt - tot het databasedeel omvalt. Daarom een tweede bestand buiten die map.
|
||||
//
|
||||
// Wat hier bewaakt wordt is het gedrag dat makkelijk fout gaat: alleen de sleutels
|
||||
// die in dat tweede bestand staan mogen eroverheen. Zou het hele object worden
|
||||
// vervangen, dan zet een geheimenbestand met alleen een wachtwoord al je andere
|
||||
// instellingen terug op de standaardwaarden.
|
||||
var map = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(map);
|
||||
|
||||
try
|
||||
{
|
||||
var hoofd = Path.Combine(map, "uitrolagent.json");
|
||||
var geheim = Path.Combine(map, "geheimen.json");
|
||||
|
||||
File.WriteAllText(hoofd, $$"""
|
||||
{
|
||||
"Poort": 5311,
|
||||
"Scriptmap": "C:\\Eigen\\Scripts",
|
||||
"DbGebruiker": "",
|
||||
"ExtraConfigFile": {{System.Text.Json.JsonSerializer.Serialize(geheim)}}
|
||||
}
|
||||
""");
|
||||
|
||||
File.WriteAllText(geheim, """
|
||||
{
|
||||
"//": "alleen dit hoort eroverheen te gaan",
|
||||
"DbGebruiker": "mijnbesluit_uitrol",
|
||||
"DbWachtwoord": "geheim"
|
||||
}
|
||||
""");
|
||||
|
||||
var uitkomst = MijnBesluit.Uitrolagent.Agentinstellingen.Lees(hoofd);
|
||||
|
||||
Assert.True(uitkomst.DbGebruiker == "mijnbesluit_uitrol", "het account komt uit het tweede bestand");
|
||||
Assert.True(uitkomst.DbWachtwoord == "geheim", "het wachtwoord ook");
|
||||
Assert.True(uitkomst.Poort == 5311, "en de poort uit het eerste blijft staan");
|
||||
Assert.True(uitkomst.Scriptmap == @"C:\Eigen\Scripts", "net als de scriptmap");
|
||||
|
||||
// Staat het tweede bestand er niet, dan verandert er niets en klaagt er niets.
|
||||
File.Delete(geheim);
|
||||
var zonder = MijnBesluit.Uitrolagent.Agentinstellingen.Lees(hoofd);
|
||||
Assert.True(zonder.Poort == 5311 && string.IsNullOrEmpty(zonder.DbGebruiker),
|
||||
"zonder geheimenbestand blijft de rest gewoon staan");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(map, true); } catch (Exception) { }
|
||||
}
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: het venster ziet zelf wat er mee moet", () =>
|
||||
{
|
||||
// Het venster zet de vinkjes op wat er sinds de vorige uitrol is gewijzigd. Die
|
||||
// afhankelijkheden zijn het soort ding dat stil gaat afwijken: verplaatst iemand een
|
||||
// klasse van Domain naar Application, of komt er een project bij, dan staat er
|
||||
// straks een website uitgerold tegen een SocketServer die iets anders verwacht -
|
||||
// zonder dat iemand iets fout heeft gedaan.
|
||||
var map = Path.Combine(Path.GetTempPath(), $"uitrolproef-{Guid.NewGuid():N}");
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var deel in new[]
|
||||
{
|
||||
@"src/MijnBesluit.Web", @"src/MijnBesluit.Domain", @"src/MijnBesluit.SocketServer",
|
||||
@"src/MijnBesluit.Protocol", @"src/MijnBesluit.App", @"db"
|
||||
})
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(map, deel));
|
||||
File.WriteAllText(Path.Combine(map, deel, "iets.txt"), "oud");
|
||||
}
|
||||
|
||||
// Alles op oud zetten, en dan doen alsof er net is uitgerold.
|
||||
var oud = DateTime.UtcNow.AddDays(-1);
|
||||
foreach (var bestand in Directory.GetFiles(map, "*", SearchOption.AllDirectories))
|
||||
File.SetLastWriteTimeUtc(bestand, oud);
|
||||
|
||||
var geheugen = new Proefgeheugen(DateTime.UtcNow.AddHours(-1));
|
||||
|
||||
var stil = Wijzigingen.Bepaal(map, geheugen);
|
||||
Assert.True(stil.Values.All(s => !s.Nodig), "zonder wijzigingen hoeft er niets mee");
|
||||
|
||||
// Eén bestand in het domein: dat zit in de website én in de SocketServer, maar
|
||||
// niet in de database en niet in de app.
|
||||
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Domain", "iets.txt"), DateTime.UtcNow);
|
||||
|
||||
var na = Wijzigingen.Bepaal(map, geheugen);
|
||||
Assert.True(na["site"].Nodig, "een wijziging in het domein raakt de website");
|
||||
Assert.True(na["socketserver"].Nodig, "en de SocketServer");
|
||||
Assert.True(!na["database"].Nodig, "maar niet de databasescripts");
|
||||
Assert.True(!na["apk"].Nodig, "en niet de app");
|
||||
|
||||
// Het protocol is de taal tussen server en app; de website praat daar niet over.
|
||||
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Domain", "iets.txt"), oud);
|
||||
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Protocol", "iets.txt"), DateTime.UtcNow);
|
||||
|
||||
var protocol = Wijzigingen.Bepaal(map, geheugen);
|
||||
Assert.True(protocol["socketserver"].Nodig && protocol["apk"].Nodig,
|
||||
"het protocol raakt de SocketServer en de app");
|
||||
Assert.True(!protocol["site"].Nodig, "en niet de website");
|
||||
|
||||
// Een nieuw databasescript raakt alleen de database.
|
||||
File.SetLastWriteTimeUtc(Path.Combine(map, "src/MijnBesluit.Protocol", "iets.txt"), oud);
|
||||
File.WriteAllText(Path.Combine(map, "db", "99_nieuw.sql"), "SELECT 1;");
|
||||
|
||||
var db = Wijzigingen.Bepaal(map, geheugen);
|
||||
Assert.True(db["database"].Nodig, "een nieuw script raakt de database");
|
||||
Assert.True(!db["site"].Nodig && !db["socketserver"].Nodig && !db["apk"].Nodig,
|
||||
"en verder niets");
|
||||
|
||||
// Wat de compiler zelf maakt telt niet mee - anders staat alles altijd aan.
|
||||
Directory.CreateDirectory(Path.Combine(map, "src/MijnBesluit.Web/bin"));
|
||||
File.WriteAllText(Path.Combine(map, "src/MijnBesluit.Web/bin", "iets.dll"), "x");
|
||||
|
||||
var gebouwd = Wijzigingen.Bepaal(map, geheugen);
|
||||
Assert.True(!gebouwd["site"].Nodig, "een bestand in bin telt niet mee");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(map, true); } catch (Exception) { }
|
||||
}
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: de agent kent alleen een vaste lijst opdrachten", () =>
|
||||
{
|
||||
// Er hoort geen "voer dit uit" bij te zitten. Deze controle is er niet om de code te
|
||||
// beproeven maar om de dag te markeren waarop iemand die wél toevoegt.
|
||||
Assert.True(Uitrolacties.Alle.Count == 5, "vijf opdrachten, niet meer");
|
||||
|
||||
foreach (var actie in Uitrolacties.Alle)
|
||||
Assert.True(actie.StartsWith("uitrol.", StringComparison.Ordinal),
|
||||
$"elke opdracht hoort bij het uitrollen ({actie})");
|
||||
|
||||
Assert.True(!Uitrolacties.Alle.Any(a => a.Contains("exec") || a.Contains("command") ||
|
||||
a.Contains("shell") || a.Contains("script")),
|
||||
"geen opdracht die code van buiten uitvoert");
|
||||
});
|
||||
|
||||
runner.Test("Uitrol: de wachtstand schrijft niets rechtstreeks naar het scherm", () =>
|
||||
{
|
||||
// Het agentvenster is een WPF-programma en heeft geen console. Alles wat de wachtstand
|
||||
// met Console.WriteLine zou schrijven, verdwijnt daar spoorloos: je kijkt naar een leeg
|
||||
// venster terwijl de uitrol wel degelijk loopt. Dat is precies het soort stilte waar in
|
||||
// dit project al twee avonden aan zijn opgegaan.
|
||||
//
|
||||
// Vandaar deze controle. Elke regel in Wachtstand.cs die Console aanroept moet met
|
||||
// // standaardluisteraar zijn gemerkt - en die staan alleen in de terugvaloptie die
|
||||
// gebruikt wordt als er geen luisteraar is meegegeven, dus in de console-agent.
|
||||
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
||||
if (map is null) return; // gebouwd buiten de projectmap
|
||||
|
||||
var bestand = Path.Combine(map.FullName, "src", "MijnBesluit.Uitrolagent", "Wachtstand.cs");
|
||||
if (!File.Exists(bestand)) return;
|
||||
|
||||
var regels = File.ReadAllLines(bestand);
|
||||
var mis = new List<string>();
|
||||
|
||||
for (var i = 0; i < regels.Length; i++)
|
||||
{
|
||||
var regel = regels[i];
|
||||
if (!regel.Contains("Console.", StringComparison.Ordinal)) continue;
|
||||
if (regel.TrimStart().StartsWith("///", StringComparison.Ordinal)) continue;
|
||||
if (regel.TrimStart().StartsWith("//", StringComparison.Ordinal)) continue;
|
||||
if (regel.Contains("// standaardluisteraar", StringComparison.Ordinal)) continue;
|
||||
|
||||
mis.Add($"regel {i + 1}: {regel.Trim()}");
|
||||
}
|
||||
|
||||
Assert.True(mis.Count == 0,
|
||||
"de wachtstand mag alleen via de meegegeven luisteraar praten, anders ziet " +
|
||||
"het agentvenster niets: " + string.Join(" | ", mis));
|
||||
});
|
||||
|
||||
runner.Test("Geen projectbestand met een onmogelijk commentaar", () =>
|
||||
{
|
||||
// Twee streepjes achter elkaar mogen niet in een XML-commentaar. In deze projectmap is
|
||||
// dat geen theorie: de uitleg bij een csproj noemt graag de schakelaars van een
|
||||
// programma, en die beginnen nu eenmaal met twee streepjes. Het gevolg is MSB4025 - het
|
||||
// projectbestand laadt niet eens, dus je krijgt geen foutmelding over je code maar over
|
||||
// je commentaar, en dat op het moment dat je wilde publiceren.
|
||||
//
|
||||
// Deze controle leest elk projectbestand als XML. Dat vangt hetzelfde af als MSBuild,
|
||||
// en meteen ook een vergeten sluittag.
|
||||
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
||||
if (map is null) return; // gebouwd buiten de projectmap
|
||||
|
||||
var bestanden = new List<string>();
|
||||
|
||||
foreach (var onder in new[] { "src", "tests" })
|
||||
{
|
||||
var deel = Path.Combine(map.FullName, onder);
|
||||
if (Directory.Exists(deel))
|
||||
bestanden.AddRange(Directory.GetFiles(deel, "*.csproj", SearchOption.AllDirectories));
|
||||
}
|
||||
|
||||
bestanden.AddRange(Directory.GetFiles(map.FullName, "*.props", SearchOption.TopDirectoryOnly));
|
||||
|
||||
var mis = new List<string>();
|
||||
|
||||
foreach (var bestand in bestanden)
|
||||
{
|
||||
if (bestand.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") ||
|
||||
bestand.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")) continue;
|
||||
|
||||
try
|
||||
{
|
||||
System.Xml.Linq.XDocument.Load(bestand);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
mis.Add($"{Path.GetFileName(bestand)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(mis.Count == 0, "een projectbestand is geen geldige XML: " + string.Join(" | ", mis));
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user