using MijnBesluit.Application.Services; using MijnBesluit.Domain.Enums; using MijnBesluit.Application.Models; namespace MijnBesluit.SelfCheck; /// Inloggen en registreren met een code per e-mail. public static class InlogChecks { public static void Register(Runner runner) { runner.TestAsync("Inloggen: code aanvragen, code invullen, binnen", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); var aanvraag = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(aanvraag.Succeeded, "de code wordt verstuurd"); Assert.True(w.Email.Sent.Any(m => m.To == "jan@example.nl" && m.Subject.Contains("inlogcode")), "er gaat een e-mail uit met de code"); var code = w.LaatsteCodeUitMail("jan@example.nl"); Assert.True(code.Length == 6, "de code in de mail is zes cijfers"); var inloggen = await w.Accounts.LogInMetCodeAsync("jan@example.nl", code); Assert.True(inloggen.Succeeded, "met die code kom je binnen"); Assert.True(inloggen.Value!.Email == "jan@example.nl", "en wel als de juiste gebruiker"); }); runner.TestAsync("De code staat vooraan in het onderwerp, bij inloggen en bij registreren", async () => { // Zo lees je hem van je vergrendelscherm zonder de mail te openen. Staat hij // verderop, dan knipt de telefoon het onderwerp af vóór de code en is het nut weg. var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "inlogcode aanvragen lukt"); var inlogmail = w.Email.Sent.Last(m => m.To == "jan@example.nl"); var inlogcode = w.LaatsteCodeUitMail("jan@example.nl"); Assert.True(inlogmail.Subject.StartsWith(inlogcode), $"de inlogcode staat vooraan (onderwerp: {inlogmail.Subject})"); Assert.True((await w.Accounts.VraagRegistratiecodeAanAsync("nieuw@example.nl", "Nieuw", "Iemand")).Succeeded, "registratiecode aanvragen lukt"); var regmail = w.Email.Sent.Last(m => m.To == "nieuw@example.nl"); var regcode = w.LaatsteCodeUitMail("nieuw@example.nl"); Assert.True(regmail.Subject.StartsWith(regcode), $"de registratiecode ook (onderwerp: {regmail.Subject})"); }); runner.TestAsync("De code staat nooit leesbaar in de opslag", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var code = w.LaatsteCodeUitMail("jan@example.nl"); var opgeslagen = w.Store.Codes.Single(); Assert.True(!opgeslagen.CodeHash.Contains(code), "in de database staat alleen een hash"); Assert.True(opgeslagen.CodeHash.StartsWith("pbkdf2-sha512$"), "met het algoritme erbij"); }); runner.TestAsync("Een code werkt maar één keer", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var code = w.LaatsteCodeUitMail("jan@example.nl"); Assert.True((await w.Accounts.LogInMetCodeAsync("jan@example.nl", code)).Succeeded, "eerste keer lukt"); var tweede = await w.Accounts.LogInMetCodeAsync("jan@example.nl", code); Assert.True(!tweede.Succeeded, "een tweede keer niet"); Assert.True(tweede.Error!.Contains("verlopen of al gebruikt"), "met een begrijpelijke uitleg"); }); runner.TestAsync("Na vijf misgokken is de code onbruikbaar", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var echteCode = w.LaatsteCodeUitMail("jan@example.nl"); var fouteCode = echteCode == "000000" ? "111111" : "000000"; for (var poging = 1; poging <= 4; poging++) { var r = await w.Accounts.LogInMetCodeAsync("jan@example.nl", fouteCode); Assert.True(!r.Succeeded, $"poging {poging} wordt geweigerd"); Assert.True(r.Error!.Contains("poging"), "en er wordt gezegd hoeveel pogingen er nog zijn"); } var vijfde = await w.Accounts.LogInMetCodeAsync("jan@example.nl", fouteCode); Assert.True(vijfde.Error!.Contains("te vaak fout"), "de vijfde keer wordt de code weggegooid"); var metEchteCode = await w.Accounts.LogInMetCodeAsync("jan@example.nl", echteCode); Assert.True(!metEchteCode.Succeeded, "zelfs de echte code werkt daarna niet meer"); }); runner.TestAsync("Een code verloopt na een kwartier", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var code = w.LaatsteCodeUitMail("jan@example.nl"); w.Clock.Advance(TimeSpan.FromMinutes(14)); Assert.True((await w.Accounts.LogInMetCodeAsync("jan@example.nl", code)).Succeeded, "binnen het kwartier werkt hij nog"); var w2 = new World(); await w2.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w2.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var code2 = w2.LaatsteCodeUitMail("jan@example.nl"); w2.Clock.Advance(TimeSpan.FromMinutes(16)); Assert.True(!(await w2.Accounts.LogInMetCodeAsync("jan@example.nl", code2)).Succeeded, "daarna niet meer"); }); runner.TestAsync("Een nieuwe code maakt de vorige ongeldig", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var eerste = w.LaatsteCodeUitMail("jan@example.nl"); // Een mislukte poging mag de bestaande code niet ongeldig maken. w.Email.Mislukt = true; w.Clock.Advance(TimeSpan.FromSeconds(31)); Assert.True(!(await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "een aanvraag die niet verstuurd kan worden mislukt"); Assert.True((await w.Accounts.LogInMetCodeAsync("jan@example.nl", eerste)).Succeeded, "en de code van daarvoor werkt dan nog gewoon"); w.Email.Mislukt = false; // Opnieuw beginnen voor de eigenlijke controle. w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); eerste = w.LaatsteCodeUitMail("jan@example.nl"); w.Clock.Advance(TimeSpan.FromMinutes(1)); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var tweede = w.LaatsteCodeUitMail("jan@example.nl"); Assert.True(eerste != tweede, "er komt echt een andere code"); Assert.True(!(await w.Accounts.LogInMetCodeAsync("jan@example.nl", eerste)).Succeeded, "de oude code doet het niet meer"); Assert.True((await w.Accounts.LogInMetCodeAsync("jan@example.nl", tweede)).Succeeded, "de nieuwe wel"); }); runner.TestAsync("Te snel opnieuw aanvragen wordt afgeremd", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "de eerste mag"); var meteen = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(!meteen.Succeeded && meteen.Error!.Contains("Wacht nog"), "meteen daarna niet, met vermelding van de wachttijd"); w.Clock.Advance(TimeSpan.FromSeconds(31)); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "na een halve minuut weer wel"); }); runner.TestAsync("Niet meer dan vijf codes per uur", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); for (var i = 0; i < 5; i++) { Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, $"aanvraag {i + 1}"); w.Clock.Advance(TimeSpan.FromMinutes(1)); } var zesde = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(!zesde.Succeeded && zesde.Error!.Contains("te veel codes"), "de zesde wordt geweigerd"); w.Clock.Advance(TimeSpan.FromHours(1)); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "een uur later weer wel"); }); runner.TestAsync("Een code die je gebruikt telt niet mee voor de rem", async () => { // De rem is er tegen het volgooien van andermans postvak. Wie de code invult zit // zelf in dat postvak; dat is geen misbruik maar inloggen. Telden gebruikte codes // wel mee, dan sloot je jezelf buiten na vijf keer inloggen in een uur - en dat // gebeurde echt, op de dag dat de app om een andere reden bleef vragen om een code. var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); for (var i = 0; i < 5; i++) { var aanvraag = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(aanvraag.Succeeded, $"aanvraag {i + 1}"); // En elke keer ook echt inloggen, zoals iemand die het gewoon probeert. var inlog = await w.Accounts.LogInMetCodeAsync("jan@example.nl", w.LaatsteCodeUitMail("jan@example.nl")); Assert.True(inlog.Succeeded, $"inloggen {i + 1} ({inlog.Error})"); w.Clock.Advance(TimeSpan.FromMinutes(1)); } var zesde = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(zesde.Succeeded, "de zesde keer inloggen mag gewoon binnen hetzelfde uur"); }); runner.TestAsync("Een code stukraden zet de rem niet uit", async () => { // De keerzijde van de vorige controle. Een code wordt ook ongeldig gemaakt als hij // te vaak fout is ingevuld - en fout invullen kan iedereen, ook iemand die alleen // het adres kent. Telde zo'n code niet mee, dan kon een vreemde de rem eindeloos // leegtrekken en toch het postvak van een ander volgooien. Alleen echt binnenkomen // telt niet mee. var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); for (var i = 0; i < 5; i++) { Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, $"aanvraag {i + 1}"); // Een code die gegarandeerd niet klopt: de echte met het eerste cijfer opgehoogd. var echt = w.LaatsteCodeUitMail("jan@example.nl"); var fout = (echt[0] == '9' ? '0' : (char)(echt[0] + 1)) + echt[1..]; for (var poging = 0; poging < 6; poging++) await w.Accounts.LogInMetCodeAsync("jan@example.nl", fout); w.Clock.Advance(TimeSpan.FromMinutes(1)); } var zesde = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(!zesde.Succeeded && zesde.Error!.Contains("te veel codes"), "stukgeraden codes tellen gewoon mee"); }); runner.TestAsync("Onbekend e-mailadres krijgt geen code", async () => { var w = new World(); var r = await w.Accounts.VraagInlogcodeAanAsync("niemand@example.nl"); Assert.True(!r.Succeeded, "er wordt niets verstuurd"); Assert.True(r.Error!.Contains("Maak dan eerst een account aan"), "met een bruikbare aanwijzing"); Assert.True(w.Email.Sent.Count == 0, "en er gaat geen e-mail uit"); }); runner.TestAsync("Een onmogelijk e-mailadres wordt geweigerd", async () => { var w = new World(); foreach (var adres in new[] { "", "geen adres", "jan@", "@example.nl", "jan example.nl" }) { var r = await w.Accounts.VraagInlogcodeAanAsync(adres); Assert.True(!r.Succeeded && r.Error!.Contains("geldig e-mailadres"), $"'{adres}' wordt geweigerd"); } }); runner.TestAsync("Registreren gaat ook met een code", async () => { var w = new World(); var aanvraag = await w.Accounts.VraagRegistratiecodeAanAsync("nieuw@example.nl", "Nieuwe", "Deelnemer"); Assert.True(aanvraag.Succeeded, "de code wordt verstuurd"); Assert.True(w.Store.Users.Count == 0, "het account bestaat nog niet"); var code = w.LaatsteCodeUitMail("nieuw@example.nl"); var fout = await w.Accounts.RegistreerMetCodeAsync("nieuw@example.nl", code == "000000" ? "111111" : "000000"); Assert.True(!fout.Succeeded, "met een verkeerde code komt er geen account"); Assert.True(w.Store.Users.Count == 0, "en er is nog steeds niets aangemaakt"); var goed = await w.Accounts.RegistreerMetCodeAsync("nieuw@example.nl", code); Assert.True(goed.Succeeded, "met de juiste code wel"); var persoon = w.Store.People.Single(p => p.Email == "nieuw@example.nl"); Assert.True(persoon.FirstName == "Nieuwe" && persoon.LastName == "Deelnemer", "de naam uit de aanvraag is bewaard gebleven"); Assert.True(w.Store.Families.Any(f => f.OwnerPersonId == persoon.Id), "en er is een eigen familieproject"); Assert.True(persoon.ClaimedByUserId == goed.Value!.Id, "het profiel is meteen van de nieuwe gebruiker"); }); runner.TestAsync("Registreren op een bestaand adres wordt geweigerd", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); var r = await w.Accounts.VraagRegistratiecodeAanAsync("jan@example.nl", "Jan", "Jansen"); Assert.True(!r.Succeeded && r.Error!.Contains("bestaat al"), "je wordt naar inloggen gestuurd"); }); runner.TestAsync("Een inlogcode werkt niet om mee te registreren", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); var inlogcode = w.LaatsteCodeUitMail("jan@example.nl"); var r = await w.Accounts.RegistreerMetCodeAsync("jan@example.nl", inlogcode); Assert.True(!r.Succeeded, "een code voor inloggen kan niet voor registreren worden gebruikt"); }); runner.TestAsync("Een geblokkeerd account krijgt geen code", async () => { var w = new World(); var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); w.Store.Users.Single(u => u.Id == jan.Id).Status = UserStatus.Geblokkeerd; var r = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(!r.Succeeded && r.Error!.Contains("geen actief"), "de aanvraag wordt geweigerd"); Assert.True(w.Email.Sent.Count == 0, "en er gaat geen e-mail uit"); }); runner.TestAsync("Het adres in de e-mail wordt afgeschermd getoond", async () => { Assert.True(new CodeVerstuurd("jonathan@kwik-bit.nl", DateTime.UtcNow).AfgeschermdAdres == "j******n@kwik-bit.nl", "alleen de eerste en laatste letter blijven zichtbaar"); Assert.True(new CodeVerstuurd("jo@example.nl", DateTime.UtcNow).AfgeschermdAdres == "j@example.nl", "een heel kort adres wordt ook afgeschermd"); }); runner.TestAsync("Een onbereikbare mailserver laat niets kapot achter", async () => { var w = new World(); await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); w.Email.Mislukt = true; var r = await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl"); Assert.True(!r.Succeeded, "de aanvraag mislukt netjes in plaats van met een uitzondering"); Assert.True(r.Error!.Contains("niet versturen"), "met een melding die je kunt tonen"); Assert.True(w.Store.Codes.Count == 0, "er blijft geen code achter die niemand ooit te zien krijgt"); // Een mislukte poging telt ook niet mee voor de rem op aanvragen per uur. Assert.True(w.Store.Codes.Count == 0, "en de teller van aanvragen loopt er niet door vol"); // En zodra de mailserver het weer doet, werkt het gewoon. w.Email.Mislukt = false; w.Clock.Advance(TimeSpan.FromSeconds(31)); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("jan@example.nl")).Succeeded, "daarna lukt het weer"); }); runner.TestAsync("Een grote foto wordt verkleind opgeslagen", async () => { var wereld = new World(); var jan = await wereld.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); wereld.Verkleiner.Aan = true; // Ruim boven de grens, zodat het verkleinen aan de beurt is. var groot = new byte[2 * 1024 * 1024]; Random.Shared.NextBytes(groot); var opgeslagen = await wereld.Vault.StoreFileAsync(jan.Id, "vakantie.png", "image/png", groot); Assert.True(opgeslagen.Succeeded, $"opslaan lukt ({opgeslagen.Error})"); var bestand = opgeslagen.Value!; Assert.True(bestand.SizeBytes == wereld.Verkleiner.Uitkomst.Length, "de kleine versie is bewaard"); Assert.True(bestand.ContentType == "image/jpeg", "en het soort klopt met wat er staat"); Assert.True(bestand.FileName == "vakantie.jpg", $"de naam belooft geen png meer (is '{bestand.FileName}')"); // De maat waarop verkleind is, is die van de foto zelf en niet die van het // pasfotootje. Die twee door elkaar halen levert een kluis vol postzegels op. Assert.True(wereld.Verkleiner.Aanroepen.Any(a => a.MaxZijde == VaultService.FotoMaxZijde), "er is verkleind op de maat voor foto's"); Assert.True(wereld.Verkleiner.Aanroepen.Any(a => a.MaxZijde == VaultService.ThumbnailZijde), "en er is een kleine versie gemaakt op de maat daarvoor"); // Byte voor byte terug te lezen: verkleinen mag het bestand veranderen, niet stuk maken. var terug = await wereld.Vault.OpenFileAsync(bestand.Id, jan.Id); Assert.True(terug.Succeeded, $"ophalen lukt ({terug.Error})"); Assert.True(terug.Value.Inhoud.SequenceEqual(wereld.Verkleiner.Uitkomst), "en levert precies op wat er is opgeslagen"); }); runner.TestAsync("Een foto zonder kleine versie krijgt er alsnog een", async () => { var wereld = new World(); var jan = await wereld.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); var familie = wereld.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId); var eva = await wereld.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen", null, RelationType.Dochter); Assert.True(eva.Succeeded, $"Eva toevoegen lukt ({eva.Error})"); // Opslaan zonder verkleiner: zo zag elke foto eruit voordat de kleine versies er waren. wereld.Verkleiner.Aan = false; var foto = await wereld.Vault.StoreFileAsync(jan.Id, "eva.jpg", "image/jpeg", new byte[] { 9, 9, 9, 9 }, subjectPersonId: eva.Value!.Id); Assert.True(foto.Succeeded, $"de foto opslaan lukt ({foto.Error})"); Assert.True(!foto.Value!.HeeftThumbnail, "en hij heeft geen kleine versie"); Assert.True(foto.Value.Thumbnail is null, "er is ook nooit een poging gedaan"); Assert.True((await wereld.Vault.SetProfilePhotoAsync(foto.Value.Id, jan.Id)).Succeeded, "aanwijzen als profielfoto lukt"); // Vanaf hier is er wél een verkleiner - zoals op een server na de uitrol. wereld.Verkleiner.Aan = true; var fotos = await wereld.Vault.ProfielfotosAsync(new List { eva.Value.Id }, jan.Id); Assert.True(fotos.Count() == 1, "de profielfoto komt terug"); Assert.True(fotos[0].Klein is not null, "en nu mét kleine versie"); Assert.True(fotos[0].Klein!.SequenceEqual(wereld.Verkleiner.Uitkomst), "die klopt met wat er is gemaakt"); // En hij is bewaard, dus de volgende keer hoeft de foto niet opnieuw open. Assert.True(foto.Value.HeeftThumbnail, "de kleine versie staat nu bij het bestand"); var aantalVoor = wereld.Verkleiner.Aanroepen.Count; var opnieuw = await wereld.Vault.ProfielfotosAsync(new List { eva.Value.Id }, jan.Id); Assert.True(opnieuw[0].Klein is not null, "de tweede keer komt hij er ook"); Assert.True(wereld.Verkleiner.Aanroepen.Count == aantalVoor, "en dan is er niet opnieuw verkleind"); }); runner.TestAsync("Een foto die niet te verkleinen is wordt niet elke keer opnieuw geprobeerd", async () => { var wereld = new World(); var jan = await wereld.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); var familie = wereld.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId); var eva = await wereld.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen", null, RelationType.Dochter); wereld.Verkleiner.Aan = false; var foto = await wereld.Vault.StoreFileAsync(jan.Id, "eva.jpg", "image/jpeg", new byte[] { 9, 9, 9, 9 }, subjectPersonId: eva.Value!.Id); await wereld.Vault.SetProfilePhotoAsync(foto.Value!.Id, jan.Id); // De verkleiner blijft uit: op een server waar het verkleinen niet werkt hoort de // lijst niet bij elke keer opnieuw elke foto open te maken. var eerste = await wereld.Vault.ProfielfotosAsync(new List { eva.Value.Id }, jan.Id); Assert.True(eerste[0].Klein is null, "er komt geen kleine versie"); Assert.True(foto.Value.Thumbnail is { Length: 0 }, "maar de poging is wel vastgelegd"); Assert.True(!foto.Value.HeeftThumbnail, "en dat telt niet als een kleine versie"); // Nu mét verkleiner: er is al een poging geweest, dus er wordt niets meer gemaakt. // Wie dat toch wil, voegt de foto opnieuw toe. wereld.Verkleiner.Aan = true; var tweede = await wereld.Vault.ProfielfotosAsync(new List { eva.Value.Id }, jan.Id); Assert.True(tweede[0].Klein is null, "en er wordt niet nog eens geprobeerd"); Assert.True(tweede[0].BestandId == foto.Value.Id, "het nummer van de foto komt wel gewoon mee"); }); runner.TestAsync("Een klein bestand en een document blijven ongemoeid", async () => { var wereld = new World(); var jan = await wereld.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen"); wereld.Verkleiner.Aan = true; // Onder de grens: verkleinen zou hier meer kwaliteit kosten dan het oplevert. var klein = new byte[10 * 1024]; Random.Shared.NextBytes(klein); var foto = await wereld.Vault.StoreFileAsync(jan.Id, "pasfoto.png", "image/png", klein); Assert.True(foto.Value!.SizeBytes == klein.Length, "een kleine foto blijft zoals hij is"); Assert.True(foto.Value.ContentType == "image/png", "en houdt zijn eigen soort"); Assert.True(foto.Value.FileName == "pasfoto.png", "en zijn eigen naam"); // Een pdf is geen afbeelding; daar valt niets te verkleinen en er hoort ook niets // aan de naam of het soort te veranderen. var groot = new byte[2 * 1024 * 1024]; Random.Shared.NextBytes(groot); var document = await wereld.Vault.StoreFileAsync(jan.Id, "testament.pdf", "application/pdf", groot); Assert.True(document.Value!.SizeBytes == groot.Length, "een pdf blijft volledig bewaard"); Assert.True(document.Value.ContentType == "application/pdf", "met zijn eigen soort"); Assert.True(document.Value.FileName == "testament.pdf", "en zijn eigen naam"); Assert.True(!document.Value.HeeftThumbnail, "en zonder kleine versie"); }); runner.TestAsync("Overlijden: van melding tot de beheerder die de kluis opent", async () => { var w = new World(); // Opa legt vast wat er moet gebeuren en wijst zijn zoon aan als beheerder. var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var geheim = await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { BurialOrCremation = "Crematie", Music = "Adagio" }, ReleasePolicy.NaOverlijden); Assert.True(geheim.Succeeded, $"de uitvaartwens staat in de kluis ({geheim.Error})"); var prive = await w.Vault.CreateAsync(opa.Id, VaultCategory.Wachtwoord, "Mijn e-mail", new VaultPayload { Secret = "alleen voor mij" }, ReleasePolicy.AlleenIkzelf); Assert.True(prive.Succeeded, "en een item dat privé blijft"); // Twee kinderen die hun uitnodiging hebben aangenomen; zij kunnen straks melden. var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var els = await w.MaakFamilielidAsync(opa, "Els", "Flietstra", "els@example.nl"); // Els wijst hij ook aan, anders mag zij straks niet melden. Truus niet: zij is er om // te laten zien dat familie zijn op zichzelf niets oplevert. var truus = await w.MaakFamilielidAsync(opa, "Truus", "Flietstra", "truus@example.nl"); await w.BeheerderAsync(opa, els); Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId, 2)).Succeeded, "Jan is aangewezen als beheerder"); // Twee meldingen zijn genoeg; standaard zijn er drie nodig, dus opa zet het op twee. Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 2, 14)).Succeeded, "twee meldingen en veertien dagen"); var eerste = await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); Assert.True(eerste.Succeeded, $"Jan kan melden ({eerste.Error})"); Assert.True(eerste.Value.State == DeathProcedureState.Lopend, "de procedure loopt"); Assert.True(eerste.Value.Missing == 1, "er is er nog één nodig"); var nogmaals = await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); Assert.True(!nogmaals.Succeeded, "dezelfde persoon telt maar één keer"); var tweede = await w.Legacy.DeclareDeathAsync(opa.PersonId, els.Id); Assert.True(tweede.Value.Missing == 0, "met Els erbij is het aantal gehaald"); Assert.True(tweede.Value.State == DeathProcedureState.Lopend, "maar de bezwaartermijn loopt nog"); Assert.True(w.Email.Sent.Any(m => m.To == "opa@example.nl" && m.Subject.Contains("Belangrijk")), "opa krijgt bericht dat hij bezwaar kan maken"); // Zolang de termijn loopt is er niets vrijgegeven. Assert.True(!(await w.Vault.OpenAsync(geheim.Value!.Id, jan.Id)).Succeeded, "Jan kan er tijdens de bezwaartermijn nog niet bij"); // Hij ziet wel dat hij is aangewezen - dat weet hij al sinds opa hem vroeg - maar // niet dat er meldingen zijn. Een lopende procedure ziet er hier precies zo uit als // iemand die springlevend is; anders kon hij het aan dit scherm aflezen. var lopend = await w.Legacy.ToevertrouwdAsync(jan.Id); Assert.True(lopend.Count == 1, "de aanwijzing staat er wel"); Assert.True(!lopend[0].Afgerond, "maar niet als afgeronde nalatenschap"); Assert.True(lopend[0].AantalItems == 0 && lopend[0].DateOfDeath is null, "en zonder aantallen of datum"); // Opa reageert niet. Na veertien dagen doet de achtergrondtaak zijn werk. w.Clock.Advance(TimeSpan.FromDays(15)); Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 1, "de procedure wordt afgerond"); Assert.True((await w.Legacy.StatusAsync(opa.PersonId)).State == DeathProcedureState.Bevestigd, "het overlijden staat als bevestigd"); // En nu komt Jan erbij - dit is het sluitstuk waar het allemaal om draait. var toevertrouwd = await w.Legacy.ToevertrouwdAsync(jan.Id); Assert.True(toevertrouwd.Count == 1, "Jan ziet de nalatenschap van zijn vader staan"); Assert.True(toevertrouwd[0].Afgerond, "nu wel als afgerond"); Assert.True(toevertrouwd[0].AantalItems == 1, "met alleen wat is vrijgegeven"); var items = await w.Legacy.NalatenschapItemsAsync(opa.PersonId, jan.Id); Assert.True(items.Succeeded, $"hij kan de lijst opvragen ({items.Error})"); Assert.True(items.Value!.Single().Id == geheim.Value.Id, "en dat is de uitvaartwens"); var geopend = await w.Vault.OpenAsync(geheim.Value.Id, jan.Id); Assert.True(geopend.Succeeded, $"en hem openen ({geopend.Error})"); Assert.True(geopend.Value!.Music == "Adagio", "de inhoud klopt"); // Wat privé was, blijft dicht. Ook voor de beheerder. Assert.True(!(await w.Vault.OpenAsync(prive.Value!.Id, jan.Id)).Succeeded, "het privé-item blijft ook nu gesloten"); // Truus staat wel in de familie maar is niet aangewezen. Zij ziet niets en kan niets. Assert.True((await w.Legacy.ToevertrouwdAsync(truus.Id)).Count == 0, "wie niet is aangewezen beheert niets en ziet niets"); Assert.True(!(await w.Legacy.NalatenschapItemsAsync(opa.PersonId, truus.Id)).Succeeded, "en kan de lijst niet opvragen"); Assert.True(!(await w.Vault.OpenAsync(geheim.Value.Id, truus.Id)).Succeeded, "en komt ook niet in de kluis"); }); runner.TestAsync("Wie is aangewezen ziet dat meteen, ook al leeft de ander gewoon", async () => { // Zonder dit weet een beheerder pas dat hij is gevraagd als het al zover is. Hij // hoort het te zien staan, met zijn volgorde erbij, en er verder niets mee kunnen. var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var els = await w.MaakFamilielidAsync(opa, "Els", "Flietstra", "els@example.nl"); var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { Music = "Adagio" }, ReleasePolicy.NaOverlijden)).Value!; Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, els.PersonId, 1)).Succeeded, "Els is eerste"); Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId, 2)).Succeeded, "Jan is tweede"); var bijJan = await w.Legacy.ToevertrouwdAsync(jan.Id); Assert.True(bijJan.Count == 1, "Jan ziet de aanwijzing van zijn vader staan"); Assert.True(bijJan[0].DisplayName == "Opa Flietstra", "met de naam erbij"); Assert.True(bijJan[0].Volgorde == 2, "en zijn volgorde"); Assert.True(!bijJan[0].Afgerond, "de procedure is niet afgerond"); var bijEls = await w.Legacy.ToevertrouwdAsync(els.Id); Assert.True(bijEls.Single().Volgorde == 1, "Els ziet dat zij als eerste aan zet is"); // Zien dat je bent gevraagd is iets anders dan ergens bij kunnen. Assert.True(!(await w.Legacy.NalatenschapItemsAsync(opa.PersonId, jan.Id)).Succeeded, "maar er valt niets op te vragen"); Assert.True(!(await w.Vault.OpenAsync(wens.Id, jan.Id)).Succeeded, "en de kluis blijft dicht"); }); runner.TestAsync("Een beheerder is weer weg te halen, behalve als hij de kluis al heeft", async () => { // Aanwijzen zonder kunnen terugnemen is geen keuze. Dit is het geval waar het om // gaat: een beheerder overlijdt zelf, en er moet iemand anders komen. var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var els = await w.MaakFamilielidAsync(opa, "Els", "Flietstra", "els@example.nl"); await w.BeheerderAsync(opa, jan); Assert.True((await w.Legacy.RemoveManagerAsync(opa.Id, jan.PersonId)).Succeeded, "Jan gaat eruit"); Assert.True((await w.Legacy.ListManagersAsync(opa.PersonId)).Count == 0, "en staat er niet meer"); // Hij ziet de aanwijzing ook niet meer bij zichzelf staan. Assert.True((await w.Legacy.ToevertrouwdAsync(jan.Id)).Count == 0, "Jan is niets meer toevertrouwd"); // En er kan een ander voor in de plaats. await w.BeheerderAsync(opa, els); Assert.True((await w.Legacy.ListManagersAsync(opa.PersonId)).Single().PersonId == els.PersonId, "Els beheert nu"); // Iemand die er niet staat weghalen zegt dat gewoon, in plaats van te doen alsof. Assert.True(!(await w.Legacy.RemoveManagerAsync(opa.Id, jan.PersonId)).Succeeded, "Jan weghalen kan geen tweede keer"); // En wie de kluis al heeft gekregen kun je niet meer terugdraaien: hij heeft gezien // wat erin stond, en de administratie opschonen verandert dat niet. Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 1)).Succeeded, "één melding volstaat"); Assert.True((await w.Legacy.DeclareDeathAsync(opa.PersonId, els.Id)).Succeeded, "Els meldt het overlijden"); w.Clock.Advance(TimeSpan.FromDays(2)); Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 1, "de procedure wordt afgerond"); var nu = (await w.Legacy.ListManagersAsync(opa.PersonId)).Single(); Assert.True(nu.AccessGrantedAt is not null, "Els heeft de kluis gekregen"); Assert.True(!(await w.Legacy.RemoveManagerAsync(opa.Id, els.PersonId)).Succeeded, "en dan is weghalen niet meer aan de orde"); }); runner.TestAsync("Ten onrechte dood verklaard: de link uit de e-mail is de weg terug", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { Music = "Adagio" }, ReleasePolicy.NaOverlijden)).Value!; Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId)).Succeeded, "Jan beheert"); Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 14)).Succeeded, "één melding volstaat"); await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); // De link zit in de waarschuwingsmail; die halen we eruit zoals opa hem zou aanklikken. var mail = w.Email.Sent.Last(m => m.To == "opa@example.nl"); var link = System.Text.RegularExpressions.Regex.Match(mail.Body, @"/leefnog/([a-zA-Z0-9_-]+)"); Assert.True(link.Success, "er staat een bezwaarlink in de e-mail"); var token = link.Groups[1].Value; // Opa merkt de mail pas op als het al te laat is: de termijn is voorbij, het // overlijden is bevestigd en zijn account staat op overleden. w.Clock.Advance(TimeSpan.FromDays(20)); Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 1, "het wordt bevestigd"); Assert.True(w.Store.Users.Single(u => u.Id == opa.Id).Status == UserStatus.Overleden, "zijn account staat op overleden"); Assert.True((await w.Vault.OpenAsync(wens.Id, jan.Id)).Succeeded, "Jan kan bij de uitvaartwens"); // Inloggen kan niet meer - precies de doodlopende weg waar dit voor is. Assert.True(!(await w.Accounts.VraagInlogcodeAanAsync("opa@example.nl")).Succeeded, "inloggen lukt niet meer, dus bezwaar maken via het scherm ook niet"); // De link werkt nog wel, en die draait alles terug. var terug = await w.Legacy.BezwaarMetTokenAsync(token); Assert.True(terug.Succeeded, $"de link werkt ({terug.Error})"); Assert.True(terug.Value == "Opa Flietstra", "en noemt wie het betreft"); Assert.True(w.Store.Users.Single(u => u.Id == opa.Id).Status == UserStatus.Actief, "zijn account is weer actief"); Assert.True(w.Store.People.Single(p => p.Id == opa.PersonId).DeathConfirmedAt is null, "de bevestiging is weg"); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("opa@example.nl")).Succeeded, "en hij kan weer inloggen"); // En Jan is zijn toegang kwijt. Dat is het punt waar het op aankomt: zonder dit // houdt de beheerder de sleutels van iemand die gewoon leeft. Assert.True(!(await w.Vault.OpenAsync(wens.Id, jan.Id)).Succeeded, "Jan kan er niet meer bij"); var na = await w.Legacy.ToevertrouwdAsync(jan.Id); Assert.True(na.Single().Afgerond == false, "en de nalatenschap staat niet meer als afgerond bij hem"); // De link is eenmalig. Assert.True(!(await w.Legacy.BezwaarMetTokenAsync(token)).Succeeded, "dezelfde link werkt daarna niet nog een keer"); }); runner.TestAsync("De beheerder draait een onterecht overlijden terug", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { Music = "Adagio" }, ReleasePolicy.NaOverlijden)).Value!; await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId); await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 1); await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); w.Clock.Advance(TimeSpan.FromDays(2)); await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }); // De bezwaarlink is inmiddels verlopen; opa kan er zelf niet meer uit komen. w.Clock.Advance(LegacyService.BezwaarlinkGeldigheid + TimeSpan.FromDays(1)); Assert.True((await w.Vault.OpenAsync(wens.Id, jan.Id)).Succeeded, "Jan heeft toegang"); Assert.True(!(await w.Accounts.VraagInlogcodeAanAsync("opa@example.nl")).Succeeded, "en opa kan niet meer inloggen"); var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Beheer", "Der"); w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true; var gevonden = await w.Beheer.ZoekOpAdresAsync("opa@example.nl", beheerder.Id); Assert.True(gevonden.Succeeded, $"de beheerder vindt het profiel ({gevonden.Error})"); Assert.True(gevonden.Value!.AccountGeblokkeerd, "en ziet dat het account op overleden staat"); Assert.True(gevonden.Value.OverlijdenBevestigdOp is not null, "en dat het is bevestigd"); Assert.True(!(await w.Beheer.HerstelOverlijdenAsync(opa.PersonId, beheerder.Id, "x")).Succeeded, "zonder behoorlijke reden gaat het niet"); var hersteld = await w.Beheer.HerstelOverlijdenAsync( opa.PersonId, beheerder.Id, "telefonisch gesproken, leeft gewoon"); Assert.True(hersteld.Succeeded, $"met een reden wel ({hersteld.Error})"); Assert.True((await w.Accounts.VraagInlogcodeAanAsync("opa@example.nl")).Succeeded, "opa kan weer inloggen"); Assert.True(!(await w.Vault.OpenAsync(wens.Id, jan.Id)).Succeeded, "en Jan is zijn toegang kwijt"); // De reden staat in het logboek; dit is de zwaarste knop in de applicatie. Assert.True(w.Store.Audit.Any(a => a.UserId == beheerder.Id && (a.Details ?? "").Contains("telefonisch gesproken")), "de reden is vastgelegd bij de beheerder"); }); runner.TestAsync("Een beheerder kan geen kluis openen en niet op naam zoeken", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var geheim = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Wachtwoord, "Bank", new VaultPayload { Secret = "hier gaat het om" }, ReleasePolicy.NaOverlijden)).Value!; var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Beheer", "Der"); w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true; // Dit is de kern: beheerder zijn geeft geen enkele toegang tot een kluis. Assert.True(!(await w.Vault.OpenAsync(geheim.Id, beheerder.Id)).Succeeded, "een beheerder kan een kluisitem niet openen"); Assert.True((await w.Vault.ListAsync(beheerder.PersonId)).Count == 0, "en ziet niets in zijn eigen lijst staan"); // Zoeken kan alleen op het volledige adres. Een deel ervan of een naam levert niets op. Assert.True(!(await w.Beheer.ZoekOpAdresAsync("opa", beheerder.Id)).Succeeded, "zoeken op een deel van het adres levert niets op"); Assert.True(!(await w.Beheer.ZoekOpAdresAsync("Flietstra", beheerder.Id)).Succeeded, "en op naam ook niet"); Assert.True((await w.Beheer.ZoekOpAdresAsync("opa@example.nl", beheerder.Id)).Succeeded, "met het volledige adres wel"); }); runner.TestAsync("Wie geen beheerder is komt er niet in", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); await w.BeheerderAsync(opa, jan); await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 1); await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); w.Clock.Advance(TimeSpan.FromDays(2)); await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }); Assert.True(!await w.Beheer.IsBeheerderAsync(jan.Id), "Jan is geen beheerder"); Assert.True(!(await w.Beheer.ZoekOpAdresAsync("opa@example.nl", jan.Id)).Succeeded, "en kan niets opzoeken"); Assert.True(!(await w.Beheer.HerstelOverlijdenAsync(opa.PersonId, jan.Id, "omdat het kan")).Succeeded, "en zeker niets terugdraaien"); Assert.True(w.Store.People.Single(p => p.Id == opa.PersonId).DeathConfirmedAt is not null, "er is niets veranderd"); }); runner.TestAsync("Een bezwaarlink die niet klopt of verlopen is doet niets", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); await w.BeheerderAsync(opa, jan); Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 14)).Succeeded, "één melding volstaat"); await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); var mail = w.Email.Sent.Last(m => m.To == "opa@example.nl"); var token = System.Text.RegularExpressions.Regex .Match(mail.Body, @"/leefnog/([a-zA-Z0-9_-]+)").Groups[1].Value; Assert.True(!(await w.Legacy.BezwaarMetTokenAsync("verzonnen")).Succeeded, "een verzonnen link doet niets"); Assert.True(!(await w.Legacy.BezwaarMetTokenAsync("")).Succeeded, "een lege link ook niet"); Assert.True((await w.Legacy.StatusAsync(opa.PersonId)).ActiveDeclarations == 1, "en de melding staat er nog gewoon"); // Na een jaar houdt het op. Dan is er geen automatische weg terug meer, en dat hoort // ook zo: een link die eeuwig blijft werken is een sleutel die eeuwig rondslingert. w.Clock.Advance(LegacyService.BezwaarlinkGeldigheid + TimeSpan.FromDays(1)); var verlopen = await w.Legacy.BezwaarMetTokenAsync(token); Assert.True(!verlopen.Succeeded, "een verlopen link werkt niet meer"); Assert.True(verlopen.Error!.Contains("verlopen"), "met een uitleg waarom"); }); runner.TestAsync("Zonder melding van een beheerder komt de procedure niet op gang", async () => { // Dit is de kern van de regel. Melden mag iedereen uit je kring - anders haal je het // vereiste aantal nooit - maar de bezwaartermijn begint pas te lopen als er een // melding bij zit van iemand die de betrokkene zélf heeft aangewezen. var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var vreemde = await w.MaakGebruikerAsync("piet@example.nl", "Piet", "Pietersen"); var stiekem = await w.Legacy.DeclareDeathAsync(opa.PersonId, vreemde.Id); Assert.True(!stiekem.Succeeded, "een vreemde kan geen overlijden melden"); Assert.True(stiekem.Error!.Contains("familie"), "met een uitleg waarom"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var els = await w.MaakFamilielidAsync(opa, "Els", "Flietstra", "els@example.nl"); var truus = await w.MaakFamilielidAsync(opa, "Truus", "Flietstra", "truus@example.nl"); // Alleen Truus is aangewezen. Jan en Els zijn gewoon familie. await w.BeheerderAsync(opa, truus); Assert.True((await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id)).Succeeded, "familie mag melden"); var tweede = await w.Legacy.DeclareDeathAsync(opa.PersonId, els.Id); Assert.True(tweede.Succeeded, "en de tweede ook"); // Nu nog één nodig voor het standaardaantal van drie. Assert.True(tweede.Value.Missing == 1, "er is er nog één nodig"); // Een derde niet-beheerder erbij: het aantal is gehaald, maar er gebeurt niets. var vierde = await w.MaakFamilielidAsync(opa, "Kees", "Flietstra", "kees@example.nl"); var derde = (await w.Legacy.DeclareDeathAsync(opa.PersonId, vierde.Id)).Value; Assert.True(derde.Missing == 0, "het aantal is gehaald"); Assert.True(derde.WachtOpBeheerder, "maar er wacht nog een beheerder"); Assert.True(derde.ObjectionDeadline is null, "er loopt geen termijn"); Assert.True(!w.Email.Sent.Any(m => m.To == "opa@example.nl" && m.Subject.Contains("melding")), "en opa krijgt geen schrikmail over een procedure die stilstaat"); // Ook na maanden wachten gebeurt er niets. w.Clock.Advance(TimeSpan.FromDays(120)); Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 0, "en er wordt niets bevestigd"); Assert.True(w.Store.People.Single(p => p.Id == opa.PersonId).DeathConfirmedAt is null, "opa staat nog gewoon als levend"); // Meldt Truus, dan begint hij alsnog. var metBeheerder = (await w.Legacy.DeclareDeathAsync(opa.PersonId, truus.Id)).Value; Assert.True(metBeheerder.BeheerderHeeftGemeld, "nu is er een beheerder bij"); Assert.True(!metBeheerder.WachtOpBeheerder, "dus wordt er niet meer gewacht"); Assert.True(metBeheerder.ObjectionDeadline is not null, "en loopt de bezwaartermijn"); Assert.True(w.Email.Sent.Any(m => m.To == "opa@example.nl" && m.Subject.Contains("melding")), "nu krijgt opa wel bericht"); }); runner.TestAsync("De bezwaartermijn is minstens één dag", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); // Nul dagen zou betekenen: bevestigd op het moment dat de laatste melding binnenkomt. // Dan is er geen termijn meer om iets tegen te spreken, en dat is de enige // bescherming die de betrokkene heeft. var nul = await w.Legacy.WijzigProcedureAsync(opa.Id, 3, 0); Assert.True(!nul.Succeeded, "nul dagen wordt geweigerd"); Assert.True(nul.Error!.Contains("1 en 90"), "met de grenzen erbij"); Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 3, 1)).Succeeded, "één dag mag"); Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 3, 90)).Succeeded, "negentig dagen ook"); Assert.True(!(await w.Legacy.WijzigProcedureAsync(opa.Id, 3, 91)).Succeeded, "eenennegentig niet"); // Elke waarde ertussenin mag; het is geen keuzelijstje. foreach (var dagen in new[] { 3, 14, 21, 45, 67 }) Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 3, dagen)).Succeeded, $"{dagen} dagen mag"); Assert.True(w.Store.People.Single(p => p.Id == opa.PersonId).DeathObjectionDays == 67, "en de laatste waarde staat er"); }); runner.TestAsync("Bezwaar maken sluit je niet voorgoed uit van de procedure", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var familie = w.Store.Families.Single(f => f.OwnerPersonId == opa.PersonId); var profiel = (await w.Families.AddMemberAsync(familie.Id, opa.Id, "Jan", "Flietstra", "jan@example.nl", RelationType.Kind)).Value!; var token = (await w.Families.InviteAsync(familie.Id, profiel.Id, opa.Id)).Value!; var jan = (await w.Families.AcceptInvitationAsync(token, w.Accounts)).Value!; await w.BeheerderAsync(opa, jan); Assert.True((await w.Legacy.WijzigProcedureAsync(opa.Id, 1, 14)).Succeeded, "één melding volstaat"); await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); Assert.True((await w.Legacy.ObjectAsync(opa.PersonId, opa.Id)).Succeeded, "opa maakt bezwaar"); Assert.True((await w.Legacy.StatusAsync(opa.PersonId)).State == DeathProcedureState.Betwist, "de procedure staat op betwist"); // Twee jaar later overlijdt opa alsnog. Het bezwaar van toen mag dat niet blokkeren. w.Clock.Advance(TimeSpan.FromDays(730)); var opnieuw = await w.Legacy.DeclareDeathAsync(opa.PersonId, jan.Id); Assert.True(opnieuw.Succeeded, $"er kan opnieuw gemeld worden ({opnieuw.Error})"); Assert.True(opnieuw.Value.State == DeathProcedureState.Lopend, "en de procedure loopt weer, niet betwist"); w.Clock.Advance(TimeSpan.FromDays(15)); Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 1, "en wordt gewoon afgerond"); }); runner.TestAsync("Twee ouders, één dochter: de tweede uitnodiging voegt samen", async () => { var w = new World(); // Allebei de ouders maken hun eigen aantekening over dezelfde dochter. Dat is geen // fout: het zijn twee eerlijke aantekeningen van degene die ze maakte. var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var vaderFamilie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var bijVader = (await w.Families.AddMemberAsync(vaderFamilie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var moeder = await w.MaakGebruikerAsync("karin@example.nl", "Karin", "Ophuis"); var moederFamilie = w.Store.Families.Single(f => f.OwnerPersonId == moeder.PersonId); var bijMoeder = (await w.Families.AddMemberAsync(moederFamilie.Id, moeder.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; Assert.True(bijVader.Id != bijMoeder.Id, "het zijn twee losse profielen"); // Moeder zet alvast een bericht en een foto bij háár profiel van Ysabeau. var bericht = await w.Messages.ComposeAsync(moeder.Id, bijMoeder.Id, "Voor jou", "Lief kind", null, true); Assert.True(bericht.Succeeded, $"het bericht staat klaar ({bericht.Error})"); var foto = await w.Vault.StoreFileAsync(moeder.Id, "ysabeau.jpg", "image/jpeg", new byte[] { 1, 2, 3 }, subjectPersonId: bijMoeder.Id); Assert.True(foto.Succeeded, $"de foto staat erbij ({foto.Error})"); // Ysabeau neemt de uitnodiging van haar vader aan en krijgt daarmee haar account. var tokenVader = (await w.Families.InviteAsync(vaderFamilie.Id, bijVader.Id, vader.Id)).Value!; var ysabeau = (await w.Families.AcceptInvitationAsync(tokenVader, w.Accounts)).Value!; Assert.True(ysabeau.PersonId == bijVader.Id, "haar account hangt aan het profiel van haar vader"); // En dan die van haar moeder. Niet ingelogd zou dat een tweede account worden, dus // dat weigeren we met een uitleg in plaats van er stilzwijgend een te maken. var tokenMoeder = (await w.Families.InviteAsync(moederFamilie.Id, bijMoeder.Id, moeder.Id)).Value!; var zonderInloggen = await w.Families.AcceptInvitationAsync(tokenMoeder, w.Accounts); Assert.True(!zonderInloggen.Succeeded, "zonder inloggen wordt de tweede uitnodiging geweigerd"); Assert.True(zonderInloggen.Error!.Contains("Log eerst in"), "met de uitleg waarom"); // Ingelogd wél: dan zegt zíj dat het om haar gaat. var samen = await w.Families.AcceptInvitationAsync(tokenMoeder, w.Accounts, default, ysabeau.Id, w.Samenvoegen); Assert.True(samen.Succeeded, $"ingelogd lukt het wel ({samen.Error})"); Assert.True(samen.Value!.Id == ysabeau.Id, "en er komt geen tweede account bij"); // Vanaf hier is er nog één Ysabeau. Assert.True(bijMoeder.MergedIntoPersonId == bijVader.Id, "het duplicaat wijst naar waar hij in opging"); var bijMoederNu = await w.Families.ListMembersAsync(moederFamilie.Id, moeder.Id); var dochterVanMoeder = bijMoederNu.Single(l => l.PersonId != moeder.PersonId); Assert.True(dochterVanMoeder.PersonId == bijVader.Id, "moeder ziet nu dezelfde Ysabeau"); Assert.True(dochterVanMoeder.Relation == RelationType.Dochter, "en nog steeds als dochter"); Assert.True(dochterVanMoeder.State == ProfileState.Geclaimd, "die haar profiel nu zelf beheert"); var bijVaderNu = await w.Families.ListMembersAsync(vaderFamilie.Id, vader.Id); Assert.True(bijVaderNu.Single(l => l.PersonId != vader.PersonId).PersonId == bijVader.Id, "vader ziet haar ook nog gewoon"); // Wat moeder had klaargezet is meeverhuisd, niet kwijtgeraakt. Assert.True(w.Store.Messages.Single().ToPersonId == bijVader.Id, "het bericht van moeder staat nu klaar voor de echte Ysabeau"); Assert.True(w.Store.VaultFiles.Single().SubjectPersonId == bijVader.Id, "en de foto hangt bij haar"); }); runner.TestAsync("Een samenvoeging is terug te draaien; alles staat daarna weer waar het stond", async () => { // Dezelfde situatie als hierboven, maar nu blijkt het mis: Ysabeau heeft de uitnodiging // van iemand anders aangenomen. Het duplicaat is dan van een ander, en alles wat die // ander had klaargezet staat nu bij haar. Dat moet terug kunnen. var w = new World(); var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var vaderFamilie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var bijVader = (await w.Families.AddMemberAsync(vaderFamilie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var moeder = await w.MaakGebruikerAsync("karin@example.nl", "Karin", "Ophuis"); var moederFamilie = w.Store.Families.Single(f => f.OwnerPersonId == moeder.PersonId); var bijMoeder = (await w.Families.AddMemberAsync(moederFamilie.Id, moeder.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; // Moeder zet een bericht en een foto klaar, en geeft het profiel een geboortedatum die // bij vader niet bekend is. var bericht = (await w.Messages.ComposeAsync(moeder.Id, bijMoeder.Id, "Voor jou", "Lief kind", null, true)).Value!; var foto = (await w.Vault.StoreFileAsync(moeder.Id, "ysabeau.jpg", "image/jpeg", new byte[] { 1, 2, 3 }, subjectPersonId: bijMoeder.Id)).Value!; bijMoeder.BirthDate = new DateOnly(2003, 4, 12); var lidmaatschapMoeder = w.Store.Members.Single(m => m.PersonId == bijMoeder.Id); var lidmaatschapId = lidmaatschapMoeder.Id; // Ysabeau claimt bij vader en neemt daarna, ingelogd, ook die van moeder aan. var tokenVader = (await w.Families.InviteAsync(vaderFamilie.Id, bijVader.Id, vader.Id)).Value!; var ysabeau = (await w.Families.AcceptInvitationAsync(tokenVader, w.Accounts)).Value!; var tokenMoeder = (await w.Families.InviteAsync(moederFamilie.Id, bijMoeder.Id, moeder.Id)).Value!; Assert.True((await w.Families.AcceptInvitationAsync(tokenMoeder, w.Accounts, default, ysabeau.Id, w.Samenvoegen)).Succeeded, "de samenvoeging lukt"); Assert.True(bijMoeder.MergedIntoPersonId == bijVader.Id, "het duplicaat is opgegaan in het doel"); Assert.True(bijVader.BirthDate == new DateOnly(2003, 4, 12), "de geboortedatum is overgenomen"); // ---------------------------------------------------------------- terugdraaien var bon = w.Store.Samenvoegingen.Single(); Assert.True(bon.DuplicaatPersonId == bijMoeder.Id && bon.DoelPersonId == bijVader.Id, "het bonnetje wijst naar de goede twee profielen"); // Alleen een beheerder mag dit. Ysabeau zelf niet, en moeder ook niet. Assert.True(!(await w.Beheer.TerugdraaienAsync(bon.Id, ysabeau.Id, "wil het terug")).Succeeded, "een gewone gebruiker kan niets terugdraaien"); var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer"); w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true; Assert.True(!(await w.Beheer.TerugdraaienAsync(bon.Id, beheerder.Id, "oeps")).Succeeded, "zonder een behoorlijke reden gebeurt er niets"); var terug = await w.Beheer.TerugdraaienAsync(bon.Id, beheerder.Id, "verkeerde uitnodiging aangenomen"); Assert.True(terug.Succeeded, $"met een reden lukt het ({terug.Error})"); // ---------------------------------------------------------------- en dan de staat Assert.True(bijMoeder.MergedIntoPersonId is null, "het duplicaat staat weer op zichzelf"); Assert.True(w.Store.Messages.Single(m => m.Id == bericht.Id).ToPersonId == bijMoeder.Id, "het bericht van moeder staat weer bij háár profiel"); Assert.True(w.Store.VaultFiles.Single(f => f.Id == foto.Id).SubjectPersonId == bijMoeder.Id, "en de foto ook"); Assert.True(w.Store.Members.Single(m => m.Id == lidmaatschapId).PersonId == bijMoeder.Id, "het lidmaatschap in de familie van moeder staat weer op het duplicaat"); Assert.True(bijVader.BirthDate is null, "de geboortedatum die we hadden overgenomen is weer weg bij het doel"); // Moeder ziet weer haar eigen aantekening, vader de zijne. var bijMoederNu = await w.Families.ListMembersAsync(moederFamilie.Id, moeder.Id); Assert.True(bijMoederNu.Single(l => l.PersonId != moeder.PersonId).PersonId == bijMoeder.Id, "moeder ziet weer haar eigen profiel van Ysabeau"); var bijVaderNu = await w.Families.ListMembersAsync(vaderFamilie.Id, vader.Id); Assert.True(bijVaderNu.Single(l => l.PersonId != vader.PersonId).PersonId == bijVader.Id, "vader ziet nog steeds het profiel dat zij heeft geclaimd"); // Het bonnetje blijft staan, met de reden erbij, en werkt geen tweede keer. Assert.True(bon.IsTeruggedraaid && bon.Reden == "verkeerde uitnodiging aangenomen", "het bonnetje vermeldt wanneer en waarom"); Assert.True(!(await w.Beheer.TerugdraaienAsync(bon.Id, beheerder.Id, "nog een keer")).Succeeded, "dezelfde samenvoeging gaat niet twee keer terug"); Assert.True(w.Store.Audit.Any(a => a.Details is not null && a.Details.Contains("teruggedraaid") && a.UserId == beheerder.Id), "het staat in het logboek, bij de beheerder"); }); runner.TestAsync("Een lidmaatschap dat bij het samenvoegen is weggegooid komt terug", async () => { // Het lastigste geval. Staan beide profielen in dezelfde familie, dan verdwijnt bij het // samenvoegen één van de twee lidmaatschappen - anders zou dezelfde persoon twee keer // in die lijst staan. Die regel is daarna weg, id en al. Terugdraaien betekent hier dus // opnieuw aanmaken, uit de kopie die in het bonnetje staat. var w = new World(); var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var familie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); // Twee keer dezelfde dochter in dezelfde lijst, met een tikfout in het tweede adres. var eerste = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var tweede = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeua@example.nl", RelationType.Kind)).Value!; Assert.True(eerste.Id != tweede.Id, "het zijn twee losse profielen"); var tokenEerste = (await w.Families.InviteAsync(familie.Id, eerste.Id, vader.Id)).Value!; var ysabeau = (await w.Families.AcceptInvitationAsync(tokenEerste, w.Accounts)).Value!; var tokenTweede = (await w.Families.InviteAsync(familie.Id, tweede.Id, vader.Id)).Value!; Assert.True((await w.Families.AcceptInvitationAsync(tokenTweede, w.Accounts, default, ysabeau.Id, w.Samenvoegen)).Succeeded, "ze neemt ook de tweede aan en die gaat op in de eerste"); Assert.True(w.Store.Members.Count(m => m.FamilyId == familie.Id && m.PersonId == tweede.Id) == 0, "het dubbele lidmaatschap is weg"); Assert.True((await w.Families.ListMembersAsync(familie.Id, vader.Id)) .Count(l => l.PersonId != vader.PersonId) == 1, "vader ziet haar nog maar één keer"); var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer"); w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true; var bon = w.Store.Samenvoegingen.Single(); Assert.True((await w.Beheer.TerugdraaienAsync(bon.Id, beheerder.Id, "was toch niet dezelfde")).Succeeded, "de samenvoeging gaat terug"); var hersteld = w.Store.Members.SingleOrDefault(m => m.FamilyId == familie.Id && m.PersonId == tweede.Id); Assert.True(hersteld is not null, "het weggegooide lidmaatschap is opnieuw aangemaakt"); Assert.True(hersteld!.Relation == RelationType.Kind, "met dezelfde relatie als toen"); Assert.True(hersteld.AddedByUserId == vader.Id, "en nog steeds op naam van wie hem toevoegde"); Assert.True((await w.Families.ListMembersAsync(familie.Id, vader.Id)) .Count(l => l.PersonId != vader.PersonId) == 2, "vader ziet de twee profielen weer allebei staan"); }); runner.TestAsync("Terugdraaien laat staan wat er ná de samenvoeging is bijgekomen", async () => { // Dit is het punt waar terugdraaien eerlijk moet zijn. Wat Ysabeau ná de samenvoeging // zelf heeft aangemaakt, is van haar en hoort bij haar te blijven staan - ook al staat // het op hetzelfde profiel waar het duplicaat in opging. var w = new World(); var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var vaderFamilie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var bijVader = (await w.Families.AddMemberAsync(vaderFamilie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var moeder = await w.MaakGebruikerAsync("karin@example.nl", "Karin", "Ophuis"); var moederFamilie = w.Store.Families.Single(f => f.OwnerPersonId == moeder.PersonId); var bijMoeder = (await w.Families.AddMemberAsync(moederFamilie.Id, moeder.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var tokenVader = (await w.Families.InviteAsync(vaderFamilie.Id, bijVader.Id, vader.Id)).Value!; var ysabeau = (await w.Families.AcceptInvitationAsync(tokenVader, w.Accounts)).Value!; var tokenMoeder = (await w.Families.InviteAsync(moederFamilie.Id, bijMoeder.Id, moeder.Id)).Value!; await w.Families.AcceptInvitationAsync(tokenMoeder, w.Accounts, default, ysabeau.Id, w.Samenvoegen); // Ná de samenvoeging legt Ysabeau zelf iets in haar kluis. var eigen = await w.Vault.CreateAsync(ysabeau.Id, VaultCategory.Wachtwoord, "Mijn e-mail", new VaultPayload { Username = "ysabeau", Secret = "geheim" }, ReleasePolicy.AlleenIkzelf); Assert.True(eigen.Succeeded, $"ze legt iets in haar kluis ({eigen.Error})"); var beheerder = await w.MaakGebruikerAsync("beheer@example.nl", "Bea", "Beheer"); w.Store.Users.Single(u => u.Id == beheerder.Id).IsBeheerder = true; var bon = w.Store.Samenvoegingen.Single(); Assert.True((await w.Beheer.TerugdraaienAsync(bon.Id, beheerder.Id, "achteraf onjuist")).Succeeded, "de samenvoeging gaat terug"); Assert.True(w.Store.VaultItems.Single(i => i.Id == eigen.Value!.Id).OwnerPersonId == ysabeau.PersonId, "wat zij er zelf in heeft gelegd blijft van haar"); Assert.True((await w.Vault.OpenAsync(eigen.Value!.Id, ysabeau.Id)).Succeeded, "en ze kan er nog gewoon bij"); }); runner.TestAsync("Je eigen profielfoto is zichtbaar voor je familie, een vreemde ziet hem niet", async () => { var w = new World(); w.Verkleiner.Aan = true; var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var familie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var profiel = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Ysabeau", "Flietstra", "ysabeau@example.nl", RelationType.Dochter)).Value!; var token = (await w.Families.InviteAsync(familie.Id, profiel.Id, vader.Id)).Value!; var ysabeau = (await w.Families.AcceptInvitationAsync(token, w.Accounts)).Value!; // Ysabeau zet zélf een foto. Die is van haar, versleuteld met haar sleutel. var foto = await w.Vault.StoreFileAsync(ysabeau.Id, "ik.jpg", "image/jpeg", new byte[] { 4, 5, 6 }, subjectPersonId: ysabeau.PersonId); Assert.True(foto.Succeeded, $"de foto opslaan lukt ({foto.Error})"); Assert.True((await w.Vault.SetProfilePhotoAsync(foto.Value!.Id, ysabeau.Id)).Succeeded, "en ze wijst hem aan als profielfoto"); // Haar vader zit met haar in een familie, dus hij hoort hem te zien - ook al kan hij // de foto op ware grootte niet openmaken. var bijVader = await w.Vault.ProfielfotosAsync(new List { ysabeau.PersonId }, vader.Id); Assert.True(bijVader.Count() == 1, "vader krijgt haar profielfoto terug"); Assert.True(bijVader[0].Klein is not null, "en kan hem tonen"); Assert.True(bijVader[0].Klein!.SequenceEqual(w.Verkleiner.Uitkomst), "het is haar foto"); Assert.True(!(await w.Vault.OpenFileAsync(foto.Value.Id, vader.Id)).Succeeded, "maar de foto op ware grootte blijft van haar"); // Een vreemde zit in geen enkele familie met haar en ziet niets. var vreemde = await w.MaakGebruikerAsync("piet@example.nl", "Piet", "Pietersen"); var bijVreemde = await w.Vault.ProfielfotosAsync(new List { ysabeau.PersonId }, vreemde.Id); Assert.True(bijVreemde.Count() == 0, "een vreemde krijgt haar profielfoto niet"); Assert.True(!(await w.Vault.OpenThumbnailAsync(foto.Value.Id, vreemde.Id)).Succeeded, "en kan hem ook niet los opvragen"); }); runner.TestAsync("Samenvoegen kan alleen met je eigen profiel", async () => { var w = new World(); var vader = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra"); var vreemde = await w.MaakGebruikerAsync("piet@example.nl", "Piet", "Pietersen"); var familie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var dochter = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Ysabeau", "Flietstra", null, RelationType.Dochter)).Value!; // Piet probeert het profiel van Ysabeau in dat van de vader te laten opgaan. Dat is // niet aan hem: alleen de persoon zelf mag zeggen dat twee profielen hem zijn. var stiekem = await w.Samenvoegen.SamenvoegAsync(dochter.Id, vader.PersonId, vreemde.Id); Assert.True(!stiekem.Succeeded, "een ander kan jouw profielen niet samenvoegen"); // En twee echte accounts gaan nooit vanzelf samen. var tweeAccounts = await w.Samenvoegen.SamenvoegAsync(vreemde.PersonId, vader.PersonId, vader.Id); Assert.True(!tweeAccounts.Succeeded, "twee geclaimde profielen worden niet automatisch samengevoegd"); }); runner.TestAsync("Uitnodiging aannemen gaat zonder code", async () => { var w = new World(); var vader = await w.MaakGebruikerAsync("vader@example.nl", "Jan", "Jansen"); var familie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId); var dochter = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Eva", "Jansen", "eva@example.nl", RelationType.Kind)).Value!; var token = (await w.Families.InviteAsync(familie.Id, dochter.Id, vader.Id)).Value!; var eva = await w.Families.AcceptInvitationAsync(token, w.Accounts); Assert.True(eva.Succeeded, "de link uit de e-mail is zelf het bewijs; geen extra code nodig"); // Voortaan logt Eva in met een code, net als iedereen. var aanvraag = await w.Accounts.VraagInlogcodeAanAsync("eva@example.nl"); Assert.True(aanvraag.Succeeded, "Eva kan daarna een inlogcode aanvragen"); }); runner.TestAsync("Uitvaartwens: een gekozen beheerder mag meteen meelezen, zonder overlijden", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); var els = await w.MaakFamilielidAsync(opa, "Els", "Flietstra", "els@example.nl"); Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId, 1)).Succeeded, "Jan is beheerder"); var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { BurialOrCremation = "Crematie", Music = "Adagio" }, ReleasePolicy.NaOverlijden, sharedManagerPersonId: jan.PersonId)).Value!; // Jan mag het meteen lezen, zonder dat er iets met overlijden is gebeurd. var doorJan = await w.Vault.OpenAsync(wens.Id, jan.Id); Assert.True(doorJan.Succeeded, $"Jan kan de wens meteen lezen ({doorJan.Error})"); Assert.True(doorJan.Value!.Music == "Adagio", "en ziet de echte inhoud"); // Els is geen gekozen beheerder voor dit item en komt er niet in, ook al is zij ook beheerder. var doorEls = await w.Vault.OpenAsync(wens.Id, els.Id); Assert.True(!doorEls.Succeeded, "Els is niet gekozen en heeft dus geen toegang"); // Op de lijst "met mij gedeeld" staat de wens voor Jan, niet voor Els. var gedeeldMetJan = (await w.Vault.ListSharedWithMeAsync(jan.Id)).Value!; Assert.True(gedeeldMetJan.Any(i => i.Id == wens.Id), "de wens staat bij Jan op 'met mij gedeeld'"); var gedeeldMetEls = (await w.Vault.ListSharedWithMeAsync(els.Id)).Value!; Assert.True(!gedeeldMetEls.Any(i => i.Id == wens.Id), "en niet bij Els"); // De eigenaar bedenkt zich en kiest niemand meer: bij de volgende wijziging verdwijnt de toegang. var gewijzigd = await w.Vault.UpdateAsync(wens.Id, opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart", new VaultPayload { BurialOrCremation = "Crematie", Music = "Adagio" }, ReleasePolicy.NaOverlijden, sharedManagerPersonId: null); Assert.True(gewijzigd.Succeeded, "de wijziging zonder gekozen beheerder lukt"); var nogSteedsDoorJan = await w.Vault.OpenAsync(wens.Id, jan.Id); Assert.True(!nogSteedsDoorJan.Succeeded, "na het terugtrekken van de keuze kan Jan er niet meer bij"); }); runner.TestAsync("Donorcodicil: dezelfde 'bespreken met'-keuze werkt ook hier", async () => { var w = new World(); var opa = await w.MaakGebruikerAsync("opa@example.nl", "Opa", "Flietstra"); var jan = await w.MaakFamilielidAsync(opa, "Jan", "Flietstra", "jan@example.nl"); Assert.True((await w.Legacy.AppointManagerAsync(opa.Id, jan.PersonId, 1)).Succeeded, "Jan is beheerder"); var codicil = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Donorcodicil, "Mijn donorcodicil", new VaultPayload { Notes = "Alles behalve de hoornvliezen." }, ReleasePolicy.NaOverlijden, sharedManagerPersonId: jan.PersonId)).Value!; var doorJan = await w.Vault.OpenAsync(codicil.Id, jan.Id); Assert.True(doorJan.Succeeded, $"Jan kan het donorcodicil meteen lezen ({doorJan.Error})"); // Op een gewoon kluisitem (geen uitvaartwens of donorcodicil) wordt een gekozen // beheerder genegeerd; delen hoort daar niet bij. var wachtwoord = await w.Vault.CreateAsync(opa.Id, VaultCategory.Wachtwoord, "Bank", new VaultPayload { Secret = "geheim" }, ReleasePolicy.AlleenIkzelf, sharedManagerPersonId: jan.PersonId); Assert.True(wachtwoord.Succeeded && wachtwoord.Value!.SharedManagerPersonId is null, "een gewoon kluisitem deelt niet, ook niet als er per ongeluk iemand gekozen staat"); }); } }