1947 lines
106 KiB
C#
1947 lines
106 KiB
C#
using MijnBesluit.Application.Models;
|
|
using MijnBesluit.Application.Security;
|
|
using MijnBesluit.Application.Services;
|
|
using MijnBesluit.Domain.Enums;
|
|
using MijnBesluit.Domain.Rules;
|
|
using MijnBesluit.SelfCheck;
|
|
|
|
// Controleert de kernregels van MijnBesluit zonder database, webserver of packages.
|
|
// Draaien met: dotnet run --project tests/MijnBesluit.SelfCheck
|
|
|
|
var runner = new Runner();
|
|
|
|
// ---------------------------------------------------------------- wachtwoorden
|
|
runner.Test("Geheimen hashen en controleren", () =>
|
|
{
|
|
var hash = SecretHasher.Hash("483920");
|
|
Assert.True(hash.StartsWith("pbkdf2-sha512$"), "de hash bevat het algoritme");
|
|
Assert.True(SecretHasher.Verify("483920", hash), "de juiste code wordt herkend");
|
|
Assert.True(!SecretHasher.Verify("483921", hash), "een code die één cijfer scheelt wordt geweigerd");
|
|
Assert.True(!hash.Contains("483920"), "de code zelf staat niet in de hash");
|
|
Assert.True(hash != SecretHasher.Hash("483920"), "twee keer dezelfde code geeft een andere hash (salt)");
|
|
});
|
|
|
|
runner.Test("De inlogcode is zes cijfers en niet te raden", () =>
|
|
{
|
|
var codes = Enumerable.Range(0, 200).Select(_ => InlogcodeGenerator.Nieuw()).ToList();
|
|
Assert.True(codes.All(c => c.Length == 6 && c.All(char.IsDigit)), "altijd zes cijfers");
|
|
Assert.True(codes.Distinct().Count() > 190, "de codes zijn niet steeds dezelfde");
|
|
Assert.True(InlogcodeGenerator.Normaliseer(" 12 34-56 ") == "123456", "spaties en streepjes worden genegeerd");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- e-mailkopteksten
|
|
runner.Test("Elke e-mail krijgt een geldig Message-ID", () =>
|
|
{
|
|
// Zonder deze koptekst weigert Gmail de mail (550-5.7.1) en komt er geen inlogcode aan.
|
|
var kenmerk = Berichtkenmerk.Nieuw("jonathan@flietstra.nl");
|
|
|
|
Assert.True(kenmerk.StartsWith("<") && kenmerk.EndsWith(">"), "het staat tussen punthaken");
|
|
Assert.True(kenmerk.Contains('@'), "er zit een apenstaartje in");
|
|
Assert.True(kenmerk.EndsWith("@flietstra.nl>"), "het domein komt van de afzender");
|
|
Assert.True(Berichtkenmerk.Nieuw("jonathan@flietstra.nl") != kenmerk, "elke mail krijgt een eigen kenmerk");
|
|
|
|
Assert.True(Berichtkenmerk.Domein("noreply@MijnBesluit.NL") == "mijnbesluit.nl", "het domein gaat naar kleine letters");
|
|
Assert.True(Berichtkenmerk.Domein("geenadres") == "mijnbesluit.nl", "zonder apenstaartje een vervanger");
|
|
Assert.True(Berichtkenmerk.Domein(null) == "mijnbesluit.nl", "en zonder adres ook");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- versleuteling
|
|
runner.Test("Kluisinhoud versleutelen en weer openen", () =>
|
|
{
|
|
var key = EnvelopeCrypto.NewKey();
|
|
var payload = new VaultPayload { Website = "bank.nl", Username = "jan", Secret = "geheim123" };
|
|
|
|
var sealedData = EnvelopeCrypto.SealJson(payload, key);
|
|
Assert.True(!System.Text.Encoding.UTF8.GetString(sealedData.Ciphertext).Contains("geheim123"),
|
|
"het geheim staat niet leesbaar in de cijfertekst");
|
|
|
|
var terug = EnvelopeCrypto.OpenJson<VaultPayload>(sealedData, key)!;
|
|
Assert.True(terug.Secret == "geheim123", "inhoud komt ongeschonden terug");
|
|
});
|
|
|
|
runner.Test("Geknoei met versleutelde data wordt opgemerkt", () =>
|
|
{
|
|
var key = EnvelopeCrypto.NewKey();
|
|
var s = EnvelopeCrypto.Seal(new byte[] { 1, 2, 3, 4, 5 }, key);
|
|
s.Ciphertext[0] ^= 0xFF;
|
|
Assert.Throws<System.Security.Cryptography.CryptographicException>(() => EnvelopeCrypto.Open(s, key),
|
|
"een gewijzigde cijfertekst wordt geweigerd");
|
|
|
|
var andereSleutel = EnvelopeCrypto.NewKey();
|
|
var s2 = EnvelopeCrypto.Seal(new byte[] { 1, 2, 3 }, key);
|
|
Assert.Throws<System.Security.Cryptography.CryptographicException>(() => EnvelopeCrypto.Open(s2, andereSleutel),
|
|
"met de verkeerde sleutel lukt openen niet");
|
|
});
|
|
|
|
runner.Test("Sleutel in- en uitpakken", () =>
|
|
{
|
|
var master = EnvelopeCrypto.NewKey();
|
|
var dek = EnvelopeCrypto.NewKey();
|
|
var wrapped = EnvelopeCrypto.WrapKey(dek, master);
|
|
Assert.True(!wrapped.SequenceEqual(dek), "de ingepakte sleutel is niet gelijk aan de sleutel zelf");
|
|
Assert.True(EnvelopeCrypto.UnwrapKey(wrapped, master).SequenceEqual(dek), "uitpakken geeft dezelfde sleutel");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- het proefscript
|
|
runner.Test("Het proefscript kent alle acties van de server", () =>
|
|
{
|
|
// SocketServer-proef.ps1 -Acties zegt of de draaiende server bij deze code hoort. Dat werkt
|
|
// alleen als de lijst in dat script compleet is. Vergeet iemand er een, dan zegt de proef
|
|
// "alles bekend" terwijl er iets mist - en dan is de controle erger dan geen controle.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "tools"))) map = map.Parent;
|
|
|
|
var script = map is null ? null : Path.Combine(map.FullName, "tools", "SocketServer-proef.ps1");
|
|
if (script is null || !File.Exists(script))
|
|
{
|
|
// Gebouwd buiten de projectmap; daar valt niets te controleren.
|
|
return;
|
|
}
|
|
|
|
var inhoud = File.ReadAllText(script);
|
|
var begin = inhoud.IndexOf("$teControleren = @(", StringComparison.Ordinal);
|
|
Assert.True(begin >= 0, "de lijst met acties staat in het script");
|
|
|
|
var blok = inhoud[begin..inhoud.IndexOf(')', begin)];
|
|
var inScript = System.Text.RegularExpressions.Regex.Matches(blok, "'([a-z0-9.\\-]+)'")
|
|
.Select(m => m.Groups[1].Value).ToHashSet(StringComparer.Ordinal);
|
|
|
|
var inProtocol = typeof(MijnBesluit.Protocol.SocketActions)
|
|
.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)
|
|
.Where(f => f.IsLiteral && f.FieldType == typeof(string))
|
|
.Select(f => (string)f.GetRawConstantValue()!)
|
|
.ToHashSet(StringComparer.Ordinal);
|
|
|
|
var ontbreekt = inProtocol.Except(inScript).OrderBy(a => a).ToList();
|
|
Assert.True(ontbreekt.Count == 0,
|
|
$"het proefscript mist: {string.Join(", ", ontbreekt)}");
|
|
|
|
var teveel = inScript.Except(inProtocol).OrderBy(a => a).ToList();
|
|
Assert.True(teveel.Count == 0,
|
|
$"het proefscript noemt acties die niet bestaan: {string.Join(", ", teveel)}");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- rechten op de tabellen
|
|
runner.Test("Elke tabel geeft de applicatiegebruiker rechten", () =>
|
|
{
|
|
// Een nieuwe tabel zonder GRANT levert "SELECT command denied to user 'mijnbesluit_app'" op -
|
|
// en dat merk je pas als iemand dat ene scherm opent. Dat is precies wat er gebeurde met
|
|
// `samenvoegingen` en met de drie tabellen van adverteren.
|
|
//
|
|
// Deze controle leest de scripts in db/ en legt elke CREATE TABLE naast elke GRANT. Hij kijkt
|
|
// naar bestanden en niet naar een draaiende database: die hebben we hier niet, en de fout zit
|
|
// toch in het script.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "db"))) map = map.Parent;
|
|
|
|
if (map is null) return; // Gebouwd buiten de projectmap; daar valt niets te controleren.
|
|
|
|
var scripts = Directory.GetFiles(Path.Combine(map.FullName, "db"), "*.sql");
|
|
Assert.True(scripts.Length > 0, "er staan scripts in db/");
|
|
|
|
var gemaakt = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var gegund = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Rechten tellen alleen mee als ze in 02 staan, en nergens anders. Sommige migratiescripts
|
|
// deelden ze vroeger zelf uit, vlak onder hun CREATE TABLE. Dat leest prettig, maar de
|
|
// scripts vanaf 03 worden ook door de uitrolagent gedraaid met een beperkt databaseaccount,
|
|
// en dat mág geen rechten uitdelen: de uitrol struikelde er dan op, en dan staat de halve
|
|
// uitrol er al. Daarom hier twee controles in één: staat elke tabel in de lijst, én staat
|
|
// die lijst op één plek.
|
|
var buitenDeLijst = new List<string>();
|
|
|
|
foreach (var script in scripts)
|
|
{
|
|
var inhoud = File.ReadAllText(script);
|
|
var isDeLijst = Path.GetFileName(script).StartsWith("02_", StringComparison.Ordinal);
|
|
|
|
foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(
|
|
inhoud, @"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`([a-z_]+)`",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
|
|
gemaakt.Add(m.Groups[1].Value);
|
|
|
|
// Alleen echte opdrachten, geen uitleg: in de koppen van de scripts staat het woord GRANT
|
|
// nu juist om te vertellen waaróm het er niet staat.
|
|
foreach (var regel in inhoud.Split('\n'))
|
|
{
|
|
var schoon = regel.TrimStart();
|
|
if (schoon.StartsWith("--", StringComparison.Ordinal)) continue;
|
|
|
|
var m = System.Text.RegularExpressions.Regex.Match(
|
|
schoon, @"GRANT[^;]*?ON\s+`mijnbesluit`\.`([a-z_]+)`",
|
|
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
|
if (!m.Success) continue;
|
|
|
|
if (isDeLijst) gegund.Add(m.Groups[1].Value);
|
|
else buitenDeLijst.Add($"{Path.GetFileName(script)} ({m.Groups[1].Value})");
|
|
}
|
|
}
|
|
|
|
Assert.True(buitenDeLijst.Count == 0,
|
|
"rechten worden alleen in 02_applicatiegebruiker.sql uitgedeeld; niet in " +
|
|
string.Join(", ", buitenDeLijst));
|
|
|
|
var zonderRecht = gemaakt.Except(gegund).OrderBy(t => t).ToList();
|
|
Assert.True(zonderRecht.Count == 0,
|
|
$"deze tabellen geven de applicatiegebruiker geen rechten: {string.Join(", ", zonderRecht)}");
|
|
|
|
var bestaatNiet = gegund.Except(gemaakt).OrderBy(t => t).ToList();
|
|
Assert.True(bestaatNiet.Count == 0,
|
|
$"er worden rechten gegeven op tabellen die nergens worden aangemaakt: {string.Join(", ", bestaatNiet)}");
|
|
|
|
// En het logboek hoort niet gewijzigd of gewist te kunnen worden, hoe vaak er ook iets
|
|
// bijkomt. Dat is de enige tabel met minder rechten, en die uitzondering is het waard om
|
|
// te bewaken.
|
|
var rechten = File.ReadAllText(Path.Combine(map.FullName, "db", "02_applicatiegebruiker.sql"));
|
|
var logregel = rechten.Split('\n').FirstOrDefault(r => r.Contains("`audit_log`") && r.Contains("GRANT"));
|
|
Assert.True(logregel is not null, "audit_log krijgt rechten");
|
|
Assert.True(!logregel!.Contains("UPDATE") && !logregel.Contains("DELETE"),
|
|
"het logboek mag alleen gelezen en aangevuld worden, niet gewijzigd of gewist");
|
|
});
|
|
|
|
runner.Test("Het e-mailadres van een persoon is niet uniek in de database", () =>
|
|
{
|
|
// Hier stond een UNIQUE-index op, en dat botste met het uitgangspunt dat iedereen zijn eigen
|
|
// aantekening heeft over een ander. Wie een adres het eerst invulde, bepaalde daarmee dat
|
|
// niemand anders die persoon nog kon toevoegen: het opslaan liep stuk op de database en de
|
|
// gebruiker zag een knop die niets deed.
|
|
//
|
|
// Bij `users` hoort uniek juist wél: dat is een account, en er kan er maar één per adres
|
|
// zijn. Deze controle bewaakt allebei die kanten. Zie docs/Identiteit.md.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "db"))) map = map.Parent;
|
|
|
|
if (map is null) return; // Gebouwd buiten de projectmap; daar valt niets te controleren.
|
|
|
|
var schema = File.ReadAllText(Path.Combine(map.FullName, "db", "01_schema.sql"));
|
|
|
|
var personen = schema[schema.IndexOf("CREATE TABLE IF NOT EXISTS `persons`", StringComparison.Ordinal)..];
|
|
personen = personen[..personen.IndexOf("ENGINE=InnoDB", StringComparison.Ordinal)];
|
|
Assert.True(!personen.Contains("UNIQUE KEY `ux_persons_email`", StringComparison.Ordinal),
|
|
"op persons.email hoort geen unieke index te staan");
|
|
Assert.True(personen.Contains("`ix_persons_email`", StringComparison.Ordinal),
|
|
"wel een gewone index, want er wordt op gezocht");
|
|
|
|
var gebruikers = schema[schema.IndexOf("CREATE TABLE IF NOT EXISTS `users`", StringComparison.Ordinal)..];
|
|
gebruikers = gebruikers[..gebruikers.IndexOf("ENGINE=InnoDB", StringComparison.Ordinal)];
|
|
Assert.True(gebruikers.Contains("UNIQUE", StringComparison.Ordinal) &&
|
|
gebruikers.Contains("`email`", StringComparison.Ordinal),
|
|
"op users.email hoort juist wél een unieke index te staan");
|
|
});
|
|
|
|
runner.Test("Elk formulier op de site heeft een model dat niet leeggemaakt kan worden", () =>
|
|
{
|
|
// De uitnodigingspagina viel om met "EditForm requires either a Model parameter". Oorzaak:
|
|
// [SupplyParameterFromForm] stond er zonder FormName terwijl het formulier er wel een had.
|
|
// Die twee horen bij elkaar; kloppen ze niet, dan zet de binder het model op null en is de
|
|
// "= new()" weg. Dat is geen foutmelding maar een witte pagina met een 500 - en je ziet het
|
|
// pas als iemand op de link in zijn uitnodigingsmail klikt.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
|
|
var paginas = map is null ? null : Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components");
|
|
if (paginas is null || !Directory.Exists(paginas)) return; // gebouwd buiten de projectmap
|
|
|
|
var fout = new List<string>();
|
|
|
|
foreach (var bestand in Directory.GetFiles(paginas, "*.razor", SearchOption.AllDirectories))
|
|
{
|
|
var naam = Path.GetFileName(bestand);
|
|
|
|
// Toelichting eruit: een uitleg die het kenmerk noemt is geen gebruik ervan. Zonder dit
|
|
// sloeg deze controle aan op de opmerking waarin staat waarom het er juist niet staat.
|
|
var inhoud = System.Text.RegularExpressions.Regex.Replace(
|
|
File.ReadAllText(bestand), @"^\s*///.*$", "", System.Text.RegularExpressions.RegexOptions.Multiline);
|
|
inhoud = System.Text.RegularExpressions.Regex.Replace(inhoud, @"@\*.*?\*@", "",
|
|
System.Text.RegularExpressions.RegexOptions.Singleline);
|
|
|
|
var gebonden = System.Text.RegularExpressions.Regex
|
|
.Matches(inhoud, @"\[SupplyParameterFromForm(?:\(FormName\s*=\s*""([^""]+)""\))?\]")
|
|
.Select(m => m.Groups[1].Success ? m.Groups[1].Value : null)
|
|
.ToList();
|
|
|
|
// Zonder FormName bindt het kenmerk aan het naamloze formulier. Heeft de pagina alleen
|
|
// formulieren met een naam, dan vindt het nooit iets en wordt het model dus null.
|
|
if (!gebonden.Any(g => g is null)) continue;
|
|
|
|
var heeftNaamloosFormulier = System.Text.RegularExpressions.Regex
|
|
.Matches(inhoud, @"<EditForm\b[^>]*")
|
|
.Any(m => !m.Value.Contains("FormName", StringComparison.Ordinal));
|
|
|
|
if (!heeftNaamloosFormulier)
|
|
fout.Add($"{naam}: [SupplyParameterFromForm] zonder FormName, maar elk formulier heeft er wel een");
|
|
}
|
|
|
|
Assert.True(fout.Count == 0, string.Join("; ", fout));
|
|
});
|
|
|
|
runner.Test("Een layout haalt zelf niets op", () =>
|
|
{
|
|
// De layout staat op ELKE pagina en wordt tegelijk met die pagina opgebouwd. Vraagt hij zelf
|
|
// iets aan de database, dan loopt die vraag gelijk op met de vraag van de pagina zelf, over
|
|
// dezelfde DbContext - en die kan er maar een tegelijk aan. Het gevolg is
|
|
// "A second operation was started on this context instance": een foutpagina die er soms wel
|
|
// en soms niet is, afhankelijk van welke vraag net eerder klaar was.
|
|
//
|
|
// Dat gebeurde toen het menu-item Beheer erbij kwam en de layout ging opzoeken of je
|
|
// beheerder bent. Wat de layout moet weten, hoort uit het inlogcookie te komen of van de
|
|
// pagina zelf. Deze controle houdt vast dat een layout niets ophaalt.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
|
|
var layouts = map is null ? null : Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components", "Layout");
|
|
if (layouts is null || !Directory.Exists(layouts)) return; // gebouwd buiten de projectmap
|
|
|
|
var fout = new List<string>();
|
|
|
|
foreach (var bestand in Directory.GetFiles(layouts, "*.razor", SearchOption.AllDirectories))
|
|
{
|
|
// De toelichting eruit: een opmerking die uitlegt waarom er geen service in zit, is zelf
|
|
// geen service. Zonder dit slaat de controle aan op zijn eigen uitleg.
|
|
var inhoud = System.Text.RegularExpressions.Regex.Replace(
|
|
File.ReadAllText(bestand), @"@\*.*?\*@", "", System.Text.RegularExpressions.RegexOptions.Singleline);
|
|
|
|
var naam = Path.GetFileName(bestand);
|
|
|
|
foreach (System.Text.RegularExpressions.Match m in System.Text.RegularExpressions.Regex.Matches(
|
|
inhoud, @"^\s*@inject\s+([A-Za-z0-9_.]+)", System.Text.RegularExpressions.RegexOptions.Multiline))
|
|
{
|
|
var soort = m.Groups[1].Value;
|
|
if (soort.EndsWith("Service", StringComparison.Ordinal) ||
|
|
soort.EndsWith("Repository", StringComparison.Ordinal) ||
|
|
soort.EndsWith("DbContext", StringComparison.Ordinal))
|
|
fout.Add($"{naam}: @inject {soort} - een layout hoort niets op te halen");
|
|
}
|
|
|
|
if (inhoud.Contains("OnInitializedAsync", StringComparison.Ordinal))
|
|
fout.Add($"{naam}: OnInitializedAsync - een layout hoort niets op te halen");
|
|
}
|
|
|
|
Assert.True(fout.Count == 0, string.Join("; ", fout));
|
|
});
|
|
|
|
// ---------------------------------------------------------------- de terugknop van het toestel
|
|
runner.Test("De terugknop gaat van boven naar beneden door wat er openstaat", () =>
|
|
{
|
|
// De volgorde is het hele punt: staat er een foto open in een wijzigscherm, dan hoort terug
|
|
// eerst die foto te sluiten, dan het wijzigscherm, en pas daarna naar het overzicht te gaan.
|
|
// Ging dat mis, dan verloor iemand zijn half ingevulde formulier - of sloot de app.
|
|
var knop = new MijnBesluit.AppUi.Terugknop();
|
|
var volgorde = new List<string>();
|
|
|
|
Assert.True(!knop.Ingedrukt(), "zonder iets open handelt niemand hem af");
|
|
|
|
// De schil meldt zich als eerste aan en ligt dus onderop, net als in de app.
|
|
var schil = knop.Aanmelden(() => { volgorde.Add("schil"); return true; });
|
|
var scherm = knop.Aanmelden(() => { volgorde.Add("wijzigscherm"); return true; });
|
|
var foto = knop.Aanmelden(() => { volgorde.Add("foto"); return true; });
|
|
|
|
knop.Ingedrukt();
|
|
Assert.True(volgorde.SequenceEqual(new[] { "foto" }), "de bovenste is als eerste aan de beurt");
|
|
|
|
foto.Dispose();
|
|
knop.Ingedrukt();
|
|
Assert.True(volgorde.Last() == "wijzigscherm", "daarna het wijzigscherm eronder");
|
|
|
|
scherm.Dispose();
|
|
knop.Ingedrukt();
|
|
Assert.True(volgorde.Last() == "schil", "en als er niets meer openstaat de schil zelf");
|
|
|
|
schil.Dispose();
|
|
Assert.True(!knop.Ingedrukt(), "alles afgemeld: het toestel mag zijn gang gaan");
|
|
});
|
|
|
|
runner.Test("Een afhandelaar die nee zegt geeft de terugknop door", () =>
|
|
{
|
|
// Zo werkt het overzicht: daar is niets meer om naar terug te gaan, dus geeft de schil hem
|
|
// door. Zegt niemand ja, dan sluit de app - en dat is dan ook de bedoeling.
|
|
var knop = new MijnBesluit.AppUi.Terugknop();
|
|
|
|
var opOverzicht = false;
|
|
knop.Aanmelden(() => !opOverzicht);
|
|
|
|
Assert.True(knop.Ingedrukt(), "buiten het overzicht wordt hij afgehandeld");
|
|
|
|
opOverzicht = true;
|
|
Assert.True(!knop.Ingedrukt(), "op het overzicht niet, en dan sluit de app");
|
|
});
|
|
|
|
runner.Test("De uitlogklok telt de stilte en staat standaard uit", () =>
|
|
{
|
|
var klok = new MijnBesluit.AppUi.Uitlogklok();
|
|
var nu = new DateTime(2026, 9, 15, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
// Zonder toestelslot standaard uit. Dat is met opzet: op zo'n toestel betekent de termijn
|
|
// een nieuwe code per e-mail, en dat leg je iemand niet ongevraagd op.
|
|
Assert.True(klok.Minuten == 0 && !klok.StaatAan, "standaard staat hij uit");
|
|
Assert.True(!klok.IsVerlopen(nu.AddDays(7)), "en dan verloopt er nooit iets");
|
|
|
|
klok.Minuten = 5;
|
|
klok.Aangeraakt();
|
|
Assert.True(!klok.IsVerlopen(nu), "net aangeraakt: nog niets aan de hand");
|
|
|
|
// De tijd komt van buiten, dus die kunnen we hier gewoon opschuiven.
|
|
var start = DateTime.UtcNow;
|
|
Assert.True(!klok.IsVerlopen(start.AddMinutes(4)), "na vier minuten nog niet");
|
|
Assert.True(klok.IsVerlopen(start.AddMinutes(5)), "na vijf minuten wel");
|
|
Assert.True(klok.IsVerlopen(start.AddHours(3)), "en daarna zeker");
|
|
|
|
// Aanraken zet hem terug op nul.
|
|
klok.Aangeraakt();
|
|
Assert.True(!klok.IsVerlopen(DateTime.UtcNow.AddMinutes(4)), "aanraken begint opnieuw");
|
|
|
|
// De termijn wijzigen mag niet meteen tot uitloggen leiden, ook niet als het lang stil was.
|
|
var traag = new MijnBesluit.AppUi.Uitlogklok();
|
|
traag.Minuten = 1;
|
|
Assert.True(!traag.IsVerlopen(DateTime.UtcNow.AddSeconds(30)), "een net gezette termijn loopt vanaf nu");
|
|
});
|
|
|
|
runner.Test("De uitlogklok onthoudt zijn termijn op het toestel", () =>
|
|
{
|
|
// Twee klokken op dezelfde opslag: wat de een zet, leest de ander. Zo werkt het straks ook
|
|
// tussen twee starts van de app.
|
|
var opslag = new MijnBesluit.AppUi.Losseinstellingen();
|
|
|
|
var eerste = new MijnBesluit.AppUi.Uitlogklok(opslag);
|
|
eerste.Minuten = 15;
|
|
|
|
var tweede = new MijnBesluit.AppUi.Uitlogklok(opslag);
|
|
Assert.True(tweede.Minuten == 15, "de tweede klok kent de termijn van de eerste");
|
|
Assert.True(tweede.StaatAan, "en weet dat hij aanstaat");
|
|
|
|
Assert.True(MijnBesluit.AppUi.Uitlogklok.Keuzes[0] == 0, "de eerste keuze is uit");
|
|
Assert.True(MijnBesluit.AppUi.Uitlogklok.Naam(0).StartsWith("Uit"), "en die heet ook zo");
|
|
Assert.True(MijnBesluit.AppUi.Uitlogklok.Naam(1) == "Na 1 minuut", "enkelvoud bij één minuut");
|
|
});
|
|
|
|
runner.Test("Met een toestelslot kan de uitlogklok niet uit", () =>
|
|
{
|
|
// Op een toestel dat om een vingerafdruk of zijn eigen code kan vragen kost het slot je één
|
|
// aanraking. "Uit" levert daar dus bijna niets op, en betekent wel dat een gevonden telefoon
|
|
// een open kluis is. Daarom bestaat die keuze daar niet - niet in de lijst, en ook niet als
|
|
// hij er van vroeger nog in staat.
|
|
//
|
|
// Zonder zo'n slot ligt het andersom: daar betekent de termijn écht uitloggen en kost
|
|
// vergeten je een e-mail met een code. Die keuze mag iemand daar wél zelf maken.
|
|
var metSlot = new MijnBesluit.AppUi.Uitlogklok(
|
|
new MijnBesluit.AppUi.Losseinstellingen(), MijnBesluit.AppUi.Uitlogklok.StandaardMetSlot,
|
|
slotBeschikbaar: true);
|
|
|
|
Assert.True(metSlot.StaatAan, "met een toestelslot staat hij aan");
|
|
Assert.True(metSlot.Minuten == 15, "en wel na een kwartier");
|
|
|
|
// Een nul uit de opslag wordt opgetild. Dat gebeurt bij iemand die zijn schermslot later
|
|
// heeft aangezet, of die de app gebruikte toen die keuze er nog wel was.
|
|
var opslag = new MijnBesluit.AppUi.Losseinstellingen();
|
|
var eerst = new MijnBesluit.AppUi.Uitlogklok(opslag, MijnBesluit.AppUi.Uitlogklok.StandaardMetSlot, true);
|
|
eerst.Minuten = 0;
|
|
|
|
var later = new MijnBesluit.AppUi.Uitlogklok(opslag, MijnBesluit.AppUi.Uitlogklok.StandaardMetSlot, true);
|
|
Assert.True(later.Minuten == MijnBesluit.AppUi.Uitlogklok.StandaardMetSlot && later.StaatAan,
|
|
"een nul uit de opslag telt niet als keuze zolang er een slot is");
|
|
|
|
// Datzelfde toestel zonder slot: dan blijft uit gewoon uit.
|
|
var zonderSlot = new MijnBesluit.AppUi.Uitlogklok(opslag, 0, slotBeschikbaar: false);
|
|
Assert.True(zonderSlot.Minuten == 0 && !zonderSlot.StaatAan, "zonder slot blijft uit wél uit");
|
|
|
|
Assert.True(!MijnBesluit.AppUi.Uitlogklok.KeuzesMetSlot.Contains(0),
|
|
"met een slot staat uit niet in de keuzelijst");
|
|
Assert.True(MijnBesluit.AppUi.Uitlogklok.Keuzes.Contains(0),
|
|
"zonder slot staat uit er wel in");
|
|
|
|
// Het slot dat er niet is zegt netjes nee, in plaats van te doen alsof het openging.
|
|
var geen = new MijnBesluit.AppUi.Geenslot();
|
|
Assert.True(!geen.Beschikbaar, "zonder toestelslot is er niets beschikbaar");
|
|
Assert.True(!geen.OntgrendelAsync("x", "y").Result, "en ontgrendelen lukt dan nooit");
|
|
});
|
|
|
|
runner.Test("Gedeelde onderdelen zijn in app en website letterlijk gelijk", () =>
|
|
{
|
|
// Er is geen project dat de app en de site allebei zien, dus staan een paar onderdelen twee
|
|
// keer op schijf. Dat mag, mits ze letterlijk hetzelfde zijn.
|
|
//
|
|
// Penning: de ronde penning met iemands profielfoto, in vier lijsten. Loopt hij uit elkaar,
|
|
// dan valt de een terug op beginletters waar de ander een foto laat zien.
|
|
//
|
|
// Productuitleg: de tekst "Wat is MijnBesluit?", op de site een pagina en in de app een
|
|
// popup. Dat is de tekst waarmee je het product uitlegt; twee versies daarvan is erger dan
|
|
// twee versies van een knop.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
// Privacytekst: de privacyverklaring, op de site een pagina en in de app een popup. Twee
|
|
// versies daarvan is het ergste geval van allemaal - het is het document waar iemand zich op
|
|
// beroept, en waarvan Google Play een adres wil.
|
|
foreach (var naam in new[] { "Penning.razor", "Productuitleg.razor", "Privacytekst.razor", "Versiehistorie.razor" })
|
|
{
|
|
var web = Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components", "Onderdelen", naam);
|
|
var app = Path.Combine(map.FullName, "src", "MijnBesluit.AppUi", "Onderdelen", naam);
|
|
if (!File.Exists(web) || !File.Exists(app)) continue;
|
|
|
|
Assert.True(File.ReadAllText(web) == File.ReadAllText(app),
|
|
$"{naam} is in beide projecten gelijk");
|
|
}
|
|
});
|
|
|
|
runner.Test("De privacyverklaring is vanaf het overzicht te lezen", () =>
|
|
{
|
|
// Op allebei de plekken waar iemand binnenkomt - het startscherm van de app en het overzicht
|
|
// op de site - staat een knop die de privacyverklaring als popup opent. Dat is bewust geen
|
|
// link naar een pagina: wie wegklikt van zijn overzicht om iets te lezen, komt niet terug.
|
|
//
|
|
// De pagina /privacy blijft bestaan, want Google Play wil een adres dat je kunt aanklikken
|
|
// en iemand moet hem los kunnen delen. De tekst zelf staat maar op één plek per project
|
|
// (Privacytekst.razor); dat die twee gelijk zijn bewaakt de controle hierboven.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var schermen = new[]
|
|
{
|
|
Path.Combine(map.FullName, "src", "MijnBesluit.AppUi", "Schermen", "Start.razor"),
|
|
Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components", "Pages", "Home.razor")
|
|
};
|
|
|
|
foreach (var scherm in schermen)
|
|
{
|
|
if (!File.Exists(scherm)) continue;
|
|
var inhoud = File.ReadAllText(scherm);
|
|
|
|
Assert.True(inhoud.Contains("Privacyverklaring"),
|
|
$"{Path.GetFileName(scherm)} heeft een knop naar de privacyverklaring");
|
|
Assert.True(inhoud.Contains("<Privacytekst />"),
|
|
$"{Path.GetFileName(scherm)} toont de tekst zelf en verwijst niet naar elders");
|
|
Assert.True(inhoud.Contains("Versiehistorie"),
|
|
$"{Path.GetFileName(scherm)} heeft een knop naar de versiehistorie");
|
|
Assert.True(inhoud.Contains("<Versiehistorie />"),
|
|
$"{Path.GetFileName(scherm)} toont de versiehistorie zelf en verwijst niet naar elders");
|
|
}
|
|
});
|
|
|
|
runner.Test("iOS: alles wat het toestel moet vrijgeven staat in Info.plist", () =>
|
|
{
|
|
// Waarom dit een controle is en geen goed voornemen: vraagt de app om Face ID, de camera,
|
|
// de microfoon of de fotobibliotheek zonder dat de bijbehorende tekst in Info.plist staat,
|
|
// dan sluit iOS de app ter plekke af. Geen foutmelding, geen crashmelding die ergens op
|
|
// wijst - hij is weg. Dat is niet te vinden door te kijken, alleen door het mee te maken.
|
|
//
|
|
// Deze controle kijkt naar bestanden en niet naar een draaiende app: we bouwen hier geen
|
|
// iOS, en de fout zit toch in het bestand.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var plist = Path.Combine(map.FullName, "src", "MijnBesluit.App", "Platforms", "iOS", "Info.plist");
|
|
if (!File.Exists(plist)) return; // nog geen iOS in dit project
|
|
|
|
var inhoud = File.ReadAllText(plist);
|
|
|
|
foreach (var sleutel in new[]
|
|
{
|
|
"NSFaceIDUsageDescription", // het slot op de app
|
|
"NSCameraUsageDescription", // foto en video bij een kluisitem of bericht
|
|
"NSMicrophoneUsageDescription", // geluid bij een videobericht
|
|
"NSPhotoLibraryUsageDescription" // iets uit de fotobibliotheek toevoegen
|
|
})
|
|
{
|
|
Assert.True(inhoud.Contains(sleutel, StringComparison.Ordinal),
|
|
$"{sleutel} staat in Info.plist");
|
|
|
|
// En er hoort ook echt iets te staan. Een lege tekst haalt de beoordeling niet, en
|
|
// helpt de gebruiker die het vraagteken op zijn scherm krijgt al helemaal niet.
|
|
var na = inhoud[(inhoud.IndexOf(sleutel, StringComparison.Ordinal) + sleutel.Length)..];
|
|
var begin = na.IndexOf("<string>", StringComparison.Ordinal);
|
|
var eind = na.IndexOf("</string>", StringComparison.Ordinal);
|
|
Assert.True(begin >= 0 && eind > begin && (eind - begin) > "<string>".Length + 20,
|
|
$"{sleutel} heeft een tekst die iets uitlegt");
|
|
}
|
|
|
|
// De exportverklaring hoort er bewust NIET in te staan: dat is een verklaring met juridisch
|
|
// gewicht en geen vinkje. Zonder deze sleutel vraagt App Store Connect het bij elke
|
|
// inzending. Zie de toelichting in Info.plist en docs/iOS-app.md.
|
|
Assert.True(!inhoud.Contains("ITSAppUsesNonExemptEncryption", StringComparison.Ordinal) ||
|
|
inhoud.Contains("<!--", StringComparison.Ordinal),
|
|
"de exportverklaring staat er niet zomaar in");
|
|
});
|
|
|
|
runner.Test("Een script hoort in de kop en niet in een pagina", () =>
|
|
{
|
|
// Waar dit vandaan komt: op het boekscherm van een adverteerder stond de rekensom als
|
|
// <script> in de pagina zelf. Dat werkte als je de pagina ververste en deed niets als je er
|
|
// via een link kwam - de prijs bleef dan staan wat hij was toen de pagina geladen werd.
|
|
//
|
|
// De reden is dat onze pagina's statisch worden weergegeven en Blazor bij het navigeren
|
|
// alleen de HTML vervangt. Een <script> dat zo binnenkomt voert de browser NIET uit. Een
|
|
// script hoort dus in App.razor te staan, waar het bij elke pagina in de kop zit, en de
|
|
// pagina roept het aan met gewone HTML-kenmerken (oninput, onchange).
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var componenten = Path.Combine(map.FullName, "src", "MijnBesluit.Web", "Components");
|
|
if (!Directory.Exists(componenten)) return;
|
|
|
|
foreach (var bestand in Directory.GetFiles(componenten, "*.razor", SearchOption.AllDirectories))
|
|
{
|
|
if (Path.GetFileName(bestand) == "App.razor") continue; // dát is de kop
|
|
Assert.True(!File.ReadAllText(bestand).Contains("<script", StringComparison.OrdinalIgnoreCase),
|
|
$"{Path.GetFileName(bestand)} heeft geen eigen <script>");
|
|
}
|
|
|
|
var kop = Path.Combine(componenten, "App.razor");
|
|
var pagina = Path.Combine(componenten, "Pages", "AdverteerderNieuw.razor");
|
|
if (File.Exists(kop) && File.Exists(pagina))
|
|
{
|
|
Assert.True(File.ReadAllText(kop).Contains("adverteren.js"),
|
|
"de rekensom van het boekscherm wordt in de kop geladen");
|
|
|
|
var js = Path.Combine(map.FullName, "src", "MijnBesluit.Web", "wwwroot", "adverteren.js");
|
|
Assert.True(File.Exists(js), "en dat bestand staat er ook");
|
|
|
|
// De pagina roept hem aan, anders staat er een rekensom die nooit loopt.
|
|
Assert.True(File.ReadAllText(pagina).Contains("mbPrijs()"),
|
|
"en het boekscherm roept hem aan bij het typen");
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------- het overzicht
|
|
runner.TestAsync("Wie zich aanmeldt krijgt de vraag of hij de aantekening van een ander is", async () =>
|
|
{
|
|
// De middenweg uit docs/Identiteit.md. Automatisch koppelen op e-mailadres doen we niet -
|
|
// een adres is geen identiteit - maar iemand die net met een code heeft bewezen bij dat
|
|
// postvak te kunnen, mag de vraag wél krijgen.
|
|
var w = new World();
|
|
var jonathan = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jonathan.PersonId);
|
|
|
|
Assert.True((await w.Families.AddMemberAsync(familie.Id, jonathan.Id, "Jan", "de Bruin",
|
|
"jan@example.nl", RelationType.Vriend)).Succeeded,
|
|
"Jonathan legt een aantekening over Jan aan");
|
|
|
|
// Jan meldt zich zelf aan. Zijn eigen profiel is NIET dat van Jonathan.
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "de Bruin");
|
|
|
|
var aanbod = await w.Accounts.AantekeningenOverMijAsync(jan.Id);
|
|
Assert.True(aanbod.Count == 1, $"Jan krijgt één vraag (nu: {aanbod.Count})");
|
|
Assert.True(aanbod[0].Van == "Jonathan Flietstra", "met de naam van degene die hem aanmaakte erbij");
|
|
Assert.True(aanbod[0].Relatie == "Vriend", "en wat hij van hem zou zijn");
|
|
|
|
// Jonathan ziet hier niets van: zijn aantekening staat er nog gewoon, onveranderd.
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, jonathan.Id);
|
|
Assert.True(leden.Any(l => l.FirstName == "Jan" && l.State != ProfileState.Geclaimd),
|
|
"en bij Jonathan is er nog niets veranderd");
|
|
});
|
|
|
|
runner.TestAsync("\"Ja, dat ben ik\" voegt de aantekening bij je eigen profiel", async () =>
|
|
{
|
|
var w = new World();
|
|
var jonathan = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jonathan.PersonId);
|
|
|
|
await w.Families.AddMemberAsync(familie.Id, jonathan.Id, "Jan", "de Bruin",
|
|
"jan@example.nl", RelationType.Vriend);
|
|
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "de Bruin");
|
|
var aanbod = await w.Accounts.AantekeningenOverMijAsync(jan.Id);
|
|
|
|
var overgenomen = await w.Accounts.NeemAantekeningOverAsync(aanbod[0].PersoonId, jan.Id, w.Samenvoegen);
|
|
Assert.True(overgenomen.Succeeded, $"overnemen lukt ({overgenomen.Error})");
|
|
|
|
// Bij Jonathan staat nu Jans eigen profiel, en dat is geclaimd.
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, jonathan.Id);
|
|
var jansKaart = leden.SingleOrDefault(l => l.PersonId == jan.PersonId);
|
|
Assert.True(jansKaart is not null, "Jan staat nog steeds bij Jonathan in de familie");
|
|
Assert.True(jansKaart!.State == ProfileState.Geclaimd, "en nu als geclaimd");
|
|
Assert.True(jansKaart.Relation == RelationType.Vriend, "met de relatie die Jonathan had ingevuld");
|
|
|
|
// En de vraag is weg: er valt niets meer over te beslissen.
|
|
Assert.True((await w.Accounts.AantekeningenOverMijAsync(jan.Id)).Count == 0,
|
|
"de vraag komt niet terug");
|
|
});
|
|
|
|
runner.TestAsync("\"Nee, dat ben ik niet\" laat alles staan en vraagt het niet nog eens", async () =>
|
|
{
|
|
// Twee mensen kunnen hetzelfde adres gebruiken - een echtpaar, een ouder die het adres van
|
|
// zijn kind gebruikt. Dan is "nee" het goede antwoord, en dat hoort te blijven staan.
|
|
var w = new World();
|
|
var jonathan = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jonathan.PersonId);
|
|
|
|
await w.Families.AddMemberAsync(familie.Id, jonathan.Id, "Karin", "Flietstra",
|
|
"samen@example.nl", RelationType.Partner);
|
|
|
|
var iemandAnders = await w.MaakGebruikerAsync("samen@example.nl", "Jan", "de Bruin");
|
|
var aanbod = await w.Accounts.AantekeningenOverMijAsync(iemandAnders.Id);
|
|
Assert.True(aanbod.Count == 1, "de vraag komt");
|
|
|
|
Assert.True((await w.Accounts.WijsAantekeningAfAsync(aanbod[0].PersoonId, iemandAnders.Id)).Succeeded,
|
|
"afwijzen lukt");
|
|
|
|
Assert.True((await w.Accounts.AantekeningenOverMijAsync(iemandAnders.Id)).Count == 0,
|
|
"en de vraag komt niet terug");
|
|
|
|
// Karin staat nog gewoon bij Jonathan, met alles erop en eraan.
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, jonathan.Id);
|
|
Assert.True(leden.Any(l => l.FirstName == "Karin" && l.State != ProfileState.Geclaimd),
|
|
"en de aantekening van Jonathan staat er nog precies zo");
|
|
});
|
|
|
|
runner.TestAsync("Je kunt alleen aantekeningen over je eigen adres overnemen", async () =>
|
|
{
|
|
// Dit komt van een scherm, en een scherm is een wens en geen bewijs. Zonder deze regel kon
|
|
// iemand met een willekeurig persoonsnummer andermans aantekening naar zich toe trekken.
|
|
var w = new World();
|
|
var jonathan = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jonathan.PersonId);
|
|
|
|
var toegevoegd = await w.Families.AddMemberAsync(familie.Id, jonathan.Id, "Jan", "de Bruin",
|
|
"jan@example.nl", RelationType.Vriend);
|
|
Assert.True(toegevoegd.Succeeded, "Jan staat erbij");
|
|
|
|
// Een vreemde, met een heel ander adres.
|
|
var vreemde = await w.MaakGebruikerAsync("vreemde@example.nl", "Vera", "Vreemd");
|
|
|
|
Assert.True((await w.Accounts.AantekeningenOverMijAsync(vreemde.Id)).Count == 0,
|
|
"zij krijgt niets te zien");
|
|
|
|
var jansAantekening = w.Store.People.Single(p => p.Email == "jan@example.nl");
|
|
Assert.True(!(await w.Accounts.NeemAantekeningOverAsync(jansAantekening.Id, vreemde.Id, w.Samenvoegen)).Succeeded,
|
|
"en overnemen lukt haar niet");
|
|
Assert.True(!(await w.Accounts.WijsAantekeningAfAsync(jansAantekening.Id, vreemde.Id)).Succeeded,
|
|
"afwijzen ook niet");
|
|
});
|
|
|
|
runner.TestAsync("Je eigen aantekeningen krijg je niet aangeboden", async () =>
|
|
{
|
|
// Wie een profiel voor zijn partner aanmaakt op zijn eigen adres, hoort niet gevraagd te
|
|
// worden of hij zijn partner is.
|
|
var w = new World();
|
|
var jonathan = await w.MaakGebruikerAsync("jonathan@example.nl", "Jonathan", "Flietstra");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jonathan.PersonId);
|
|
|
|
await w.Families.AddMemberAsync(familie.Id, jonathan.Id, "Karin", "Flietstra",
|
|
"jonathan@example.nl", RelationType.Partner);
|
|
|
|
Assert.True((await w.Accounts.AantekeningenOverMijAsync(jonathan.Id)).Count == 0,
|
|
"hij krijgt zijn eigen aantekening niet aangeboden");
|
|
});
|
|
|
|
runner.Test("Datums staan in het Nederlands, ook met invariante cultuur", () =>
|
|
{
|
|
// De hele oplossing draait met InvariantGlobalization aan: goed voor de diensten, maar het
|
|
// betekent dat er maar één cultuur bestaat en die is Engels. ToString("d MMMM yyyy") gaf op
|
|
// de website dus "17 September 2026" onder een Nederlandse zin, en "Thursday" op het
|
|
// overzicht. Vandaar dat de maandnamen met de hand gaan.
|
|
//
|
|
// Deze controle rekent het echt uit in plaats van de lijst na te lopen: het gaat erom wat
|
|
// de gebruiker ziet.
|
|
var moment = new DateTime(2026, 9, 17, 13, 45, 0, DateTimeKind.Utc);
|
|
|
|
Assert.True(MijnBesluit.Domain.Rules.Datums.Datum(new DateOnly(2026, 9, 17)) == "17 september 2026",
|
|
"een kale datum is Nederlands");
|
|
Assert.True(MijnBesluit.Domain.Rules.Datums.Kort(new DateOnly(2026, 9, 17)) == "17 Sep. 2026",
|
|
"de korte vorm ook");
|
|
Assert.True(MijnBesluit.Domain.Rules.Datums.MetDag(moment).StartsWith("donderdag", StringComparison.Ordinal),
|
|
$"en de dag van de week ook (nu: {MijnBesluit.Domain.Rules.Datums.MetDag(moment)})");
|
|
|
|
// Niets van dit alles mag afhangen van welke cultuur er toevallig staat ingesteld.
|
|
Assert.True(!MijnBesluit.Domain.Rules.Datums.Datum(moment).Contains("September", StringComparison.Ordinal),
|
|
"nergens een Engelse maandnaam");
|
|
|
|
// En de app rekent met dezelfde lijst.
|
|
var domein = MijnBesluit.Domain.Rules.Datums.Maanden;
|
|
var app = MijnBesluit.Client.Datums.Maanden;
|
|
Assert.True(domein.SequenceEqual(app), "app en website gebruiken dezelfde maandnamen");
|
|
Assert.True(MijnBesluit.Domain.Rules.Datums.KorteMaanden.SequenceEqual(MijnBesluit.Client.Datums.KorteMaanden),
|
|
"en dezelfde afkortingen");
|
|
Assert.True(MijnBesluit.Domain.Rules.Datums.Dagen.SequenceEqual(MijnBesluit.Client.Datums.Dagen),
|
|
"en dezelfde dagen");
|
|
|
|
Assert.True(domein.Length == 12 && MijnBesluit.Domain.Rules.Datums.Dagen.Length == 7,
|
|
"twaalf maanden en zeven dagen");
|
|
});
|
|
|
|
runner.Test("Geen scherm maakt zelf een datum op", () =>
|
|
{
|
|
// MMMM en dddd volgen de taalinstelling, en met InvariantGlobalization is die Engels. Eén
|
|
// vergeten plek levert "Thursday 17 September 2026" op tussen Nederlandse tekst - en dat
|
|
// valt bij het bouwen nergens op, want het compileert prima.
|
|
var map = new DirectoryInfo(AppContext.BaseDirectory);
|
|
while (map is not null && !Directory.Exists(Path.Combine(map.FullName, "src"))) map = map.Parent;
|
|
if (map is null) return; // gebouwd buiten de projectmap
|
|
|
|
var mis = new List<string>();
|
|
|
|
foreach (var project in new[] { "MijnBesluit.Web", "MijnBesluit.AppUi" })
|
|
{
|
|
var projectmap = Path.Combine(map.FullName, "src", project);
|
|
if (!Directory.Exists(projectmap)) continue;
|
|
|
|
foreach (var bestand in Directory.GetFiles(projectmap, "*.razor", SearchOption.AllDirectories)
|
|
.Concat(Directory.GetFiles(projectmap, "*.cs", SearchOption.AllDirectories)))
|
|
{
|
|
if (bestand.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") ||
|
|
bestand.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")) continue;
|
|
|
|
var regels = File.ReadAllLines(bestand);
|
|
for (var i = 0; i < regels.Length; i++)
|
|
{
|
|
if (regels[i].TrimStart().StartsWith("//", StringComparison.Ordinal)) continue;
|
|
if (regels[i].Contains("MMMM", StringComparison.Ordinal) ||
|
|
regels[i].Contains("dddd", StringComparison.Ordinal))
|
|
mis.Add($"{Path.GetFileName(bestand)} regel {i + 1}");
|
|
}
|
|
}
|
|
}
|
|
|
|
Assert.True(mis.Count == 0,
|
|
"gebruik Datums.Datum/MetDag in plaats van eigen opmaak, in: " + string.Join(", ", mis));
|
|
});
|
|
|
|
runner.Test("De lijst met relaties is in app en website gelijk", () =>
|
|
{
|
|
// "Wat is het van je?" staat twee keer op schijf: in het domein voor de website, en in
|
|
// MijnBesluit.Client voor de app, want de app ziet het domein niet. Lopen ze uit elkaar,
|
|
// dan kiest iemand in de app "Schoonzoon" en staat er op de website "Familie" - of erger,
|
|
// een verkeerde relatie, want het gaat over een nummer.
|
|
var domein = MijnBesluit.Domain.Rules.Relaties.Keuzes;
|
|
var app = MijnBesluit.Client.Relaties.Keuzes;
|
|
|
|
Assert.True(domein.Length == app.Length,
|
|
$"beide lijsten zijn even lang (domein: {domein.Length}, app: {app.Length})");
|
|
|
|
for (var i = 0; i < Math.Min(domein.Length, app.Length); i++)
|
|
{
|
|
Assert.True((int)domein[i].Waarde == app[i].Waarde && domein[i].Naam == app[i].Naam,
|
|
$"nummer {i + 1} is in allebei {(int)domein[i].Waarde} \"{domein[i].Naam}\" " +
|
|
$"(app: {app[i].Waarde} \"{app[i].Naam}\")");
|
|
}
|
|
});
|
|
|
|
runner.Test("Relaties die niet meer te kiezen zijn, blijven wel leesbaar", () =>
|
|
{
|
|
// Kind, Bonuskind, Ouder, Bonusouder, Kleinkind en Grootouder zijn in september 2026 uit de
|
|
// keuzelijst gehaald: mensen zeggen "mijn oma", niet "mijn grootouder". Maar ze staan als
|
|
// nummer in de database bij iedereen die ze eerder koos. Verdwijnt de vertaling, dan staat
|
|
// er bij die mensen ineens "Familie" - een stille gegevensverlies dat niemand meldt.
|
|
var oud = new[]
|
|
{
|
|
(RelationType.Kind, "Kind"), (RelationType.BonusKind, "Bonuskind"),
|
|
(RelationType.Ouder, "Ouder"), (RelationType.BonusOuder, "Bonusouder"),
|
|
(RelationType.Kleinkind, "Kleinkind"), (RelationType.Grootouder, "Grootouder")
|
|
};
|
|
|
|
foreach (var (waarde, naam) in oud)
|
|
{
|
|
Assert.True(MijnBesluit.Domain.Rules.Relaties.Naam(waarde) == naam,
|
|
$"{naam} is nog te lezen op de website");
|
|
Assert.True(MijnBesluit.Client.Relaties.Naam((int)waarde) == naam,
|
|
$"{naam} is nog te lezen in de app");
|
|
Assert.True(!MijnBesluit.Domain.Rules.Relaties.Keuzes.Any(k => k.Waarde == waarde),
|
|
$"{naam} staat niet meer in de keuzelijst");
|
|
}
|
|
|
|
// En elke keuze die er wél is, heeft in allebei dezelfde naam als in de lijst zelf.
|
|
foreach (var (waarde, naam) in MijnBesluit.Domain.Rules.Relaties.Keuzes)
|
|
{
|
|
Assert.True(MijnBesluit.Domain.Rules.Relaties.Naam(waarde) == naam, $"{naam} vertaalt naar zichzelf");
|
|
Assert.True(MijnBesluit.Client.Relaties.Naam((int)waarde) == naam, $"{naam} doet dat in de app ook");
|
|
}
|
|
});
|
|
|
|
runner.TestAsync("Een vriendin staat bij de vrienden en niet tussen de familie", async () =>
|
|
{
|
|
// De sortering zet familie bovenaan op leeftijd en vrienden daaronder op alfabet. Die regel
|
|
// kijkt naar de soort relatie, en toen Vriendin erbij kwam hoorde die mee te gaan - anders
|
|
// staat zij ineens tussen de kleinkinderen. Dat is het soort fout dat je pas ziet als er
|
|
// iemand met die relatie in je lijst staat.
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
async Task Toevoegen(string voornaam, RelationType relatie, DateOnly? geboren) =>
|
|
Assert.True((await w.Families.AddMemberAsync(familie.Id, jan.Id, voornaam, "Jansen",
|
|
null, relatie, null, geboren)).Succeeded,
|
|
$"{voornaam} toevoegen lukt");
|
|
|
|
await Toevoegen("Aaltje", RelationType.Vriendin, new DateOnly(1940, 1, 1)); // oudste van allemaal
|
|
await Toevoegen("Tim", RelationType.Kleinzoon, new DateOnly(2018, 5, 4));
|
|
await Toevoegen("Bea", RelationType.Schoondochter, new DateOnly(1985, 2, 2));
|
|
|
|
var leden = (await w.Families.ListMembersAsync(familie.Id, jan.Id))
|
|
.Where(l => l.PersonId != jan.PersonId)
|
|
.Select(l => l.FirstName)
|
|
.ToList();
|
|
|
|
Assert.True(leden.Last() == "Aaltje",
|
|
$"de vriendin staat onderaan, ondanks haar leeftijd (nu: {string.Join(", ", leden)})");
|
|
Assert.True(leden.IndexOf("Bea") < leden.IndexOf("Tim"),
|
|
"en de familie staat daarboven, oudste eerst");
|
|
});
|
|
|
|
runner.Test("De lijst met sociale media is in app en website gelijk", () =>
|
|
{
|
|
// De app ziet het domein niet, dus die lijst staat twee keer. Dat mag, mits ze gelijk
|
|
// blijven: kiest iemand in de app "Mastodon" en kent de website dat niet, dan staat er
|
|
// straks een leeg vakje in zijn kluis.
|
|
var domein = MijnBesluit.Domain.Rules.SocialeMedia.Namen;
|
|
var app = MijnBesluit.Client.SocialeMedia.Namen;
|
|
|
|
Assert.True(domein.SequenceEqual(app),
|
|
$"beide lijsten zijn gelijk (domein: {domein.Length}, app: {app.Length})");
|
|
Assert.True(domein.Contains(MijnBesluit.Domain.Rules.SocialeMedia.Anders),
|
|
"\"Anders\" staat erin, zodat je ook iets kunt invullen wat er niet bij staat");
|
|
});
|
|
|
|
runner.TestAsync("Familie: eerst de familie op leeftijd, daarna de rest op alfabet", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
async Task Toevoegen(string voornaam, RelationType relatie, DateOnly? geboren) =>
|
|
Assert.True((await w.Families.AddMemberAsync(familie.Id, jan.Id, voornaam, "Jansen",
|
|
null, relatie, null, geboren)).Succeeded,
|
|
$"{voornaam} toevoegen lukt");
|
|
|
|
// Bewust door elkaar toegevoegd, zodat de volgorde echt van het sorteren komt.
|
|
await Toevoegen("Zoë", RelationType.Vriend, new DateOnly(1950, 1, 1)); // vriend: hoort onderaan
|
|
await Toevoegen("Eva", RelationType.Dochter, new DateOnly(2010, 5, 4));
|
|
await Toevoegen("Karin", RelationType.Partner, new DateOnly(1968, 3, 12));
|
|
await Toevoegen("Anna", RelationType.Vriend, null); // vriend zonder datum
|
|
await Toevoegen("Tim", RelationType.Zoon, new DateOnly(2006, 11, 30));
|
|
await Toevoegen("Wim", RelationType.Broer, null); // familie zonder datum
|
|
|
|
var leden = (await w.Families.ListMembersAsync(familie.Id, jan.Id))
|
|
.Where(l => l.PersonId != jan.PersonId)
|
|
.Select(l => l.FirstName)
|
|
.ToList();
|
|
|
|
// Familie eerst, oudste bovenaan. Wie geen geboortedatum heeft kan niet op leeftijd en
|
|
// sluit de familie af. Daarna de vrienden op alfabet - dus niet op leeftijd, ook al
|
|
// heeft Zoë wel een datum.
|
|
var verwacht = new[] { "Karin", "Tim", "Eva", "Wim", "Anna", "Zoë" };
|
|
|
|
Assert.True(leden.SequenceEqual(verwacht),
|
|
$"de volgorde klopt (kreeg: {string.Join(", ", leden)})");
|
|
});
|
|
|
|
runner.TestAsync("Familie: wanneer iemand is uitgenodigd en wanneer hij het profiel overnam", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
var toegevoegd = await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter);
|
|
Assert.True(toegevoegd.Succeeded, "Eva toevoegen lukt");
|
|
var evaId = toegevoegd.Value!.Id;
|
|
|
|
// Vóór de uitnodiging: wel een aanmaakmoment, verder niets.
|
|
var voor = (await w.Families.ListMembersAsync(familie.Id, jan.Id)).Single(l => l.PersonId == evaId);
|
|
Assert.True(voor.InvitedAt is null, "zonder uitnodiging staat er geen verzendmoment");
|
|
Assert.True(voor.ClaimedAt is null, "en het profiel is nog niet overgenomen");
|
|
Assert.True(voor.CreatedAt != default, "het aanmaakmoment is er wel");
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(1));
|
|
Assert.True((await w.Families.InviteAsync(familie.Id, evaId, jan.Id)).Succeeded, "uitnodigen lukt");
|
|
|
|
var na = (await w.Families.ListMembersAsync(familie.Id, jan.Id)).Single(l => l.PersonId == evaId);
|
|
Assert.True(na.State == ProfileState.Uitgenodigd, "de stand is nu uitgenodigd");
|
|
Assert.True(na.InvitedAt is not null, "en we weten wanneer de uitnodiging uitging");
|
|
Assert.True(na.InvitationExpiresAt > w.Clock.UtcNow, "de uitnodiging is nog geldig");
|
|
Assert.True(na.InvitationResends == 0, "hij is nog niet opnieuw verstuurd");
|
|
|
|
// Opnieuw uitnodigen telt mee, zodat je op het scherm kunt zien dat je het al drie keer
|
|
// hebt geprobeerd en het dus niet aan de ontvanger ligt.
|
|
w.Clock.Advance(TimeSpan.FromDays(1));
|
|
await w.Families.InviteAsync(familie.Id, evaId, jan.Id);
|
|
var nogmaals = (await w.Families.ListMembersAsync(familie.Id, jan.Id)).Single(l => l.PersonId == evaId);
|
|
Assert.True(nogmaals.InvitationResends >= 1, "een tweede uitnodiging wordt geteld");
|
|
|
|
// Eva neemt het profiel over.
|
|
var mail = w.Email.Sent.Last(m => m.To == "eva@example.nl");
|
|
var token = mail.Body.Split("/uitnodiging/")[1].Split('"')[0];
|
|
w.Clock.Advance(TimeSpan.FromDays(2));
|
|
Assert.True((await w.Families.AcceptInvitationAsync(token, w.Accounts)).Succeeded, "Eva kan claimen");
|
|
|
|
var geclaimd = (await w.Families.ListMembersAsync(familie.Id, jan.Id)).Single(l => l.PersonId == evaId);
|
|
Assert.True(geclaimd.State == ProfileState.Geclaimd, "het profiel is nu van Eva");
|
|
Assert.True(geclaimd.ClaimedAt is not null, "en we weten sinds wanneer");
|
|
Assert.True(geclaimd.ClaimedAt > geclaimd.InvitedAt, "overnemen komt na uitnodigen");
|
|
});
|
|
|
|
runner.TestAsync("Overzicht: wat er speelt, en wat er bewust niet in staat", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen", "eva@example.nl", RelationType.Dochter);
|
|
|
|
// Jan wijst Eva aan als beheerder van zijn nalatenschap.
|
|
Assert.True((await w.Legacy.AppointManagerAsync(jan.Id, eva.PersonId)).Succeeded, "aanwijzen lukt");
|
|
|
|
// Eva zet een moment klaar voor Jan, over tien dagen.
|
|
await w.Messages.ComposeAsync(eva.Id, jan.PersonId, "Verjaardag", "Gefeliciteerd!",
|
|
w.Clock.UtcNow.AddDays(10));
|
|
|
|
// En eentje die pas na haar overlijden komt.
|
|
await w.Messages.ComposeAsync(eva.Id, jan.PersonId, "Als ik er niet meer ben", "Dag lieve pap",
|
|
null, afterDeath: true);
|
|
|
|
var overzicht = (await w.Overzicht.VoorAsync(jan.Id)).Value!;
|
|
|
|
Assert.True(overzicht.AantalFamilie == 2, "de aantallen kloppen");
|
|
Assert.True(overzicht.Regels.Any(r => r.Soort == "beheerder" && r.Tekst.Contains("Eva")),
|
|
"er staat wie zijn nalatenschap beheert");
|
|
Assert.True(overzicht.Regels.Any(r => r.Soort == "moment" && r.Tekst.Contains("Eva")),
|
|
"en welk moment eraan komt");
|
|
|
|
// Dit is het punt: een bericht voor na iemands overlijden hoort niet op een beginscherm.
|
|
// Anders staat er "op 12 maart komt er iets van Eva" - en dat gebeurt alleen als zij er
|
|
// dan niet meer is.
|
|
Assert.True(overzicht.Regels.Count(r => r.Soort == "moment") == 1,
|
|
"het bericht voor na een overlijden staat er niet bij");
|
|
|
|
// Eva ziet aan haar kant dat zij die taak heeft.
|
|
var vanEva = (await w.Overzicht.VoorAsync(eva.Id)).Value!;
|
|
Assert.True(vanEva.Regels.Any(r => r.Soort == "beheer-voor" && r.Tekst.Contains("Jan")),
|
|
"Eva ziet dat zij de nalatenschap van Jan beheert");
|
|
});
|
|
|
|
runner.TestAsync("Overzicht: de vooruitblik is in te stellen, binnen grenzen", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva");
|
|
|
|
await w.Messages.ComposeAsync(eva.Id, jan.PersonId, "Over veertig dagen", "Hallo",
|
|
w.Clock.UtcNow.AddDays(40));
|
|
|
|
Assert.True((await w.Overzicht.VoorAsync(jan.Id)).Value!.Regels.All(r => r.Soort != "moment"),
|
|
"standaard kijkt hij 30 dagen vooruit, dus dit valt er nog buiten");
|
|
|
|
Assert.True((await w.Overzicht.VooruitblikAsync(jan.Id, 60)).Succeeded, "60 dagen mag");
|
|
Assert.True((await w.Overzicht.VoorAsync(jan.Id)).Value!.Regels.Any(r => r.Soort == "moment"),
|
|
"en dan valt het er wel binnen");
|
|
|
|
Assert.True((await w.Overzicht.VooruitblikAsync(jan.Id, 0)).Succeeded, "0 mag ook");
|
|
Assert.True((await w.Overzicht.VoorAsync(jan.Id)).Value!.Regels.All(r => r.Soort != "moment"),
|
|
"en dan zie je niets vooruit");
|
|
|
|
Assert.True(!(await w.Overzicht.VooruitblikAsync(jan.Id, 61)).Succeeded, "meer dan 60 niet");
|
|
Assert.True(!(await w.Overzicht.VooruitblikAsync(jan.Id, -1)).Succeeded, "en negatief ook niet");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- meldingen
|
|
runner.Test("De handtekening waarmee we bij Google aankloppen klopt", () =>
|
|
{
|
|
// Firebase laat je een servicesleutel downloaden; daarmee maken we een JWT en ruilen die
|
|
// in voor een toegangstoken. Gaat hier iets mis, dan komt er nooit een melding aan en zegt
|
|
// Google alleen "invalid_grant". Dus controleren we het hier, met een verzonnen sleutel.
|
|
using var rsa = System.Security.Cryptography.RSA.Create(2048);
|
|
var json = System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string>
|
|
{
|
|
["type"] = "service_account",
|
|
["project_id"] = "mijnbesluit-test",
|
|
["client_email"] = "meldingen@mijnbesluit-test.iam.gserviceaccount.com",
|
|
["private_key"] = rsa.ExportPkcs8PrivateKeyPem()
|
|
});
|
|
|
|
var sleutel = MijnBesluit.Infrastructure.Meldingen.Servicesleutel.Lees(json);
|
|
Assert.True(sleutel.VerzendAdres.Contains("mijnbesluit-test"), "het projectnummer zit in het verzendadres");
|
|
|
|
var nu = new DateTime(2026, 9, 15, 10, 0, 0, DateTimeKind.Utc);
|
|
var jwt = sleutel.MaakJwt(nu);
|
|
var delen = jwt.Split('.');
|
|
|
|
static byte[] VanBase64Url(string s)
|
|
{
|
|
s = s.Replace('-', '+').Replace('_', '/');
|
|
return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
|
|
}
|
|
|
|
Assert.True(delen.Length == 3, "een JWT bestaat uit drie delen");
|
|
Assert.True(!jwt.Contains('='), "base64url kent geen opvultekens");
|
|
|
|
var kop = System.Text.Json.JsonDocument.Parse(VanBase64Url(delen[0])).RootElement;
|
|
var inhoud = System.Text.Json.JsonDocument.Parse(VanBase64Url(delen[1])).RootElement;
|
|
|
|
Assert.True(kop.GetProperty("alg").GetString() == "RS256", "ondertekend met RS256");
|
|
Assert.True(inhoud.GetProperty("aud").GetString() == "https://oauth2.googleapis.com/token",
|
|
"gericht aan de tokendienst van Google");
|
|
Assert.True(inhoud.GetProperty("scope").GetString() == "https://www.googleapis.com/auth/firebase.messaging",
|
|
"met alleen het recht om meldingen te sturen");
|
|
Assert.True(inhoud.GetProperty("exp").GetInt64() - inhoud.GetProperty("iat").GetInt64() == 3600,
|
|
"een uur geldig, het maximum dat Google toestaat");
|
|
|
|
var teTekenen = System.Text.Encoding.ASCII.GetBytes($"{delen[0]}.{delen[1]}");
|
|
Assert.True(rsa.VerifyData(teTekenen, VanBase64Url(delen[2]),
|
|
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
|
System.Security.Cryptography.RSASignaturePadding.Pkcs1),
|
|
"de handtekening hoort bij de sleutel");
|
|
|
|
using var andere = System.Security.Cryptography.RSA.Create(2048);
|
|
Assert.True(!andere.VerifyData(teTekenen, VanBase64Url(delen[2]),
|
|
System.Security.Cryptography.HashAlgorithmName.SHA256,
|
|
System.Security.Cryptography.RSASignaturePadding.Pkcs1),
|
|
"en niet bij een andere sleutel");
|
|
|
|
Assert.Throws<InvalidOperationException>(
|
|
() => MijnBesluit.Infrastructure.Meldingen.Servicesleutel.Lees("""{"project_id":"x"}"""),
|
|
"een half bestand wordt geweigerd");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- meldingen
|
|
runner.TestAsync("Meldingen: het toestel volgt de gebruiker, niet andersom", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva");
|
|
|
|
Assert.True((await w.Meldingen.AanmeldenAsync(jan.Id, "kenmerk-telefoon", "android", "Galaxy S26")).Succeeded,
|
|
"aanmelden lukt");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(jan.Id)).Count == 1, "Jan heeft één toestel");
|
|
|
|
// Nog een keer hetzelfde kenmerk: bijwerken, geen tweede regel.
|
|
await w.Meldingen.AanmeldenAsync(jan.Id, "kenmerk-telefoon", "android", "Galaxy S26");
|
|
Assert.True(w.Store.Toestellen.Count == 1, "hetzelfde kenmerk levert geen tweede toestel op");
|
|
|
|
// Eva logt in op diezelfde telefoon. Dan horen de meldingen bij haar, niet meer bij Jan.
|
|
await w.Meldingen.AanmeldenAsync(eva.Id, "kenmerk-telefoon", "android", "Galaxy S26");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(jan.Id)).Count == 0, "Jan krijgt er niets meer op");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(eva.Id)).Count == 1, "Eva wel");
|
|
|
|
// Afmelden van iemand anders' toestel doet niets.
|
|
await w.Meldingen.AfmeldenAsync(jan.Id, "kenmerk-telefoon");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(eva.Id)).Count == 1, "Jan kan Eva's toestel niet afmelden");
|
|
|
|
await w.Meldingen.AfmeldenAsync(eva.Id, "kenmerk-telefoon");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(eva.Id)).Count == 0, "zelf afmelden lukt wel");
|
|
});
|
|
|
|
runner.TestAsync("Meldingen: een bezorgd bericht geeft een melding, zonder de inhoud", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva", "Jansen");
|
|
|
|
await w.Meldingen.AanmeldenAsync(eva.Id, "kenmerk-eva", "android");
|
|
|
|
await w.Messages.ComposeAsync(jan.Id, eva.PersonId, "Wachtwoord van de kluis", "Het is hunter2");
|
|
await w.Messages.DeliverDueAsync();
|
|
|
|
Assert.True(w.Push.Verstuurd.Count == 1, "er is één melding verstuurd");
|
|
var melding = w.Push.Verstuurd.Single();
|
|
Assert.True(melding.Kenmerk == "kenmerk-eva", "naar het toestel van de ontvanger");
|
|
Assert.True(melding.Tekst.Contains("Jan"), "met de naam van de afzender erin");
|
|
|
|
// Dit is het punt: op een scherm dat op tafel ligt hoort de inhoud niet te staan.
|
|
var alles = melding.Titel + " " + melding.Tekst;
|
|
Assert.True(!alles.Contains("hunter2"), "de inhoud van het bericht staat er niet in");
|
|
Assert.True(!alles.Contains("Wachtwoord van de kluis"), "het onderwerp ook niet");
|
|
});
|
|
|
|
runner.TestAsync("Meldingen: een toestel dat Google niet meer kent wordt afgemeld", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva");
|
|
|
|
await w.Meldingen.AanmeldenAsync(eva.Id, "oud-kenmerk", "android");
|
|
await w.Meldingen.AanmeldenAsync(eva.Id, "goed-kenmerk", "android");
|
|
w.Push.Onbekend.Add("oud-kenmerk");
|
|
|
|
var verstuurd = await w.Meldingen.StuurAsync(eva.Id, "Test", "Test");
|
|
Assert.True(verstuurd == 1, "alleen het werkende toestel krijgt hem");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(eva.Id)).Count == 1,
|
|
"het onbruikbare toestel is afgemeld en wordt niet meer geprobeerd");
|
|
|
|
// En zonder instellingen gebeurt er niets, zonder dat er iets omvalt.
|
|
w.Push.IsIngesteld = false;
|
|
Assert.True(await w.Meldingen.StuurAsync(eva.Id, "Test", "Test") == 0, "zonder Firebase gaat er niets uit");
|
|
});
|
|
|
|
runner.TestAsync("Meldingen: de proefmelding zegt precies waar het op vastloopt", async () =>
|
|
{
|
|
// Blijft een melding uit, dan zijn er drie oorzaken die er van buiten hetzelfde uitzien.
|
|
// Deze knop hoort ze uit elkaar te houden; als hij het verkeerde zegt, stuurt hij iemand
|
|
// een uur de verkeerde kant op.
|
|
var w = new World();
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva", "Jansen");
|
|
|
|
// 1. Geen servicesleutel op de server.
|
|
w.Push.IsIngesteld = false;
|
|
var zonderSleutel = await w.Meldingen.ProefAsync(eva.Id);
|
|
Assert.True(!zonderSleutel.Ingesteld && !zonderSleutel.Gelukt, "zonder sleutel lukt het niet");
|
|
Assert.True(zonderSleutel.Uitleg.Contains("servicesleutel"), "en dat is wat er staat");
|
|
|
|
// 2. Sleutel wel, maar geen toestel aangemeld.
|
|
w.Push.IsIngesteld = true;
|
|
var zonderToestel = await w.Meldingen.ProefAsync(eva.Id);
|
|
Assert.True(zonderToestel.Ingesteld && zonderToestel.Toestellen == 0, "de sleutel is er, het toestel niet");
|
|
Assert.True(zonderToestel.Uitleg.Contains("geen toestel"), "en dat is wat er staat");
|
|
|
|
// 3. Allebei goed.
|
|
await w.Meldingen.AanmeldenAsync(eva.Id, "kenmerk-van-eva", "android", "Eva's telefoon");
|
|
var goed = await w.Meldingen.ProefAsync(eva.Id);
|
|
Assert.True(goed.Gelukt && goed.Verstuurd == 1, "nu gaat hij eruit");
|
|
Assert.True(w.Push.Verstuurd.Single().Kenmerk == "kenmerk-van-eva", "naar haar eigen toestel");
|
|
|
|
// 4. Google kent het kenmerk niet meer: dan wordt het toestel afgemeld en zegt de uitleg dat.
|
|
w.Push.Onbekend.Add("kenmerk-van-eva");
|
|
var verlopen = await w.Meldingen.ProefAsync(eva.Id);
|
|
Assert.True(!verlopen.Gelukt, "een verlopen kenmerk levert niets op");
|
|
Assert.True(verlopen.Uitleg.Contains("opnieuw in"), "en de uitleg zegt wat je eraan doet");
|
|
Assert.True((await w.Meldingen.ToestellenAsync(eva.Id)).Count == 0, "het toestel is afgemeld");
|
|
|
|
// En een proefmelding gaat nooit naar iemand anders.
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
await w.Meldingen.AanmeldenAsync(jan.Id, "kenmerk-van-jan", "android");
|
|
w.Push.Verstuurd.Clear();
|
|
await w.Meldingen.ProefAsync(eva.Id);
|
|
Assert.True(w.Push.Verstuurd.All(v => v.Kenmerk != "kenmerk-van-jan"),
|
|
"het toestel van Jan krijgt niets van Eva's proef");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- videoberichten
|
|
runner.TestAsync("Videobericht: de ontvanger kan de opname ook echt openen", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva", "Jansen");
|
|
|
|
// Jan neemt iets op. Het bestand staat los in de kluis, niet bij een item.
|
|
var opname = System.Text.Encoding.UTF8.GetBytes("dit stelt een filmpje voor");
|
|
var bestand = (await w.Vault.StoreFileAsync(jan.Id, "opname.mp4", "video/mp4", opname)).Value!;
|
|
|
|
Assert.True(!(await w.Vault.OpenFileAsync(bestand.Id, eva.Id)).Succeeded,
|
|
"Eva kan er nog niet bij");
|
|
|
|
var bericht = (await w.Messages.ComposeAsync(jan.Id, eva.PersonId, "Voor jou", "Kijk maar",
|
|
null, afterDeath: false, MessageKind.Video, bestand.Id)).Value!;
|
|
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Store.Messages.Single(m => m.Id == bericht.Id).Status == MessageStatus.Bezorgd,
|
|
"het bericht is bezorgd");
|
|
|
|
// Hier zat het gat: zonder toegang tot het bestand krijgt Eva wel de tekst maar geen beeld.
|
|
var bij = await w.Vault.OpenFileAsync(bestand.Id, eva.Id);
|
|
Assert.True(bij.Succeeded, $"de ontvanger kan de opname openen ({bij.Error})");
|
|
Assert.True(bij.Value.Inhoud.SequenceEqual(opname), "en het is dezelfde opname");
|
|
|
|
// Iemand die niets met dit bericht te maken heeft, kan er niet bij.
|
|
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
|
Assert.True(!(await w.Vault.OpenFileAsync(bestand.Id, piet.Id)).Succeeded, "een derde kan er niet bij");
|
|
});
|
|
|
|
runner.TestAsync("Bijlagen: bij het item, en weg met het item", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
|
|
var item = (await w.Vault.CreateAsync(jan.Id, VaultCategory.Polis, "Uitvaartpolis",
|
|
new VaultPayload { Insurer = "Verzekeraar" })).Value!;
|
|
|
|
var inhoud = System.Text.Encoding.UTF8.GetBytes("de polis als pdf");
|
|
var bijlage = (await w.Vault.StoreFileAsync(jan.Id, "polis.pdf", "application/pdf", inhoud, item.Id)).Value!;
|
|
|
|
var lijst = await w.Vault.ListFilesAsync(item.Id, jan.Id);
|
|
Assert.True(lijst.Succeeded && lijst.Value!.Count == 1, "de bijlage hangt aan het item");
|
|
|
|
var geopend = await w.Vault.OpenFileAsync(bijlage.Id, jan.Id);
|
|
Assert.True(geopend.Succeeded && geopend.Value.Inhoud.SequenceEqual(inhoud), "en is te openen");
|
|
|
|
// Padaanduidingen in de naam horen er niet in te komen.
|
|
var gemeen = (await w.Vault.StoreFileAsync(jan.Id, @"..\..\windows\system32\wachtwoorden.txt",
|
|
"text/plain", inhoud, item.Id)).Value!;
|
|
Assert.True(gemeen.FileName == "wachtwoorden.txt",
|
|
$"de bestandsnaam bevat geen pad meer ({gemeen.FileName})");
|
|
|
|
var unix = (await w.Vault.StoreFileAsync(jan.Id, "../../etc/passwd", "text/plain", inhoud, item.Id)).Value!;
|
|
Assert.True(unix.FileName == "passwd", $"ook met schuine strepen ({unix.FileName})");
|
|
|
|
// Het item weg betekent de bijlagen weg.
|
|
Assert.True((await w.Vault.DeleteAsync(item.Id, jan.Id)).Succeeded, "het item is te verwijderen");
|
|
Assert.True(!(await w.Vault.OpenFileAsync(bijlage.Id, jan.Id)).Succeeded,
|
|
"de bijlage is daarmee ook weg");
|
|
});
|
|
|
|
runner.TestAsync("Bijlagen: te groot wordt geweigerd", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
|
|
var teGroot = new byte[VaultService.MaxBestandsgrootte + 1];
|
|
var uitkomst = await w.Vault.StoreFileAsync(jan.Id, "film.mp4", "video/mp4", teGroot);
|
|
|
|
Assert.True(!uitkomst.Succeeded, "een te groot bestand wordt geweigerd");
|
|
Assert.True(uitkomst.Error!.Contains("MB"), $"met de grens erbij ({uitkomst.Error})");
|
|
Assert.True(!(await w.Vault.StoreFileAsync(jan.Id, "leeg.txt", "text/plain", Array.Empty<byte>())).Succeeded,
|
|
"en een leeg bestand ook");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- wijzigen en verwijderen
|
|
runner.TestAsync("Kluisitem wijzigen en verwijderen: alleen door de eigenaar", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan");
|
|
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
|
|
|
var item = (await w.Vault.CreateAsync(jan.Id, VaultCategory.Wachtwoord, "Bank",
|
|
new VaultPayload { Website = "bank.nl", Secret = "eerstegeheim" })).Value!;
|
|
|
|
var doorEenAnder = await w.Vault.UpdateAsync(item.Id, piet.Id, VaultCategory.Wachtwoord, "Bank",
|
|
new VaultPayload { Secret = "gekaapt" }, ReleasePolicy.AlleenIkzelf);
|
|
Assert.True(!doorEenAnder.Succeeded, "iemand anders kan mijn kluisitem niet wijzigen");
|
|
|
|
var gewijzigd = await w.Vault.UpdateAsync(item.Id, jan.Id, VaultCategory.Wachtwoord, "Bank en sparen",
|
|
new VaultPayload { Website = "bank.nl", Secret = "tweedegeheim" }, ReleasePolicy.NaOverlijden);
|
|
Assert.True(gewijzigd.Succeeded, $"de eigenaar kan wijzigen ({gewijzigd.Error})");
|
|
|
|
var open = (await w.Vault.OpenAsync(item.Id, jan.Id)).Value!;
|
|
Assert.True(open.Secret == "tweedegeheim", "de nieuwe inhoud komt terug");
|
|
Assert.True(w.Store.VaultItems.Single(v => v.Id == item.Id).Title == "Bank en sparen", "en de nieuwe titel");
|
|
|
|
var eersteHash = System.Convert.ToBase64String(w.Store.VaultItems.Single(v => v.Id == item.Id).WrappedContentKey);
|
|
await w.Vault.UpdateAsync(item.Id, jan.Id, VaultCategory.Wachtwoord, "Bank en sparen",
|
|
new VaultPayload { Secret = "derdegeheim" }, ReleasePolicy.NaOverlijden);
|
|
Assert.True(System.Convert.ToBase64String(w.Store.VaultItems.Single(v => v.Id == item.Id).WrappedContentKey) != eersteHash,
|
|
"elke wijziging krijgt een nieuwe contentsleutel");
|
|
|
|
var doorEenAnderWeg = await w.Vault.DeleteAsync(item.Id, piet.Id);
|
|
Assert.True(!doorEenAnderWeg.Succeeded, "iemand anders kan mijn kluisitem niet verwijderen");
|
|
|
|
var weg = await w.Vault.DeleteAsync(item.Id, jan.Id);
|
|
Assert.True(weg.Succeeded, $"de eigenaar kan verwijderen ({weg.Error})");
|
|
Assert.True((await w.Vault.ListAsync(jan.PersonId)).Count == 0, "het item staat niet meer in de lijst");
|
|
Assert.True(!(await w.Vault.OpenAsync(item.Id, jan.Id)).Succeeded, "en is niet meer te openen");
|
|
Assert.True(!(await w.Vault.DeleteAsync(item.Id, jan.Id)).Succeeded, "twee keer verwijderen gaat niet");
|
|
});
|
|
|
|
runner.TestAsync("Bericht wijzigen en intrekken: alleen zolang het niet bezorgd is", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
var eva = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen", null, RelationType.Dochter)).Value!;
|
|
|
|
var bericht = (await w.Messages.ComposeAsync(jan.Id, eva.Id, "Voor jou", "Eerste versie",
|
|
null, afterDeath: true)).Value!;
|
|
|
|
var gewijzigd = await w.Messages.UpdateAsync(bericht.Id, jan.Id, "Voor jou, Eva", "Tweede versie", null, true);
|
|
Assert.True(gewijzigd.Succeeded, $"wijzigen lukt zolang het klaarstaat ({gewijzigd.Error})");
|
|
Assert.True(w.Store.Messages.Single(m => m.Id == bericht.Id).Subject == "Voor jou, Eva", "het onderwerp is bij");
|
|
|
|
var ingetrokken = await w.Messages.WithdrawAsync(bericht.Id, jan.Id);
|
|
Assert.True(ingetrokken.Succeeded, "intrekken lukt");
|
|
Assert.True((await w.Messages.OutboxAsync(jan.PersonId)).Count == 0, "het staat niet meer in uitgaande post");
|
|
Assert.True(!(await w.Messages.UpdateAsync(bericht.Id, jan.Id, "Nog eens", "Derde", null, true)).Succeeded,
|
|
"een ingetrokken bericht is niet meer te wijzigen");
|
|
|
|
// Een bezorgd bericht staat vast. Bezorgen kan alleen bij iemand met een eigen account,
|
|
// dus daar hebben we Piet voor nodig.
|
|
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet");
|
|
var tweede = (await w.Messages.ComposeAsync(jan.Id, piet.PersonId, "Nu meteen", "Hallo")).Value!;
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Store.Messages.Single(m => m.Id == tweede.Id).Status == MessageStatus.Bezorgd, "het is bezorgd");
|
|
|
|
var teLaat = await w.Messages.UpdateAsync(tweede.Id, jan.Id, "Toch niet", "Andere tekst", null, false);
|
|
Assert.True(!teLaat.Succeeded, "een bezorgd bericht kan niet meer gewijzigd worden");
|
|
Assert.True(!(await w.Messages.WithdrawAsync(tweede.Id, jan.Id)).Succeeded, "en ook niet meer ingetrokken");
|
|
});
|
|
|
|
runner.TestAsync("Familielid verwijderen: de koppeling verdwijnt, het profiel blijft", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
var eva = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter)).Value!;
|
|
await w.Families.InviteAsync(familie.Id, eva.Id, jan.Id);
|
|
|
|
Assert.True(!(await w.Families.VerwijderLidAsync(familie.Id, jan.PersonId, jan.Id)).Succeeded,
|
|
"jezelf uit je eigen familie halen kan niet");
|
|
|
|
var weg = await w.Families.VerwijderLidAsync(familie.Id, eva.Id, jan.Id);
|
|
Assert.True(weg.Succeeded, $"verwijderen lukt ({weg.Error})");
|
|
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, jan.Id);
|
|
Assert.True(leden.All(m => m.PersonId != eva.Id), "Eva staat niet meer in de familie");
|
|
Assert.True(w.Store.People.Any(p => p.Id == eva.Id), "haar profiel bestaat nog wel");
|
|
Assert.True(w.Store.Invitations.All(i => i.Status == InvitationStatus.Ingetrokken),
|
|
"de openstaande uitnodiging is ingetrokken");
|
|
});
|
|
|
|
runner.TestAsync("Familielid verwijderen: niet als hij je nalatenschap beheert", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
var eva = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter)).Value!;
|
|
|
|
// Eva is aangewezen om Jans nalatenschap te beheren.
|
|
Assert.True((await w.Legacy.AppointManagerAsync(jan.Id, eva.Id)).Succeeded, "aanwijzen lukt");
|
|
|
|
var geweigerd = await w.Families.VerwijderLidAsync(familie.Id, eva.Id, jan.Id);
|
|
Assert.True(!geweigerd.Succeeded, "weghalen kan niet zolang ze beheerder is");
|
|
Assert.True(geweigerd.Error!.Contains("Nalatenschap"), $"de reden wijst naar dat scherm ({geweigerd.Error})");
|
|
|
|
var nogSteedsLid = await w.Families.ListMembersAsync(familie.Id, jan.Id);
|
|
Assert.True(nogSteedsLid.Any(m => m.PersonId == eva.Id), "ze staat nog gewoon in de familie");
|
|
|
|
// Haal haar eerst weg als beheerder; dan kan het wél. ListManagersAsync vraagt om de
|
|
// PersonId van de eigenaar, niet zijn UserId - dat scheelt hier maar de moeite van het
|
|
// uit elkaar houden is precies waarom dit soort dingen een test verdient.
|
|
var beheerders = await w.Legacy.ListManagersAsync(jan.PersonId);
|
|
Assert.True((await w.Legacy.RemoveManagerAsync(jan.Id, beheerders.Single().PersonId)).Succeeded,
|
|
"beheerder weghalen lukt");
|
|
|
|
var nu = await w.Families.VerwijderLidAsync(familie.Id, eva.Id, jan.Id);
|
|
Assert.True(nu.Succeeded, $"en nu kan ze wel weg ({nu.Error})");
|
|
});
|
|
|
|
runner.TestAsync("Familielid verwijderen: klaarstaande momenten voor hem verdwijnen mee", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
var eva = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter)).Value!;
|
|
|
|
// Eva heeft een geclaimd account nodig: zonder ontvanger met een eigen account bezorgt
|
|
// DeliverAsync nooit iets (het bericht blijft klaarstaan tot iemand zijn profiel claimt), en
|
|
// dan bewijst "het bezorgde bericht blijft staan" niets. Dezelfde weg als bij "Ben jij dit?":
|
|
// uitnodigen en de uitnodiging aannemen.
|
|
await w.Families.InviteAsync(familie.Id, eva.Id, jan.Id);
|
|
var mail = w.Email.Sent.Last(m => m.To == "eva@example.nl");
|
|
var token = mail.Body.Split("/uitnodiging/")[1].Split('"')[0];
|
|
Assert.True((await w.Families.AcceptInvitationAsync(token, w.Accounts)).Succeeded, "Eva claimt haar profiel");
|
|
|
|
// Eén klaarstaand moment van Jan aan Eva, en - ter controle - eentje al bezorgd.
|
|
var klaar = (await w.Messages.ComposeAsync(jan.Id, eva.Id, "Voor later", "Tekst",
|
|
w.Clock.UtcNow.AddDays(5))).Value!;
|
|
var meteen = (await w.Messages.ComposeAsync(jan.Id, eva.Id, "Nu", "Tekst")).Value!;
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Store.Messages.Single(m => m.Id == meteen.Id).Status == MessageStatus.Bezorgd,
|
|
"het tweede bericht is alvast bezorgd");
|
|
|
|
var voorVerwijderen = await w.Families.ListMembersAsync(familie.Id, jan.Id);
|
|
Assert.True(voorVerwijderen.Single(m => m.PersonId == eva.Id).MomentenKlaar == 1,
|
|
"de lijst weet dat er één moment voor haar klaarstaat");
|
|
|
|
var weg = await w.Families.VerwijderLidAsync(familie.Id, eva.Id, jan.Id);
|
|
Assert.True(weg.Succeeded, $"verwijderen lukt ({weg.Error})");
|
|
|
|
Assert.True(w.Store.Messages.All(m => m.Id != klaar.Id), "het klaarstaande moment is weg");
|
|
Assert.True(w.Store.Messages.Any(m => m.Id == meteen.Id), "het bezorgde moment blijft gewoon staan");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- moment, kijklink zonder account
|
|
runner.TestAsync("Moment voor iemand zonder account: een kijklink per e-mail, geen bezorging", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
// Karin heeft (nog) geen account - alleen een naam en een e-mailadres, zoals bij iedereen
|
|
// die je toevoegt voordat je hem uitnodigt.
|
|
var karin = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Karin", "Ophuis",
|
|
"karin@example.nl", RelationType.Partner)).Value!;
|
|
|
|
var moment = (await w.Messages.ComposeAsync(jan.Id, karin.Id, "Voor jou",
|
|
"Lieve Karin, dit is voor je.")).Value!;
|
|
|
|
var bezorgd = await w.Messages.DeliverDueAsync();
|
|
Assert.True(bezorgd == 0, "er is niemand met een account om normaal aan te bezorgen");
|
|
Assert.True(w.Store.Messages.Single(m => m.Id == moment.Id).Status == MessageStatus.Gepland,
|
|
"het bericht blijft klaarstaan - de kijklink is geen bezorging");
|
|
|
|
var mail = w.Email.Sent.Last(m => m.To == "karin@example.nl");
|
|
Assert.True(mail.Subject.Contains("staat een bericht"), "de eerste mail is de aankondiging, geen herinnering");
|
|
var token = mail.Body.Split("/moment/")[1].Split('"')[0];
|
|
|
|
var weergave = await w.Messages.ViewByTokenAsync(token);
|
|
Assert.True(weergave.Succeeded, $"de link werkt ({weergave.Error})");
|
|
Assert.True(weergave.Value!.Text == "Lieve Karin, dit is voor je.", "de tekst komt uit de landingspagina goed tevoorschijn");
|
|
Assert.True(weergave.Value!.AfzenderNaam == "Jan Jansen", "de afzender staat erbij");
|
|
Assert.True(weergave.Value!.KanNogClaimen, "Karin kan haar profiel alsnog claimen");
|
|
|
|
// Nog een keer de ronde draaien: de link is nog geldig, dus geen tweede mail.
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Email.Sent.Count(m => m.To == "karin@example.nl") == 1, "geen tweede link zolang de eerste nog geldig is");
|
|
|
|
// Een verkeerd token laat niets los.
|
|
var fout = await w.Messages.ViewByTokenAsync(token + "verkeerd");
|
|
Assert.True(!fout.Succeeded, "een verkeerd token wordt geweigerd");
|
|
});
|
|
|
|
runner.TestAsync("Moment voor iemand zonder account: na 30 dagen niet geopend komt er een herinnering", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == jan.PersonId);
|
|
|
|
var karin = (await w.Families.AddMemberAsync(familie.Id, jan.Id, "Karin", "Ophuis",
|
|
"karin@example.nl", RelationType.Partner)).Value!;
|
|
await w.Messages.ComposeAsync(jan.Id, karin.Id, "Voor jou", "Lieve Karin, dit is voor je.");
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Email.Sent.Count(m => m.To == "karin@example.nl") == 1, "de eerste link is verstuurd");
|
|
|
|
// 30 dagen verder, zonder dat Karin ooit geklikt heeft.
|
|
w.Clock.Advance(TimeSpan.FromDays(31));
|
|
await w.Messages.DeliverDueAsync();
|
|
|
|
var mails = w.Email.Sent.Where(m => m.To == "karin@example.nl").ToList();
|
|
Assert.True(mails.Count == 2, "er komt precies één herinnering bij");
|
|
Assert.True(mails[1].Subject.Contains("nog een bericht openstaan"), "de tweede mail is herkenbaar als herinnering");
|
|
|
|
var nieuwToken = mails[1].Body.Split("/moment/")[1].Split('"')[0];
|
|
var weergave = await w.Messages.ViewByTokenAsync(nieuwToken);
|
|
Assert.True(weergave.Succeeded, "de nieuwe link uit de herinnering werkt gewoon");
|
|
|
|
// Nog een keer 31 dagen verder, zonder te openen: geen tweede herinnering.
|
|
w.Clock.Advance(TimeSpan.FromDays(31));
|
|
await w.Messages.DeliverDueAsync();
|
|
Assert.True(w.Email.Sent.Count(m => m.To == "karin@example.nl") == 2,
|
|
"er komt maar één herinnering, niet steeds opnieuw");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- familielid wijzigen
|
|
runner.TestAsync("Familielid wijzigen: mag tot het geclaimd is, ook na een uitnodiging", 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);
|
|
|
|
// Met een tikfout in het adres, zoals dat in het echt gaat.
|
|
var zoon = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Jarron", "Flietstra",
|
|
"jarron@gmial.com", RelationType.Kind)).Value!;
|
|
|
|
var herstel = await w.Families.WijzigLidAsync(familie.Id, zoon.Id, vader.Id,
|
|
"Jarron", "Flietstra", "jarron@gmail.com", RelationType.Zoon);
|
|
Assert.True(herstel.Succeeded, $"het adres is te herstellen ({herstel.Error})");
|
|
|
|
var na = await w.Families.ListMembersAsync(familie.Id, vader.Id);
|
|
var regel = na.Single(m => m.PersonId == zoon.Id);
|
|
Assert.True(regel.Email == "jarron@gmail.com", "het nieuwe adres staat erin");
|
|
Assert.True(regel.Relation == RelationType.Zoon, "en de relatie is bijgewerkt");
|
|
|
|
// Na de uitnodiging moet wijzigen nog steeds kunnen: juist dan merk je dat het adres niet klopt.
|
|
var oudToken = (await w.Families.InviteAsync(familie.Id, zoon.Id, vader.Id)).Value!;
|
|
var naUitnodiging = await w.Families.WijzigLidAsync(familie.Id, zoon.Id, vader.Id,
|
|
"Jarron", "Flietstra", "jarron2@gmail.com", RelationType.Zoon);
|
|
Assert.True(naUitnodiging.Succeeded, "wijzigen kan ook nog nadat de uitnodiging verstuurd is");
|
|
|
|
// De oude uitnodiging is daarmee waardeloos geworden. Dat hoort ook: die lag in het postvak
|
|
// van het oude adres, en dat is precies waar je vanaf wilde.
|
|
var metOudToken = await w.Families.AcceptInvitationAsync(oudToken, w.Accounts);
|
|
Assert.True(!metOudToken.Succeeded, "de uitnodiging naar het oude adres werkt niet meer");
|
|
|
|
// Met een nieuwe uitnodiging naar het juiste adres gaat het wel, en dan is het profiel van hem.
|
|
var token = (await w.Families.InviteAsync(familie.Id, zoon.Id, vader.Id)).Value!;
|
|
var jarron = (await w.Families.AcceptInvitationAsync(token, w.Accounts)).Value!;
|
|
|
|
Assert.True(w.Store.People.Count(p => p.Id == zoon.Id) == 1, "er is geen tweede profiel ontstaan");
|
|
Assert.True(w.Store.People.Single(p => p.Id == zoon.Id).ClaimedByUserId == jarron.Id,
|
|
"het oorspronkelijke profiel is geclaimd, niet een nieuw aangemaakt profiel");
|
|
|
|
var naClaim = await w.Families.WijzigLidAsync(familie.Id, zoon.Id, vader.Id,
|
|
"Iets", "Anders", null, RelationType.Zoon);
|
|
Assert.True(!naClaim.Succeeded, "na het claimen mag de vader niets meer wijzigen");
|
|
Assert.True(naClaim.Error!.Contains("zelf"), $"met uitleg waarom ({naClaim.Error})");
|
|
});
|
|
|
|
runner.TestAsync("Familielid wijzigen: onmogelijke en bezette adressen worden geweigerd", 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 eva = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter)).Value!;
|
|
var tim = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Tim", "Jansen",
|
|
"tim@example.nl", RelationType.Zoon)).Value!;
|
|
|
|
var onzin = await w.Families.WijzigLidAsync(familie.Id, eva.Id, vader.Id, "Eva", "Jansen",
|
|
"eva apenstaartje example", RelationType.Dochter);
|
|
Assert.True(!onzin.Succeeded, "een adres zonder @ wordt geweigerd");
|
|
|
|
// Het adres van iemand die JIJ al hebt staan: dat is een botsing, want dan weet niemand
|
|
// meer wie er wordt uitgenodigd. Het adres van andermans aantekening is dat juist niet;
|
|
// zie de controle hieronder.
|
|
var bezet = await w.Families.WijzigLidAsync(familie.Id, eva.Id, vader.Id, "Eva", "Jansen",
|
|
"tim@example.nl", RelationType.Dochter);
|
|
Assert.True(!bezet.Succeeded, "het adres van je eigen andere profiel wordt geweigerd");
|
|
Assert.True(bezet.Error!.Contains("al staan bij"), $"met uitleg ({bezet.Error})");
|
|
|
|
// En de oorspronkelijke gegevens staan er nog gewoon.
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, vader.Id);
|
|
Assert.True(leden.Single(m => m.PersonId == eva.Id).Email == "eva@example.nl", "er is niets half opgeslagen");
|
|
Assert.True(leden.Single(m => m.PersonId == tim.Id).Relation == RelationType.Zoon, "Tim is nog steeds zoon");
|
|
|
|
var zonderAdres = await w.Families.WijzigLidAsync(familie.Id, eva.Id, vader.Id, "Eva", "Jansen",
|
|
null, RelationType.Dochter);
|
|
Assert.True(zonderAdres.Succeeded, "het adres mag ook leeggemaakt worden");
|
|
});
|
|
|
|
runner.TestAsync("Twee mensen mogen dezelfde persoon kennen", async () =>
|
|
{
|
|
// Dit ging fout, en niet zo'n beetje ook: op persons.email stond een UNIQUE-index, dus wie
|
|
// een e-mailadres het eerst invulde bepaalde dat niemand anders die persoon nog kon
|
|
// toevoegen. Bij het opslaan liep de database stuk en zag de gebruiker een knop die niets
|
|
// deed. In een applicatie waar twee ouders allebei hun dochter toevoegen is dat precies de
|
|
// verkeerde kant om. Zie docs/Identiteit.md en db/15_adres_niet_uniek.sql.
|
|
var w = new World();
|
|
|
|
var vader = await w.MaakGebruikerAsync("vader@example.nl", "Jan", "Jansen");
|
|
var moeder = await w.MaakGebruikerAsync("moeder@example.nl", "Ans", "Jansen");
|
|
|
|
var vadersFamilie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId);
|
|
var moedersFamilie = w.Store.Families.Single(f => f.OwnerPersonId == moeder.PersonId);
|
|
|
|
// Moeder eerst. Dat is met opzet: haar aantekening staat daarna vooraan bij het zoeken op
|
|
// adres, en dan moet vader nog steeds bij de zijne uitkomen en niet bij die van haar.
|
|
var bijMoeder = await w.Families.AddMemberAsync(moedersFamilie.Id, moeder.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter);
|
|
Assert.True(bijMoeder.Succeeded, $"moeder voegt haar dochter toe ({bijMoeder.Error})");
|
|
|
|
var bijVader = await w.Families.AddMemberAsync(vadersFamilie.Id, vader.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter);
|
|
Assert.True(bijVader.Succeeded, $"en vader ook, met hetzelfde adres ({bijVader.Error})");
|
|
|
|
// Twee aantekeningen, geen gedeelde. Andermans profiel adopteer je niet: dan zou je meteen
|
|
// zien wat een ander over iemand heeft ingevuld.
|
|
Assert.True(bijVader.Value!.Id != bijMoeder.Value!.Id, "allebei hun eigen aantekening");
|
|
Assert.True(bijMoeder.Value.ManagedByUserId == moeder.Id, "en die van moeder is van moeder");
|
|
|
|
// Voegt vader haar toe aan een tweede familie van zichzelf, dan hoort dat wél hetzelfde
|
|
// profiel te zijn - anders ziet hij zijn eigen dochter twee keer.
|
|
var tweede = new MijnBesluit.Domain.Entities.Family
|
|
{
|
|
Name = "Familie Jansen (tweede)",
|
|
OwnerPersonId = vader.PersonId,
|
|
CreatedAt = w.Clock.UtcNow,
|
|
UpdatedAt = w.Clock.UtcNow
|
|
};
|
|
w.Store.Families.Add(tweede);
|
|
tweede.Id = w.Store.Families.Max(f => f.Id) + 1;
|
|
|
|
var nogmaals = await w.Families.AddMemberAsync(tweede.Id, vader.Id, "Eva", "Jansen",
|
|
"eva@example.nl", RelationType.Dochter);
|
|
Assert.True(nogmaals.Succeeded, $"vader voegt haar toe aan zijn tweede familie ({nogmaals.Error})");
|
|
Assert.True(nogmaals.Value!.Id == bijVader.Value.Id, "en dat is zijn eigen aantekening, geen derde");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- account opheffen
|
|
runner.TestAsync("Account opheffen: weg is wat van jou is, blijven doet wat van een ander is", async () =>
|
|
{
|
|
// Google Play eist dat je je account kunt laten verwijderen. Maar de vraag die ertoe doet is
|
|
// niet óf het kan, het is wat er precies weggaat - want in deze applicatie liggen "van jou"
|
|
// en "van een ander" dicht tegen elkaar aan. Zie AccountOpheffenService.
|
|
var w = new World();
|
|
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var eva = await w.MaakGebruikerAsync("eva@example.nl", "Eva", "Jansen");
|
|
|
|
// Jan zet iets in zijn kluis.
|
|
var kluisitem = (await w.Vault.CreateAsync(jan.Id, VaultCategory.Wachtwoord, "De meterkast",
|
|
new VaultPayload { Secret = "hunter2" })).Value!;
|
|
|
|
// Eén bericht dat al bezorgd is, en één dat nog klaarstaat.
|
|
var bezorgd = (await w.Messages.ComposeAsync(jan.Id, eva.PersonId, "Voor jou", "Dag lieve Eva")).Value!;
|
|
await w.Messages.DeliverDueAsync();
|
|
|
|
var klaarstaand = (await w.Messages.ComposeAsync(jan.Id, eva.PersonId, "Later", "Nog niet lezen",
|
|
null, afterDeath: true)).Value!;
|
|
|
|
// En Eva heeft Jan in haar eigen lijst staan - haar aantekening over hem.
|
|
var evasFamilie = w.Store.Families.Single(f => f.OwnerPersonId == eva.PersonId);
|
|
w.Store.Members.Add(new MijnBesluit.Domain.Entities.FamilyMember
|
|
{
|
|
Id = w.Store.Members.Count + 1000,
|
|
FamilyId = evasFamilie.Id,
|
|
PersonId = jan.PersonId,
|
|
Relation = RelationType.Ouder,
|
|
AddedByUserId = eva.Id,
|
|
CreatedAt = w.Clock.UtcNow
|
|
});
|
|
|
|
// Eerst tellen wat er weggaat; dat staat in het scherm voordat iemand ja zegt.
|
|
var telling = await w.Opheffen.TelOpAsync(jan.Id);
|
|
Assert.True(telling.Succeeded, $"de telling lukt ({telling.Error})");
|
|
Assert.True(telling.Value!.Kluisitems == 1, "één kluisitem");
|
|
Assert.True(telling.Value.BerichtenKlaar == 1, "één bericht dat nog klaarstond");
|
|
Assert.True(telling.Value.BerichtenBezorgd == 1, "en één dat al bezorgd was");
|
|
|
|
var weg = await w.Opheffen.VerwijderAsync(jan.Id);
|
|
Assert.True(weg.Succeeded, $"het opheffen lukt ({weg.Error})");
|
|
|
|
// --- wat weg hoort te zijn
|
|
Assert.True(!w.Store.Users.Any(u => u.Id == jan.Id), "het account is weg");
|
|
Assert.True(!w.Store.VaultItems.Any(v => v.Id == kluisitem.Id), "de kluis is leeg");
|
|
Assert.True(!w.Store.Messages.Any(m => m.Id == klaarstaand.Id), "het bericht dat klaarstond is weg");
|
|
Assert.True(!w.Store.Codes.Any(c => c.Email == "jan@example.nl"), "zijn inlogcodes zijn weg");
|
|
|
|
// --- wat blijven hoort
|
|
var gebleven = w.Store.Messages.SingleOrDefault(m => m.Id == bezorgd.Id);
|
|
Assert.True(gebleven is not null, "het bezorgde bericht blijft bij Eva staan");
|
|
|
|
// En het blijft leesbaar: bij het bezorgen kreeg Eva haar eigen kopie van de sleutel, dus
|
|
// Jans sleutel weggooien maakt het niet stuk. Dat dit kan is precies waarom "bezorgd blijft"
|
|
// een houdbare afspraak is en geen belofte die we niet waarmaken.
|
|
var gelezen = await w.Messages.ReadAsync(bezorgd.Id, eva.Id);
|
|
Assert.True(gelezen.Succeeded && gelezen.Value == "Dag lieve Eva",
|
|
$"Eva kan het nog lezen ({gelezen.Error})");
|
|
|
|
// Jans naam staat nog in Eva's lijst - dat is háár aantekening - maar alles wat aan hem
|
|
// vastzat is eraf.
|
|
var naam = w.Store.People.SingleOrDefault(p => p.Id == jan.PersonId);
|
|
Assert.True(naam is not null, "zijn naam blijft in de lijst van Eva staan");
|
|
Assert.True(naam!.Email is null, "maar zonder e-mailadres");
|
|
Assert.True(naam.ClaimedByUserId is null && naam.ManagedByUserId is null,
|
|
"en het profiel is van niemand meer");
|
|
|
|
// Opnieuw beginnen met hetzelfde adres kan gewoon. Er staat niets meer van vroeger klaar.
|
|
var opnieuw = await w.Accounts.VraagRegistratiecodeAanAsync("jan@example.nl", "Jan", "Jansen");
|
|
Assert.True(opnieuw.Succeeded, $"met hetzelfde adres kan hij opnieuw beginnen ({opnieuw.Error})");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- uitnodigen en claimen
|
|
runner.TestAsync("Uitnodigen: profiel blijft van de uitnodiger tot het geclaimd wordt", 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!;
|
|
|
|
Assert.True(!dochter.IsClaimed, "het nieuwe profiel is nog niet geclaimd");
|
|
Assert.True(ProfileOwnership.CanEdit(dochter, vader.Id), "de vader mag het profiel wijzigen");
|
|
|
|
var wijziging = await w.Families.UpdatePersonAsync(dochter.Id, vader.Id, p => p.MobileNumber = "0612345678");
|
|
Assert.True(wijziging.Succeeded, "wijzigen lukt zolang het profiel niet geclaimd is");
|
|
|
|
var token = (await w.Families.InviteAsync(familie.Id, dochter.Id, vader.Id)).Value!;
|
|
Assert.True(w.Email.Sent.Any(m => m.To == "eva@example.nl"), "er is een uitnodiging verstuurd");
|
|
Assert.True(!w.Email.Sent.Any(m => m.Body.Contains(token) == false), "de mail bevat de link met het token");
|
|
Assert.True(w.Store.Invitations.Single().TokenHash.Length == 32, "alleen de hash van het token staat opgeslagen");
|
|
|
|
var leden = await w.Families.ListMembersAsync(familie.Id, vader.Id);
|
|
Assert.True(leden.Single(m => m.PersonId == dochter.Id).State == ProfileState.Uitgenodigd,
|
|
"het profiel staat nu op 'uitgenodigd'");
|
|
|
|
// Eva neemt de uitnodiging aan.
|
|
var eva = (await w.Families.AcceptInvitationAsync(token, w.Accounts)).Value!;
|
|
|
|
var dochterNa = w.Store.People.Single(p => p.Id == dochter.Id);
|
|
Assert.True(dochterNa.ClaimedByUserId == eva.Id, "het profiel is nu van Eva");
|
|
Assert.True(!ProfileOwnership.CanEdit(dochterNa, vader.Id), "de vader mag het profiel niet meer wijzigen");
|
|
Assert.True(ProfileOwnership.CanEdit(dochterNa, eva.Id), "Eva mag haar eigen profiel wel wijzigen");
|
|
|
|
var geweigerd = await w.Families.UpdatePersonAsync(dochter.Id, vader.Id, p => p.MobileNumber = "0600000000");
|
|
Assert.True(!geweigerd.Succeeded, "een wijziging door de vader wordt geweigerd");
|
|
Assert.True(geweigerd.Error!.Contains("beheert dit profiel inmiddels zelf"), "met een begrijpelijke uitleg");
|
|
|
|
var ledenNa = await w.Families.ListMembersAsync(familie.Id, vader.Id);
|
|
var evaInFamilie = ledenNa.Single(m => m.PersonId == dochter.Id);
|
|
Assert.True(evaInFamilie.State == ProfileState.Geclaimd, "de vader ziet dat het profiel geclaimd is");
|
|
Assert.True(!evaInFamilie.CanEdit, "en dat hij het niet meer kan wijzigen");
|
|
|
|
Assert.True(w.Store.Families.Any(f => f.OwnerPersonId == dochter.Id), "Eva heeft nu haar eigen familieproject");
|
|
|
|
// Een tweede keer hetzelfde token gebruiken mag niet.
|
|
var nogmaals = await w.Families.AcceptInvitationAsync(token, w.Accounts);
|
|
Assert.True(!nogmaals.Succeeded, "een uitnodiging is eenmalig");
|
|
});
|
|
|
|
runner.TestAsync("Verlopen uitnodiging wordt geweigerd", async () =>
|
|
{
|
|
var w = new World();
|
|
var vader = await w.MaakGebruikerAsync("v@example.nl", "Jan", "Jansen");
|
|
var familie = w.Store.Families.Single(f => f.OwnerPersonId == vader.PersonId);
|
|
var kind = (await w.Families.AddMemberAsync(familie.Id, vader.Id, "Tim", "Jansen", "tim@example.nl", RelationType.Kind)).Value!;
|
|
var token = (await w.Families.InviteAsync(familie.Id, kind.Id, vader.Id)).Value!;
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(31));
|
|
var r = await w.Families.AcceptInvitationAsync(token, w.Accounts);
|
|
Assert.True(!r.Succeeded && r.Error!.Contains("verlopen"), "na 30 dagen is de uitnodiging verlopen");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- kluis
|
|
runner.TestAsync("Kluisitem is alleen leesbaar voor de eigenaar", async () =>
|
|
{
|
|
var w = new World();
|
|
var jan = await w.MaakGebruikerAsync("jan@example.nl", "Jan", "Jansen");
|
|
var piet = await w.MaakGebruikerAsync("piet@example.nl", "Piet", "Pieters");
|
|
|
|
var item = (await w.Vault.CreateAsync(jan.Id, VaultCategory.Wachtwoord, "Bank",
|
|
new VaultPayload { Website = "bank.nl", Username = "jan", Secret = "zeergeheim" })).Value!;
|
|
|
|
Assert.True(!w.Store.VaultItems.Single().Ciphertext.SequenceEqual(System.Text.Encoding.UTF8.GetBytes("zeergeheim")),
|
|
"de inhoud staat versleuteld in de opslag");
|
|
|
|
var vanJan = await w.Vault.OpenAsync(item.Id, jan.Id);
|
|
Assert.True(vanJan.Succeeded && vanJan.Value!.Secret == "zeergeheim", "Jan kan zijn eigen item openen");
|
|
|
|
var vanPiet = await w.Vault.OpenAsync(item.Id, piet.Id);
|
|
Assert.True(!vanPiet.Succeeded, "Piet kan er niet bij");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- overlijden en nalatenschap
|
|
runner.TestAsync("Overlijden: drie meldingen, bezwaartermijn, dan toegang voor de beheerder", async () =>
|
|
{
|
|
var w = new World();
|
|
var opa = await w.MaakGebruikerAsync("opa@example.nl", "Klaas", "de Vries");
|
|
// Ze staan bij opa in de familie én hebben hun uitnodiging aangenomen, en hij heeft ze alle
|
|
// drie aangewezen. Dat laatste is nodig: melden kan alleen als je bent aangewezen.
|
|
var zoon = await w.MaakFamilielidAsync(opa, "Peter", "de Vries", "zoon@example.nl");
|
|
var dochter = await w.MaakFamilielidAsync(opa, "Anja", "de Vries", "dochter@example.nl");
|
|
var buurman = await w.MaakFamilielidAsync(opa, "Henk", "Bakker", "buur@example.nl",
|
|
RelationType.Vriend);
|
|
await w.BeheerderAsync(opa, zoon, dochter, buurman);
|
|
|
|
var wens = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Uitvaartwens, "Mijn uitvaart",
|
|
new VaultPayload { BurialOrCremation = "Begrafenis", Music = "Adagio", Wishes = "Geen toespraken." },
|
|
ReleasePolicy.NaOverlijden)).Value!;
|
|
|
|
var prive = (await w.Vault.CreateAsync(opa.Id, VaultCategory.Notitie, "Persoonlijk",
|
|
new VaultPayload { Notes = "Blijft van mij." })).Value!;
|
|
|
|
var na1 = (await w.Legacy.DeclareDeathAsync(opa.PersonId, zoon.Id)).Value;
|
|
Assert.True(na1.State == DeathProcedureState.Lopend && na1.Missing == 2, "één melding: nog twee nodig");
|
|
|
|
var nogmaals = await w.Legacy.DeclareDeathAsync(opa.PersonId, zoon.Id);
|
|
Assert.True(!nogmaals.Succeeded, "dezelfde persoon kan niet twee keer melden");
|
|
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, dochter.Id);
|
|
var na3 = (await w.Legacy.DeclareDeathAsync(opa.PersonId, buurman.Id)).Value;
|
|
Assert.True(na3.Missing == 0, "drie meldingen binnen");
|
|
Assert.True(na3.State == DeathProcedureState.Lopend, "nog niet bevestigd: de bezwaartermijn loopt");
|
|
Assert.True(w.Email.Sent.Any(m => m.To == "opa@example.nl" && m.Subject.Contains("melding")),
|
|
"de betrokkene krijgt bericht dat hij bezwaar kan maken");
|
|
|
|
var voorTermijn = await w.Vault.OpenAsync(wens.Id, zoon.Id);
|
|
Assert.True(!voorTermijn.Succeeded, "de beheerder kan er tijdens de bezwaartermijn nog niet bij");
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(15));
|
|
Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 1, "na de termijn wordt het overlijden bevestigd");
|
|
|
|
var naTermijn = await w.Vault.OpenAsync(wens.Id, zoon.Id);
|
|
Assert.True(naTermijn.Succeeded, "de beheerder kan de vrijgegeven uitvaartwens nu lezen");
|
|
Assert.True(naTermijn.Value!.Music == "Adagio", "en de inhoud klopt");
|
|
|
|
var priveNaTermijn = await w.Vault.OpenAsync(prive.Id, zoon.Id);
|
|
Assert.True(!priveNaTermijn.Succeeded, "wat niet was vrijgegeven blijft dicht");
|
|
|
|
Assert.True(w.Store.Users.Single(u => u.Id == opa.Id).Status == UserStatus.Overleden, "het account staat op overleden");
|
|
});
|
|
|
|
runner.TestAsync("Bezwaar van de betrokkene draait alles terug", async () =>
|
|
{
|
|
var w = new World();
|
|
var opa = await w.MaakGebruikerAsync("opa@example.nl", "Klaas", "de Vries");
|
|
var a = await w.MaakGebruikerAsync("a@example.nl", "A", "A");
|
|
var b = await w.MaakGebruikerAsync("b@example.nl", "B", "B");
|
|
var c = await w.MaakGebruikerAsync("c@example.nl", "C", "C");
|
|
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, a.Id);
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, b.Id);
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, c.Id);
|
|
|
|
Assert.True((await w.Legacy.ObjectAsync(opa.PersonId, opa.Id)).Succeeded, "de betrokkene maakt bezwaar");
|
|
|
|
var status = await w.Legacy.StatusAsync(opa.PersonId);
|
|
Assert.True(status.State == DeathProcedureState.Betwist, "de procedure staat op betwist");
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(60));
|
|
Assert.True(await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId }) == 0, "er wordt niets meer bevestigd");
|
|
Assert.True(w.Store.People.Single(p => p.Id == opa.PersonId).DeathConfirmedAt is null, "geen bevestiging");
|
|
});
|
|
|
|
runner.TestAsync("Instelbaar aantal meldingen per profiel", async () =>
|
|
{
|
|
var w = new World();
|
|
var oma = await w.MaakGebruikerAsync("oma@example.nl", "Truus", "de Wit");
|
|
w.Store.People.Single(p => p.Id == oma.PersonId).RequiredDeathDeclarations = 1;
|
|
w.Store.People.Single(p => p.Id == oma.PersonId).DeathObjectionDays = 0;
|
|
|
|
var melder = await w.MaakFamilielidAsync(oma, "M", "M", "m@example.nl");
|
|
await w.BeheerderAsync(oma, melder);
|
|
|
|
var r = (await w.Legacy.DeclareDeathAsync(oma.PersonId, melder.Id)).Value;
|
|
Assert.True(r.State == DeathProcedureState.Bevestigd, "met één vereiste melding en geen bezwaartermijn is het meteen rond");
|
|
});
|
|
|
|
// ---------------------------------------------------------------- berichten
|
|
runner.TestAsync("Bericht op datum wordt pas daarna bezorgd", async () =>
|
|
{
|
|
var w = new World();
|
|
var moeder = await w.MaakGebruikerAsync("moeder@example.nl", "Ria", "Smit");
|
|
var kind = await w.MaakGebruikerAsync("kind@example.nl", "Sam", "Smit");
|
|
|
|
var over1jaar = w.Clock.UtcNow.AddDays(365);
|
|
var bericht = (await w.Messages.ComposeAsync(moeder.Id, kind.PersonId, "Voor je verjaardag",
|
|
"Gefeliciteerd, lieverd.", over1jaar)).Value!;
|
|
|
|
Assert.True(await w.Messages.DeliverDueAsync() == 0, "vandaag wordt er nog niets bezorgd");
|
|
Assert.True(!(await w.Messages.ReadAsync(bericht.Id, kind.Id)).Succeeded, "en het kind kan het niet lezen");
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(366));
|
|
Assert.True(await w.Messages.DeliverDueAsync() == 1, "na de datum wordt het bezorgd");
|
|
|
|
var gelezen = await w.Messages.ReadAsync(bericht.Id, kind.Id);
|
|
Assert.True(gelezen.Succeeded && gelezen.Value == "Gefeliciteerd, lieverd.", "en is het leesbaar voor het kind");
|
|
|
|
var doorEenAnder = await w.Messages.ReadAsync(bericht.Id, moeder.Id);
|
|
Assert.True(!doorEenAnder.Succeeded, "iemand anders kan het niet openen");
|
|
});
|
|
|
|
runner.TestAsync("Bericht na overlijden wacht op de bevestiging", async () =>
|
|
{
|
|
var w = new World();
|
|
var opa = await w.MaakGebruikerAsync("opa@example.nl", "Klaas", "de Vries");
|
|
var klein = await w.MaakFamilielidAsync(opa, "Noor", "de Vries", "klein@example.nl",
|
|
RelationType.Kleinkind);
|
|
var a = await w.MaakFamilielidAsync(opa, "A", "A", "a@example.nl");
|
|
await w.BeheerderAsync(opa, klein, a);
|
|
|
|
var bericht = (await w.Messages.ComposeAsync(opa.Id, klein.PersonId, "Als ik er niet meer ben",
|
|
"Wees lief voor je moeder.", null, afterDeath: true)).Value!;
|
|
|
|
w.Clock.Advance(TimeSpan.FromDays(400));
|
|
Assert.True(await w.Messages.DeliverDueAsync() == 0, "zolang opa leeft blijft het bericht staan");
|
|
|
|
w.Store.People.Single(p => p.Id == opa.PersonId).RequiredDeathDeclarations = 2;
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, a.Id);
|
|
await w.Legacy.DeclareDeathAsync(opa.PersonId, klein.Id);
|
|
w.Clock.Advance(TimeSpan.FromDays(15));
|
|
await w.Legacy.ProcessPendingAsync(new[] { opa.PersonId });
|
|
|
|
Assert.True(await w.Messages.DeliverDueAsync() == 1, "na de bevestiging wordt het bezorgd");
|
|
var gelezen = await w.Messages.ReadAsync(bericht.Id, klein.Id);
|
|
Assert.True(gelezen.Succeeded && gelezen.Value!.Contains("Wees lief"), "het kleinkind kan het lezen");
|
|
});
|
|
|
|
InlogChecks.Register(runner);
|
|
SocketChecks.Register(runner);
|
|
EndToEndChecks.Register(runner);
|
|
SessieChecks.Register(runner);
|
|
ClientChecks.Register(runner);
|
|
ServiceChecks.Register(runner);
|
|
AdvertentieChecks.Register(runner);
|
|
UitrolChecks.Register(runner);
|
|
|
|
return runner.Summary();
|