using System.Net;
using MijnBesluit.Client;
using MijnBesluit.Protocol;
using MijnBesluit.Server;
namespace MijnBesluit.SelfCheck;
///
/// 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.
///
public static class ClientChecks
{
/// Een draaiende SocketServer met opslag in het geheugen, zoals de app hem ziet.
private sealed class Proef : IAsyncDisposable
{
public World Wereld { get; } = new();
public SocketListener Server { get; }
/// De sessies van de server, om een herstart mee na te bootsen.
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,
Soorten = Wereld.Soorten,
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);
/// Registreert een verse gebruiker en geeft een aangemelde app terug.
public async Task 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: de soortenlijst komt over de lijn en de ingebouwde lijst in de app klopt ermee", async () =>
{
await using var proef = new Proef();
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
var lijst = await app.KluisSoortenAsync();
Assert.True(lijst.Gelukt, $"de lijst komt binnen ({lijst.Melding})");
var server = lijst.Waarde!;
var ingebouwd = IngebouwdeSoorten.Lijst;
Assert.True(server.Select(s => (s.Id, s.Naam, s.BesprekenMet)).SequenceEqual(
ingebouwd.Select(s => (s.Id, s.Naam, s.BesprekenMet))),
"de ingebouwde lijst in de app is gelijk aan wat de server als startlijst heeft");
Assert.True(IngebouwdeSoorten.Naam(11) == "Paspoort" && IngebouwdeSoorten.Naam(4242) == "Overig",
"een onbekend nummer heet Overig");
});
runner.TestAsync("App: een eigen soort met velden, en de naam reist mee met het item", async () =>
{
await using var proef = new Proef();
var w = proef.Wereld;
await using var app = await proef.AangemeldeAppAsync("jan@example.nl");
var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Beheer");
w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true;
var soortId = (await w.Soorten.MaakAsync(beheerder.Id, "Niet reanimeren", "Alleen een papieren verklaring is leidend.", false)).Value;
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Wil je gereanimeerd worden?", "keuze",
"Ja\nNee\nAlleen na overleg", "Kies wat het beste past.", true);
await w.Soorten.VeldToevoegenAsync(beheerder.Id, soortId, "Datum verklaring", "datum", null, null, false);
var lijst = (await app.KluisSoortenAsync()).Waarde!;
var eigen = lijst.Single(s => s.Id == soortId);
Assert.True(!eigen.Ingebouwd && eigen.Uitleg!.Contains("papieren"), "de soort komt met zijn uitleg binnen");
Assert.True(eigen.Velden.Count == 2, "met twee velden");
Assert.True(eigen.Velden[0].Soort == KluisVeld.Keuze && eigen.Velden[0].Verplicht
&& eigen.Velden[0].Opties.SequenceEqual(new[] { "Ja", "Nee", "Alleen na overleg" }),
"het keuzeveld heeft zijn opties en is verplicht");
Assert.True(eigen.Velden[1].Soort == KluisVeld.Datum && !eigen.Velden[1].Verplicht, "het datumveld is niet verplicht");
var inhoud = new KluisInhoud
{
Velden = new() { ["wil_je_gereanimeerd_worden"] = "Nee", ["datum_verklaring"] = "2026-10-12" }
};
var opgeslagen = await app.KluisOpslaanAsync(soortId, "Mijn verklaring", inhoud, 0);
Assert.True(opgeslagen.Gelukt, $"het item wordt bewaard ({opgeslagen.Melding})");
var items = (await app.KluisAsync()).Waarde!;
var item = items.Single(i => i.Title == "Mijn verklaring");
Assert.True(item.Category == soortId, "het item staat in de nieuwe soort");
Assert.True(item.CategoryName == "Niet reanimeren", "en de naam reist met het item mee");
Assert.True(item.SoortNaam(IngebouwdeSoorten.Naam) == "Niet reanimeren", "ook zonder de lijst van de app");
var open = (await app.KluisOpenenAsync(item.Id)).Waarde!;
Assert.True(open.Velden["wil_je_gereanimeerd_worden"] == "Nee" && open.Velden["datum_verklaring"] == "2026-10-12",
"de velden komen er precies zo uit als ze erin gingen");
// Een ingebouwd item krijgt zijn ingebouwde naam, ook zonder CategoryName van de server.
var polis = new KluisItem(1, "Polis", 2, 0, null, DateTime.UtcNow);
Assert.True(polis.SoortNaam(IngebouwdeSoorten.Naam) == "Polis", "terugval op de ingebouwde naam");
});
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();
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();
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");
}
});
}
/// Een poortnummer waar gegarandeerd niets luistert.
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;
}
}