Files
MijnBesluit/tests/MijnBesluit.SelfCheck/InMemory.cs
T
jfandClaude Sonnet 5.5 5446dd4170 Versiehistorie in een tabel, voor site en app
De versiehistorie stond als tekst in zowel de app als de website. Nu staat hij in
tabel version_history, met een beheerpagina (Beheer > Versiehistorie) op de site.
De site leest de tabel rechtstreeks, de app via de nieuwe actie app.versiehistorie.
Startrijen: db/handmatig/22b_versiehistorie_startrijen.sql (als root).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014MGLqt1uKfNARQHut9ifcx
2026-10-20 11:47:06 +00:00

618 lines
34 KiB
C#

using MijnBesluit.Application.Abstractions;
using MijnBesluit.Domain.Entities;
using MijnBesluit.Domain.Enums;
namespace MijnBesluit.SelfCheck;
/// <summary>Eenvoudige opslag in het geheugen, zodat de regels zonder database te controleren zijn.</summary>
public class Store
{
public List<User> Users = new();
public List<Person> People = new();
public List<Family> Families = new();
public List<FamilyMember> Members = new();
public List<Invitation> Invitations = new();
public List<VaultItem> VaultItems = new();
public List<VaultFile> VaultFiles = new();
public List<ContentGrant> Grants = new();
public List<Message> Messages = new();
public List<DeathDeclaration> Declarations = new();
public List<LegacyManager> Managers = new();
public List<AuditLogEntry> Audit = new();
public List<LoginCode> Codes = new();
public List<Toestel> Toestellen = new();
public List<Samenvoeging> Samenvoegingen = new();
public List<Adverteerder> Adverteerders = new();
public List<Campagne> Campagnes = new();
public List<CampagneUur> CampagneUren = new();
public List<VaultCategoryDefinition> Soorten = VaultCategorySaatgoed.Maak();
public List<VaultCategoryField> SoortVelden = new();
public List<VersieRegel> VersieRegels = new();
private long _seq;
public long NextId() => ++_seq;
}
public class TestClock : IClock
{
public DateTime UtcNow { get; set; } = new(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
public void Advance(TimeSpan by) => UtcNow = UtcNow.Add(by);
}
/// <summary>Mastersleutel in het geheugen. In productie komt deze uit een sleutelbestand of sleutelkluis.</summary>
/// <summary>
/// Een verkleiner die je per test aanzet. Standaard uit, zodat de andere controles blijven
/// werken zoals ze waren: zonder verkleiner is er geen kleine versie en wordt er niets omgezet,
/// precies zoals op een server waar het verkleinen niet beschikbaar is.
/// </summary>
public class TestVerkleiner : IAfbeeldingVerkleiner
{
public bool Aan { get; set; }
/// <summary>Wat er teruggegeven wordt als hij aanstaat. Klein, zodat het verschil duidelijk is.</summary>
public byte[] Uitkomst { get; set; } = { 1, 2, 3, 4, 5, 6, 7, 8 };
/// <summary>Waarmee hij is aangeroepen; zo kan een test nakijken of de maat klopt.</summary>
public List<(string Soort, int MaxZijde)> Aanroepen { get; } = new();
public byte[]? Verklein(byte[] inhoud, string soort, int maxZijde)
{
if (!Aan) return null;
if (!soort.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) return null;
Aanroepen.Add((soort, maxZijde));
return Uitkomst;
}
}
public class TestKeyProtector : IMasterKeyProtector
{
private readonly byte[] _master = MijnBesluit.Application.Security.EnvelopeCrypto.NewKey();
public int CurrentKeyVersion => 1;
public byte[] Protect(byte[] dataKey) => MijnBesluit.Application.Security.EnvelopeCrypto.WrapKey(dataKey, _master);
public byte[] Unprotect(byte[] p) => MijnBesluit.Application.Security.EnvelopeCrypto.UnwrapKey(p, _master);
}
public class TestEmail : IEmailSender
{
public List<(string To, string Subject, string Body)> Sent = new();
/// <summary>Zet dit aan om te doen alsof de mailserver onbereikbaar is.</summary>
public bool Mislukt { get; set; }
public Task SendAsync(string to, string subject, string body, CancellationToken ct = default)
{
if (Mislukt) throw new InvalidOperationException("Failure sending mail.");
Sent.Add((to, subject, body));
return Task.CompletedTask;
}
}
public class TestFiles : IFileStore
{
private readonly Dictionary<string, byte[]> _files = new();
public Task<string> WriteAsync(byte[] bytes, string name, CancellationToken ct = default)
{ var key = Guid.NewGuid().ToString("N"); _files[key] = bytes; return Task.FromResult(key); }
public Task<byte[]> ReadAsync(string key, CancellationToken ct = default) => Task.FromResult(_files[key]);
public Task DeleteAsync(string key, CancellationToken ct = default) { _files.Remove(key); return Task.CompletedTask; }
}
public class Uow : IUnitOfWork
{
private readonly Store _s;
public Uow(Store s) => _s = s;
/// <summary>Deelt id's uit aan nieuwe records, net zoals de database dat bij opslaan doet.</summary>
public Task<int> SaveChangesAsync(CancellationToken ct = default)
{
foreach (var p in _s.People.Where(x => x.Id == 0)) p.Id = _s.NextId();
foreach (var u in _s.Users.Where(x => x.Id == 0))
{
u.Id = _s.NextId();
if (u.PersonId == 0 && u.Person is not null) u.PersonId = u.Person.Id;
}
foreach (var f in _s.Families.Where(x => x.Id == 0)) f.Id = _s.NextId();
foreach (var m in _s.Members.Where(x => x.Id == 0)) m.Id = _s.NextId();
foreach (var i in _s.Invitations.Where(x => x.Id == 0)) i.Id = _s.NextId();
foreach (var v in _s.VaultItems.Where(x => x.Id == 0)) v.Id = _s.NextId();
foreach (var v in _s.VaultFiles.Where(x => x.Id == 0)) v.Id = _s.NextId();
foreach (var g in _s.Grants.Where(x => x.Id == 0)) g.Id = _s.NextId();
foreach (var m in _s.Messages.Where(x => x.Id == 0)) m.Id = _s.NextId();
foreach (var d in _s.Declarations.Where(x => x.Id == 0)) d.Id = _s.NextId();
foreach (var m in _s.Managers.Where(x => x.Id == 0)) m.Id = _s.NextId();
foreach (var a in _s.Audit.Where(x => x.Id == 0)) a.Id = _s.NextId();
foreach (var c in _s.Codes.Where(x => x.Id == 0)) c.Id = _s.NextId();
foreach (var v in _s.Samenvoegingen.Where(x => x.Id == 0)) v.Id = _s.NextId();
foreach (var a in _s.Adverteerders.Where(x => x.Id == 0)) a.Id = _s.NextId();
foreach (var c in _s.Campagnes.Where(x => x.Id == 0)) c.Id = _s.NextId();
foreach (var u in _s.CampagneUren.Where(x => x.Id == 0)) u.Id = _s.NextId();
return Task.FromResult(0);
}
}
public class UserRepo : IUserRepository
{
private readonly Store _s; public UserRepo(Store s) => _s = s;
/// <summary>
/// Vult de persoon in, net als de echte opslag met Include doet. Zonder dit gedraagt de
/// zelftest zich anders dan de server en merk je een ontbrekende Include hier niet.
/// </summary>
private User? MetPersoon(User? u)
{
if (u is not null) u.Person ??= _s.People.FirstOrDefault(p => p.Id == u.PersonId);
return u;
}
public Task<User?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.Id == id)));
public Task<User?> FindByEmailAsync(string e, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.Email == e)));
public Task<User?> FindByPersonIdAsync(long pid, CancellationToken ct = default) => Task.FromResult(MetPersoon(_s.Users.FirstOrDefault(u => u.PersonId == pid)));
public Task<bool> EmailExistsAsync(string e, CancellationToken ct = default) => Task.FromResult(_s.Users.Any(u => u.Email == e));
public Task<IReadOnlyList<User>> ListBeheerdersAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<User>>(_s.Users.Where(u => u.IsBeheerder).OrderBy(u => u.Id).ToList());
public void Add(User u) => _s.Users.Add(u);
public void Remove(User u) => _s.Users.Remove(u);
}
public class LoginCodeRepo : ILoginCodeRepository
{
private readonly Store _s; public LoginCodeRepo(Store s) => _s = s;
public Task<LoginCode?> FindOpenAsync(string email, LoginCodePurpose doel, DateTime nu, CancellationToken ct = default)
=> Task.FromResult(_s.Codes.Where(c => c.Email == email && c.Purpose == doel && c.IsOpen(nu))
.OrderByDescending(c => c.CreatedAt).FirstOrDefault());
public Task<IReadOnlyList<LoginCode>> ListOpenAsync(string email, LoginCodePurpose doel, DateTime nu, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<LoginCode>>(
_s.Codes.Where(c => c.Email == email && c.Purpose == doel && c.IsOpen(nu)).ToList());
public Task<int> CountSinceAsync(string email, DateTime sinds, CancellationToken ct = default)
=> Task.FromResult(_s.Codes.Count(c => c.Email == email && c.CreatedAt >= sinds && c.SignedInAt is null));
public Task<int> RemoveExpiredAsync(DateTime ouderDan, CancellationToken ct = default)
{
var aantal = _s.Codes.RemoveAll(c => c.ExpiresAt < ouderDan);
return Task.FromResult(aantal);
}
public Task<int> RemoveForEmailAsync(string email, CancellationToken ct = default)
{
var aantal = _s.Codes.RemoveAll(c => c.Email == email);
return Task.FromResult(aantal);
}
public void Add(LoginCode code) => _s.Codes.Add(code);
}
public class PersonRepo : IPersonRepository
{
private readonly Store _s; public PersonRepo(Store s) => _s = s;
public Task<Person?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.People.FirstOrDefault(p => p.Id == id));
// Dezelfde volgorde als in de database: geclaimde profielen eerst. Er kunnen er meerdere
// zijn met hetzelfde adres; zie docs/Identiteit.md.
public Task<Person?> FindByEmailAsync(string e, CancellationToken ct = default) =>
Task.FromResult(_s.People.Where(p => p.Email == e)
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id)
.FirstOrDefault());
public Task<IReadOnlyList<Person>> ListByEmailAsync(string e, CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<Person>>(
_s.People.Where(p => p.Email == e)
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id).ToList());
public Task<IReadOnlyList<Person>> ListAantekeningenOverAdresAsync(string e, long gebruikerId,
CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<Person>>(
_s.People.Where(p => p.Email == e
&& p.ClaimedByUserId == null
&& p.MergedIntoPersonId == null
&& p.ManagedByUserId != null
&& p.ManagedByUserId != gebruikerId
&& p.LinkDeclinedByUserId != gebruikerId)
.OrderBy(p => p.CreatedAt).ToList());
public Task<IReadOnlyList<Person>> ListByIdsAsync(IEnumerable<long> ids, CancellationToken ct = default)
{ var set = ids.ToHashSet(); return Task.FromResult<IReadOnlyList<Person>>(_s.People.Where(p => set.Contains(p.Id)).ToList()); }
public Task<Person?> FindByObjectionTokenAsync(byte[] hash, CancellationToken ct = default)
=> Task.FromResult(_s.People.FirstOrDefault(
p => p.ObjectionTokenHash is not null && p.ObjectionTokenHash.SequenceEqual(hash)));
public void Add(Person p) => _s.People.Add(p);
public void Remove(Person p) => _s.People.Remove(p);
}
public class FamilyRepo : IFamilyRepository
{
private readonly Store _s; public FamilyRepo(Store s) => _s = s;
public Task<Family?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Families.FirstOrDefault(f => f.Id == id));
public Task<IReadOnlyList<Family>> ListOwnedByPersonAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => f.OwnerPersonId == pid).ToList());
public Task<IReadOnlyList<FamilyMember>> ListMembersAsync(long fid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<FamilyMember>>(_s.Members.Where(m => m.FamilyId == fid).ToList());
public Task<FamilyMember?> FindMemberAsync(long fid, long pid, CancellationToken ct = default)
=> Task.FromResult(_s.Members.FirstOrDefault(m => m.FamilyId == fid && m.PersonId == pid));
public Task<IReadOnlyList<Family>> ListFamiliesContainingAsync(long pid, CancellationToken ct = default)
{ var ids = _s.Members.Where(m => m.PersonId == pid).Select(m => m.FamilyId).ToHashSet();
return Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => ids.Contains(f.Id)).ToList()); }
public Task<IReadOnlyList<long>> ListCircleAsync(long pid, CancellationToken ct = default)
{
var families = _s.Members.Where(m => m.PersonId == pid).Select(m => m.FamilyId)
.Concat(_s.Families.Where(f => f.OwnerPersonId == pid).Select(f => f.Id))
.ToHashSet();
var kring = _s.Members.Where(m => families.Contains(m.FamilyId)).Select(m => m.PersonId).ToHashSet();
kring.Add(pid);
return Task.FromResult<IReadOnlyList<long>>(kring.ToList());
}
public Task<IReadOnlyList<FamilyMember>> ListMembershipsOfPersonAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<FamilyMember>>(_s.Members.Where(m => m.PersonId == pid).ToList());
public Task<IReadOnlyList<Family>> ListOwnedAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Family>>(_s.Families.Where(f => f.OwnerPersonId == pid).ToList());
public void Add(Family f) => _s.Families.Add(f);
public void Add(FamilyMember m) => _s.Members.Add(m);
public void Remove(FamilyMember m) => _s.Members.Remove(m);
public void Remove(Family f) => _s.Families.Remove(f);
}
public class InvitationRepo : IInvitationRepository
{
private readonly Store _s; public InvitationRepo(Store s) => _s = s;
public Task<Invitation?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Invitations.FirstOrDefault(i => i.Id == id));
public Task<Invitation?> FindByTokenHashAsync(byte[] h, CancellationToken ct = default)
=> Task.FromResult(_s.Invitations.FirstOrDefault(i => i.TokenHash.SequenceEqual(h)));
public Task<IReadOnlyList<Invitation>> ListOpenForPersonAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.PersonId == pid).ToList());
public Task<bool> HasOpenInvitationAsync(long pid, DateTime now, CancellationToken ct = default)
=> Task.FromResult(_s.Invitations.Any(i => i.PersonId == pid && i.IsOpen(now)));
public Task<IReadOnlyList<Invitation>> ListLatestForPeopleAsync(IEnumerable<long> personIds, CancellationToken ct = default)
{
var ids = personIds.Distinct().ToHashSet();
return Task.FromResult<IReadOnlyList<Invitation>>(
_s.Invitations.Where(i => ids.Contains(i.PersonId))
.GroupBy(i => i.PersonId)
.Select(g => g.OrderByDescending(i => i.SentAt ?? i.CreatedAt).First())
.ToList());
}
public Task<IReadOnlyList<Invitation>> ListAllForPersonAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.PersonId == pid).ToList());
public Task<IReadOnlyList<Invitation>> ListSentByUserAsync(long userId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Invitation>>(_s.Invitations.Where(i => i.CreatedByUserId == userId).ToList());
public void Add(Invitation i) => _s.Invitations.Add(i);
public void Remove(Invitation i) => _s.Invitations.Remove(i);
}
public class VaultRepo : IVaultRepository
{
private readonly Store _s; public VaultRepo(Store s) => _s = s;
public Task<VaultItem?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.VaultItems.FirstOrDefault(v => v.Id == id));
public Task<IReadOnlyList<VaultItem>> ListForPersonAsync(long pid, VaultCategory? cat = null, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<VaultItem>>(_s.VaultItems.Where(v => v.OwnerPersonId == pid && v.DeletedAt == null
&& (cat == null || v.Category == cat)).ToList());
public Task<VaultFile?> FindFileAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.VaultFiles.FirstOrDefault(f => f.Id == id));
public Task<IReadOnlyList<VaultFile>> ListFilesForItemAsync(long itemId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
.Where(f => f.VaultItemId == itemId && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
public Task<IReadOnlyList<VaultFile>> ListFilesForPersonAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
.Where(f => f.SubjectPersonId == pid && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
public Task<IReadOnlyList<VaultFile>> ListFilesForMessageAsync(long mid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
.Where(f => f.MessageId == mid && f.DeletedAt == null).OrderBy(f => f.CreatedAt).ToList());
public Task<IReadOnlyList<VaultFile>> ListProfilePhotosAsync(IEnumerable<long> pids, CancellationToken ct = default)
{
var ids = pids.Distinct().ToHashSet();
return Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles
.Where(f => f.SubjectPersonId is { } p && ids.Contains(p) && f.IsProfilePhoto && f.DeletedAt == null)
.ToList());
}
public Task<ContentGrant?> FindGrantAsync(string type, long rid, long uid, CancellationToken ct = default)
=> Task.FromResult(_s.Grants.FirstOrDefault(g => g.ResourceType == type && g.ResourceId == rid && g.UserId == uid));
public Task<IReadOnlyList<VaultItem>> ListSharedWithAsync(long userId, CancellationToken ct = default)
{
var ids = _s.Grants.Where(g => g.ResourceType == nameof(VaultItem) && g.UserId == userId && g.RevokedAt == null)
.Select(g => g.ResourceId).ToHashSet();
return Task.FromResult<IReadOnlyList<VaultItem>>(
_s.VaultItems.Where(v => ids.Contains(v.Id) && v.DeletedAt == null).ToList());
}
public Task<IReadOnlyList<VaultFile>> ListFilesOwnedByAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<VaultFile>>(_s.VaultFiles.Where(f => f.OwnerPersonId == pid).ToList());
public void Add(VaultItem i) => _s.VaultItems.Add(i);
public void Add(VaultFile f) => _s.VaultFiles.Add(f);
public void Add(ContentGrant g) => _s.Grants.Add(g);
public void Remove(VaultItem i) => _s.VaultItems.Remove(i);
public void Remove(VaultFile f) => _s.VaultFiles.Remove(f);
public Task RemoveGrantsForUserAsync(long userId, CancellationToken ct = default)
{
_s.Grants.RemoveAll(g => g.UserId == userId);
return Task.CompletedTask;
}
public Task<int> RevokeGrantsForOwnerAsync(long ownerPersonId, string reason, DateTime nowUtc,
CancellationToken ct = default)
{
var items = _s.VaultItems.Where(i => i.OwnerPersonId == ownerPersonId).Select(i => i.Id).ToHashSet();
var verleend = _s.Grants.Where(g => g.ResourceType == nameof(VaultItem) && items.Contains(g.ResourceId)
&& g.Reason == reason && g.RevokedAt == null).ToList();
foreach (var t in verleend) t.RevokedAt = nowUtc;
return Task.FromResult(verleend.Count);
}
public Task RemoveGrantsAsync(long itemId, CancellationToken ct = default)
{
_s.Grants.RemoveAll(g => g.ResourceType == nameof(VaultItem) && g.ResourceId == itemId);
return Task.CompletedTask;
}
}
public class MessageRepo : IMessageRepository
{
private readonly Store _s; public MessageRepo(Store s) => _s = s;
public Task<Message?> FindByIdAsync(long id, CancellationToken ct = default) => Task.FromResult(_s.Messages.FirstOrDefault(m => m.Id == id));
public Task<IReadOnlyList<Message>> ListOutboxAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.FromPersonId == pid).ToList());
public Task<IReadOnlyList<Message>> ListInboxAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.ToPersonId == pid && m.Status >= MessageStatus.Bezorgd).ToList());
public Task<IReadOnlyList<Message>> ListDueAsync(DateTime now, int max, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.Status == MessageStatus.Gepland).Take(max).ToList());
public Task<IReadOnlyList<Message>> ListPlannedForAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages
.Where(m => m.ToPersonId == pid && m.Status == MessageStatus.Gepland).ToList());
public Task<IReadOnlyList<Message>> ListAwaitingDeathOfAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(_s.Messages.Where(m => m.FromPersonId == pid && m.DeliverAfterDeath).ToList());
public Task<IReadOnlyList<Message>> ListAllTouchingAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Message>>(
_s.Messages.Where(m => m.FromPersonId == pid || m.ToPersonId == pid).ToList());
public Task<Message?> FindByViewTokenHashAsync(byte[] tokenHash, CancellationToken ct = default)
=> Task.FromResult(_s.Messages.FirstOrDefault(m => m.ViewTokenHash is not null && m.ViewTokenHash.SequenceEqual(tokenHash)));
public void Add(Message m) => _s.Messages.Add(m);
public void Remove(Message m) => _s.Messages.Remove(m);
}
public class LegacyRepo : ILegacyRepository
{
private readonly Store _s; public LegacyRepo(Store s) => _s = s;
public Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(_s.Declarations.Where(d => d.PersonId == pid).ToList());
public Task<DeathDeclaration?> FindDeclarationByUserAsync(long pid, long uid, CancellationToken ct = default)
=> Task.FromResult(_s.Declarations.FirstOrDefault(d => d.PersonId == pid && d.DeclaredByUserId == uid));
public Task<IReadOnlyList<LegacyManager>> ListManagersForAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<LegacyManager>>(_s.Managers.Where(m => m.PersonId == pid).ToList());
public Task<IReadOnlyList<LegacyManager>> ListAssignmentsOfAsync(long mpid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<LegacyManager>>(_s.Managers.Where(m => m.ManagerPersonId == mpid).ToList());
public Task<IReadOnlyList<DeathDeclaration>> ListAllDeclarationsAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(_s.Declarations.Where(d => d.PersonId == pid).ToList());
public Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsByUserAsync(long userId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<DeathDeclaration>>(
_s.Declarations.Where(d => d.DeclaredByUserId == userId).ToList());
public void Remove(DeathDeclaration d) => _s.Declarations.Remove(d);
public void Add(DeathDeclaration d) => _s.Declarations.Add(d);
public void Add(LegacyManager m) => _s.Managers.Add(m);
public void Remove(LegacyManager m) => _s.Managers.Remove(m);
}
public class SamenvoegRepo : ISamenvoegRepository
{
private readonly Store _s; public SamenvoegRepo(Store s) => _s = s;
public Task<Samenvoeging?> FindByIdAsync(long id, CancellationToken ct = default)
=> Task.FromResult(_s.Samenvoegingen.FirstOrDefault(x => x.Id == id));
public Task<IReadOnlyList<Samenvoeging>> ListVoorDuplicaatAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Samenvoeging>>(
_s.Samenvoegingen.Where(x => x.DuplicaatPersonId == pid)
.OrderByDescending(x => x.SamengevoegdOp).ToList());
public Task<IReadOnlyList<Samenvoeging>> ListVoorDoelAsync(long pid, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Samenvoeging>>(
_s.Samenvoegingen.Where(x => x.DoelPersonId == pid)
.OrderByDescending(x => x.SamengevoegdOp).ToList());
public void Add(Samenvoeging s) => _s.Samenvoegingen.Add(s);
}
/// <summary>
/// Adverteerders, campagnes en tellers. Vult de adverteerder in bij een campagne, net als de
/// echte opslag met Include doet - zonder dat gedraagt de zelftest zich anders dan de server.
/// </summary>
public class AdvertentieRepo : IAdvertentieRepository
{
private readonly Store _s; public AdvertentieRepo(Store s) => _s = s;
private Campagne? Met(Campagne? c)
{
if (c is not null) c.Adverteerder ??= _s.Adverteerders.FirstOrDefault(a => a.Id == c.AdverteerderId);
return c;
}
public Task<Adverteerder?> FindAdverteerderAsync(long id, CancellationToken ct = default)
=> Task.FromResult(_s.Adverteerders.FirstOrDefault(a => a.Id == id));
public Task<Adverteerder?> FindAdverteerderByEmailAsync(string email, CancellationToken ct = default)
=> Task.FromResult(_s.Adverteerders.FirstOrDefault(a => a.Email == email));
public Task<IReadOnlyList<Adverteerder>> ListAdverteerdersAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Adverteerder>>(_s.Adverteerders.OrderBy(a => a.Bedrijfsnaam).ToList());
public void Add(Adverteerder a) => _s.Adverteerders.Add(a);
public Task<Campagne?> FindCampagneAsync(long id, CancellationToken ct = default)
=> Task.FromResult(Met(_s.Campagnes.FirstOrDefault(c => c.Id == id)));
public Task<IReadOnlyList<Campagne>> ListCampagnesAsync(long adverteerderId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Campagne>>(
_s.Campagnes.Where(c => c.AdverteerderId == adverteerderId)
.OrderByDescending(c => c.AangemaaktOp).Select(c => Met(c)!).ToList());
public Task<IReadOnlyList<Campagne>> ListLopendAsync(DateOnly dag, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Campagne>>(
_s.Campagnes.Where(c => c.DraaitOp(dag)).Select(c => Met(c)!).ToList());
public Task<IReadOnlyList<Campagne>> ListWachtendAsync(CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Campagne>>(
_s.Campagnes.Where(c => c.Stand == CampagneStand.WachtOpBetaling ||
c.Stand == CampagneStand.WachtOpGoedkeuring)
.OrderBy(c => c.AangemaaktOp).Select(c => Met(c)!).ToList());
public void Add(Campagne c) => _s.Campagnes.Add(c);
public Task<CampagneUur?> FindUurAsync(long campagneId, DateTime uurUtc, CancellationToken ct = default)
=> Task.FromResult(_s.CampagneUren.FirstOrDefault(u => u.CampagneId == campagneId && u.UurUtc == uurUtc));
public Task<IReadOnlyList<CampagneUur>> ListUrenAsync(long campagneId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<CampagneUur>>(
_s.CampagneUren.Where(u => u.CampagneId == campagneId).OrderBy(u => u.UurUtc).ToList());
public Task<IReadOnlyDictionary<long, int>> VertoningenInUurAsync(
DateTime uurUtc, IEnumerable<long> campagneIds, CancellationToken ct = default)
{
var lijst = campagneIds.ToHashSet();
return Task.FromResult<IReadOnlyDictionary<long, int>>(
_s.CampagneUren.Where(u => u.UurUtc == uurUtc && lijst.Contains(u.CampagneId))
.ToDictionary(u => u.CampagneId, u => u.Vertoningen));
}
/// <summary>Doet hetzelfde als de INSERT ... ON DUPLICATE KEY UPDATE van de echte opslag.</summary>
public Task TelAsync(long campagneId, DateTime uurUtc, int vertoningen, int kliks,
CancellationToken ct = default)
{
var regel = _s.CampagneUren.FirstOrDefault(u => u.CampagneId == campagneId && u.UurUtc == uurUtc);
if (regel is null)
{
regel = new CampagneUur { Id = _s.NextId(), CampagneId = campagneId, UurUtc = uurUtc };
_s.CampagneUren.Add(regel);
}
regel.Vertoningen += vertoningen;
regel.Kliks += kliks;
return Task.CompletedTask;
}
public Task<(int Vertoningen, int Kliks)> TotalenAsync(long campagneId, CancellationToken ct = default)
{
var uren = _s.CampagneUren.Where(u => u.CampagneId == campagneId).ToList();
return Task.FromResult((uren.Sum(u => u.Vertoningen), uren.Sum(u => u.Kliks)));
}
public void Add(CampagneUur u) => _s.CampagneUren.Add(u);
}
public class Audit : IAuditLog
{
private readonly Store _s; private readonly IClock _c;
public Audit(Store s, IClock c) { _s = s; _c = c; }
public Task WriteAsync(AuditAction a, long? uid, string? et = null, long? eid = null, string? d = null, CancellationToken ct = default)
{ _s.Audit.Add(new AuditLogEntry { Action = a, UserId = uid, EntityType = et, EntityId = eid, Details = d, CreatedAt = _c.UtcNow }); return Task.CompletedTask; }
}
public class ToestelRepo : IToestelRepository
{
private readonly Store _s; public ToestelRepo(Store s) => _s = s;
public Task<Toestel?> FindByKenmerkAsync(string kenmerk, CancellationToken ct = default)
=> Task.FromResult(_s.Toestellen.FirstOrDefault(t => t.Kenmerk == kenmerk));
public Task<IReadOnlyList<Toestel>> ListForUserAsync(long userId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<Toestel>>(_s.Toestellen
.Where(t => t.UserId == userId && t.AfgemeldOp == null).ToList());
public void Add(Toestel t) => _s.Toestellen.Add(t);
public void Remove(Toestel t) => _s.Toestellen.Remove(t);
}
/// <summary>Onthoudt wat er verstuurd zou zijn, en kan doen alsof een toestel niet meer bestaat.</summary>
public class TestPush : IPushSender
{
public List<(string Kenmerk, string Titel, string Tekst)> Verstuurd = new();
/// <summary>Kenmerken die Google niet meer kent.</summary>
public HashSet<string> Onbekend = new();
public bool IsIngesteld { get; set; } = true;
public Task<bool> StuurAsync(string kenmerk, string titel, string tekst,
IReadOnlyDictionary<string, string>? gegevens = null,
CancellationToken ct = default)
{
if (Onbekend.Contains(kenmerk)) return Task.FromResult(false);
Verstuurd.Add((kenmerk, titel, tekst));
return Task.FromResult(true);
}
}
/// <summary>De startrijen uit db/21_kluissoorten.sql, zodat de tests met dezelfde soorten beginnen als de echte database.</summary>
public class VersiehistorieRepo : IVersiehistorieRepository
{
private readonly Store _s; public VersiehistorieRepo(Store s) => _s = s;
private int _volgende = 1;
public Task<IReadOnlyList<VersieRegel>> ListAsync(bool alleenZichtbaar, CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<VersieRegel>>(
_s.VersieRegels.Where(r => !alleenZichtbaar || r.Zichtbaar)
.OrderByDescending(r => r.Datum).ThenBy(r => r.Id).ToList());
public Task<VersieRegel?> FindAsync(int id, CancellationToken ct = default) =>
Task.FromResult(_s.VersieRegels.FirstOrDefault(r => r.Id == id));
// Hoofdletterongevoelig, zoals de database (utf8mb4_unicode_ci) ook doet.
public Task<bool> BestaatAsync(DateOnly datum, string kop, int? behalveId, CancellationToken ct = default) =>
Task.FromResult(_s.VersieRegels.Any(r => r.Datum == datum && string.Equals(r.Kop, kop, StringComparison.OrdinalIgnoreCase) && (behalveId == null || r.Id != behalveId)));
public void Add(VersieRegel regel) { if (regel.Id == 0) regel.Id = _volgende++; _s.VersieRegels.Add(regel); }
public void Remove(VersieRegel regel) => _s.VersieRegels.Remove(regel);
}
public static class VaultCategorySaatgoed
{
public static List<VaultCategoryDefinition> Maak()
{
VaultCategoryDefinition S(int id, string naam, int volgorde, bool bespreken = false) => new()
{
Id = id, Naam = naam, Actief = true, Volgorde = volgorde, BesprekenMet = bespreken, Ingebouwd = true
};
return new List<VaultCategoryDefinition>
{
S(3, "Wachtwoord", 10), S(8, "Sociale media", 20), S(4, "Apparaat", 30), S(5, "Pincode", 40),
S(2, "Polis", 50), S(1, "Uitvaartwens", 60, true), S(9, "Donorcodicil", 70, true),
S(10, "Rijbewijs", 80), S(11, "Paspoort", 90), S(6, "Document", 100), S(7, "Notitie", 110),
S(99, "Overig", 120)
};
}
}
public class VaultCategoryRepo : IVaultCategoryRepository
{
private readonly Store _s; public VaultCategoryRepo(Store s) => _s = s;
private VaultCategoryDefinition Met(VaultCategoryDefinition d)
{
d.Velden = _s.SoortVelden.Where(v => v.CategoryId == d.Id).ToList();
return d;
}
public Task<IReadOnlyList<VaultCategoryDefinition>> ListAsync(CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<VaultCategoryDefinition>>(
_s.Soorten.OrderBy(c => c.Volgorde).ThenBy(c => c.Naam).Select(Met).ToList());
public Task<VaultCategoryDefinition?> FindAsync(int id, CancellationToken ct = default) =>
Task.FromResult(_s.Soorten.Where(c => c.Id == id).Select(Met).FirstOrDefault());
public Task<VaultCategoryField?> FindFieldAsync(long fieldId, CancellationToken ct = default) =>
Task.FromResult(_s.SoortVelden.FirstOrDefault(f => f.Id == fieldId));
public Task<IReadOnlyList<MijnBesluit.Application.Models.SoortGebruik>> GebruikAsync(CancellationToken ct = default) =>
Task.FromResult<IReadOnlyList<MijnBesluit.Application.Models.SoortGebruik>>(
_s.VaultItems.Where(v => v.DeletedAt == null).GroupBy(v => v.Category)
.Select(g => new MijnBesluit.Application.Models.SoortGebruik(
(int)g.Key, g.Count(), g.Select(v => v.OwnerPersonId).Distinct().Count())).ToList());
public Task<IReadOnlySet<int>> InGebruikDoorAsync(long personId, CancellationToken ct = default) =>
Task.FromResult<IReadOnlySet<int>>(
_s.VaultItems.Where(v => v.OwnerPersonId == personId && v.DeletedAt == null)
.Select(v => (int)v.Category).ToHashSet());
public Task<int> AantalItemsAsync(int categoryId, CancellationToken ct = default) =>
Task.FromResult(_s.VaultItems.Count(v => (int)v.Category == categoryId && v.DeletedAt == null));
private int _volgende = VaultCategoryDefinition.EersteEigenNummer;
public void Add(VaultCategoryDefinition soort) { if (soort.Id == 0) soort.Id = _volgende++; _s.Soorten.Add(soort); }
public void Remove(VaultCategoryDefinition soort) { _s.Soorten.Remove(soort); _s.SoortVelden.RemoveAll(v => v.CategoryId == soort.Id); }
public void Add(VaultCategoryField veld) { if (veld.Id == 0) veld.Id = _s.NextId(); _s.SoortVelden.Add(veld); }
public void Remove(VaultCategoryField veld) => _s.SoortVelden.Remove(veld);
}