Baustein 3: Repositories verdrahten Teil A (Kapitel 10)

OnChange-Hook auf den 19 einfachen Repositories (keine Kaskaden/Batches)
ergaenzt: SeatingPlan, GroupMembership, GradingKeyTemplate, Grade,
GradingScheme, ReportGrade, Unit, Lesson, WorkTask, TimeEntry,
ParticipationAspect, ParticipationSection, Subject, ShorthandCode,
AlternativeLessonPath, TimetableSlot, SchoolHoliday, SupervisionDuty,
SubstitutionEntry. Mechanisch, ein Aufruf nach dem bestehenden
Upsert/Delete.

Tabellengetriebener Test statt 19 fast identischer Testdateien.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:22:46 +02:00
co-authored by Claude Sonnet 5
parent 831cc1c16c
commit ed34e5d036
2 changed files with 296 additions and 13 deletions
@@ -0,0 +1,204 @@
using LehrerApp.Core.Models;
using LehrerApp.Data.Repositories;
using Xunit;
namespace LehrerApp.Data.Tests;
// Baustein 3: prüft für jedes der ~19 "einfachen" Repositories (keine Kaskaden/Batches), dass
// Save/Delete den OnChange-Hook mit dem korrekten EntityType auslöst. Tabellengetrieben statt
// eine Datei pro Repository, um die immer gleiche Prüfung nicht 19x zu wiederholen.
public sealed class ChangeHookMatrixTests
{
public static IEnumerable<object[]> Cases()
{
yield return Case("SeatingPlan", nameof(SeatingPlan), db =>
{
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
new GroupRepository(db).Save(group);
var repo = new SeatingPlanRepository(db);
var plan = new SeatingPlan { GroupId = group.Id, Name = "Standard", Rows = 2, Columns = 2 };
repo.Save(plan);
return (plan.Id, () => repo.Delete(plan.Id));
});
yield return Case("GroupMembership", nameof(GroupMembership), db =>
{
var repo = new GroupMembershipRepository(db);
var membership = new GroupMembership { StudentId = Guid.NewGuid(), GroupId = Guid.NewGuid() };
repo.Save(membership);
return (membership.Id, () => repo.Delete(membership.Id));
});
yield return Case("GradingKeyTemplate", nameof(GradingKeyTemplate), db =>
{
var repo = new GradingKeyTemplateRepository(db);
var template = new GradingKeyTemplate { Name = "Standard", GradingSystem = GradingSystem.Points0To15 };
repo.Save(template);
return (template.Id, () => repo.Delete(template.Id));
});
yield return Case("Grade", nameof(Grade), db =>
{
var repo = new GradeRepository(db);
var grade = new Grade { GroupId = Guid.NewGuid(), StudentId = Guid.NewGuid(), Value = "2" };
repo.Save(grade);
return (grade.Id, () => repo.Delete(grade.Id));
});
yield return Case("GradingScheme", nameof(GradingScheme), db =>
{
var repo = new GradingSchemeRepository(db);
var scheme = new GradingScheme { GroupId = Guid.NewGuid(), ExamsPercent = 60, ParticipationPercent = 30, OtherPercent = 10 };
repo.Save(scheme);
return (scheme.Id, () => repo.Delete(scheme.Id));
});
yield return Case("ReportGrade", nameof(ReportGrade), db =>
{
var repo = new ReportGradeRepository(db);
var grade = new ReportGrade { StudentId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Period = "1. Halbjahr" };
repo.Save(grade);
return (grade.Id, () => repo.Delete(grade.Id));
});
yield return Case("Unit", nameof(Unit), db =>
{
var repo = new UnitRepository(db);
var unit = new Unit { GroupId = Guid.NewGuid(), Title = "Einheit" };
repo.Save(unit);
return (unit.Id, () => repo.Delete(unit.Id));
});
yield return Case("Lesson", nameof(Lesson), db =>
{
var repo = new LessonRepository(db);
var lesson = new Lesson { UnitId = Guid.NewGuid(), GroupId = Guid.NewGuid(), Date = new DateOnly(2026, 3, 12) };
repo.Save(lesson);
return (lesson.Id, () => repo.Delete(lesson.Id));
});
yield return Case("WorkTask", nameof(WorkTask), db =>
{
var repo = new WorkTaskRepository(db);
var task = new WorkTask { Title = "Klausur korrigieren", Category = TaskCategory.Correction };
repo.Save(task);
return (task.Id, () => repo.Delete(task.Id));
});
yield return Case("TimeEntry", nameof(TimeEntry), db =>
{
var repo = new TimeEntryRepository(db);
var entry = new TimeEntry { Date = new DateOnly(2026, 3, 12), DurationMinutes = 30, Category = "Vorbereitung" };
repo.Save(entry);
return (entry.Id, () => repo.Delete(entry.Id));
});
yield return Case("ParticipationAspect", nameof(ParticipationAspect), db =>
{
var repo = new ParticipationAspectRepository(db);
var aspect = new ParticipationAspect { GroupId = Guid.NewGuid(), Key = "quality", Label = "Qualität" };
repo.Save(aspect);
return (aspect.Id, () => repo.Delete(aspect.Id));
});
yield return Case("ParticipationSection", nameof(ParticipationSection), db =>
{
var repo = new ParticipationSectionRepository(db);
var section = new ParticipationSection
{
GroupId = Guid.NewGuid(), Label = "1. Abschnitt",
StartDate = new DateOnly(2026, 3, 1), EndDate = new DateOnly(2026, 4, 1),
};
repo.Save(section);
return (section.Id, () => repo.Delete(section.Id));
});
yield return Case("Subject", nameof(Subject), db =>
{
var repo = new SubjectRepository(db);
var subject = new Subject { Name = "Chemie" };
repo.Save(subject);
return (subject.Id, () => repo.Delete(subject.Id));
});
yield return Case("ShorthandCode", nameof(ShorthandCode), db =>
{
var repo = new ShorthandCodeRepository(db);
var code = new ShorthandCode { Code = "Tb", Label = "Tafelbild" };
repo.Save(code);
return (code.Id, () => repo.Delete(code.Id));
});
yield return Case("AlternativeLessonPath", nameof(AlternativeLessonPath), db =>
{
var repo = new AlternativeLessonPathRepository(db);
var path = new AlternativeLessonPath { Name = "Kurzversion" };
repo.Save(path);
return (path.Id, () => repo.Delete(path.Id));
});
yield return Case("TimetableSlot", nameof(TimetableSlot), db =>
{
var repo = new TimetableSlotRepository(db);
var slot = new TimetableSlot { GroupId = Guid.NewGuid(), Weekday = DayOfWeek.Tuesday, PeriodNumber = 3 };
repo.Save(slot);
return (slot.Id, () => repo.Delete(slot.Id));
});
yield return Case("SchoolHoliday", nameof(SchoolHoliday), db =>
{
var repo = new SchoolHolidayRepository(db);
var holiday = new SchoolHoliday { Name = "Osterferien", StartDate = new DateOnly(2026, 3, 30), EndDate = new DateOnly(2026, 4, 10) };
repo.Save(holiday);
return (holiday.Id, () => repo.Delete(holiday.Id));
});
yield return Case("SupervisionDuty", nameof(SupervisionDuty), db =>
{
var repo = new SupervisionDutyRepository(db);
var duty = new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" };
repo.Save(duty);
return (duty.Id, () => repo.Delete(duty.Id));
});
yield return Case("SubstitutionEntry", nameof(SubstitutionEntry), db =>
{
var repo = new SubstitutionEntryRepository(db);
var entry = new SubstitutionEntry { Date = new DateOnly(2026, 3, 12), Kind = SubstitutionKind.Lesson, PeriodNumber = 3, Description = "Vertretung 8a" };
repo.Save(entry);
return (entry.Id, () => repo.Delete(entry.Id));
});
}
[Theory]
[MemberData(nameof(Cases))]
public void Save_LoestOnChangeMitKorrektemEntityTypeAus(
string _, string entityType, Func<LiteDbContext, (Guid Id, Action Delete)> setup)
{
using var db = new LiteDbContext(new MemoryStream());
var calls = new List<(string EntityType, string Operation)>();
db.OnChange = (type, id, op, payload) => calls.Add((type, op));
setup(db);
Assert.Contains((entityType, "Save"), calls);
}
[Theory]
[MemberData(nameof(Cases))]
public void Delete_LoestOnChangeMitKorrektemEntityTypeAus(
string _, string entityType, Func<LiteDbContext, (Guid Id, Action Delete)> setup)
{
using var db = new LiteDbContext(new MemoryStream());
var (id, delete) = setup(db);
var calls = new List<(string EntityType, string EntityId, string Operation)>();
db.OnChange = (type, entId, op, payload) => calls.Add((type, entId, op));
delete();
Assert.Contains((entityType, id.ToString(), "Delete"), calls);
}
private static object[] Case(string name, string entityType, Func<LiteDbContext, (Guid Id, Action Delete)> setup) =>
[name, entityType, setup];
}