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; } }