Files
MijnBesluit/tests/MijnBesluit.SelfCheck/InlogChecks.cs
T
2026-09-27 14:21:31 +00:00

1316 lines
78 KiB
C#

using MijnBesluit.Application.Services;
using MijnBesluit.Domain.Enums;
using MijnBesluit.Application.Models;
namespace MijnBesluit.SelfCheck;
/// <summary>Inloggen en registreren met een code per e-mail.</summary>
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<long> { 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<long> { 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<long> { 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<long> { 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<long> { 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<long> { 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");
});
}
}