Kapitel 7 abgeschlossen.

This commit is contained in:
2026-08-16 00:29:01 +02:00
parent da8d2bb1da
commit f9e398b225
47 changed files with 2636 additions and 107 deletions
@@ -0,0 +1,108 @@
using System.IO.Compression;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Students;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class CreateLetterDialogViewModelTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"lehrerapp-letter-vm-tests-{Guid.NewGuid():N}");
public CreateLetterDialogViewModelTests() => Directory.CreateDirectory(_directory);
[Fact]
public void OhneVorlage_KannKeinenBriefErzeugen()
{
var student = StudentWithContact("Sehr geehrte Frau Muster,");
var vm = Build(student, new LetterTemplateService(_directory));
Assert.False(vm.CanGenerate);
Assert.Contains(vm.Issues, i => i.Message.Contains("Vorlage", StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void VerwendeteBriefanredeFehlt_BlockiertErzeugung()
{
var service = ServiceWithTemplate("Letter.Salutation");
var vm = Build(StudentWithContact(null), service);
Assert.False(vm.CanGenerate);
Assert.Contains(vm.Issues, i => i.Message.Contains("Briefanrede"));
}
[Fact]
public void VollstaendigeDaten_ErzeugenEditierbareDocxKopie()
{
var service = ServiceWithTemplate("Letter.Salutation", "Student.FirstName", "CurrentDate");
var vm = Build(StudentWithContact("Sehr geehrte Frau Muster,"), service);
var output = Path.Combine(_directory, "Brief.docx");
var generated = vm.Generate(output);
Assert.True(generated);
Assert.True(File.Exists(output));
using var archive = ZipFile.OpenRead(output);
using var reader = new StreamReader(archive.GetEntry("word/document.xml")!.Open());
var xml = reader.ReadToEnd();
Assert.Contains("Sehr geehrte Frau Muster,", xml);
Assert.Contains("Lena", xml);
}
[Fact]
public void KontaktBearbeiten_SpeichertExpliziteBriefanrede()
{
var vm = new ContactEditDialogViewModel(new Contact { Name = "Frau Muster" });
vm.LetterSalutation = "Sehr geehrte Frau Muster,";
vm.SaveCommand.Execute(null);
Assert.Equal("Sehr geehrte Frau Muster,", vm.Result!.LetterSalutation);
}
private CreateLetterDialogViewModel Build(Student student, LetterTemplateService service) =>
new(student, service, new FakeMemberships([]), new FakeGroups([]));
private LetterTemplateService ServiceWithTemplate(params string[] tags)
{
var source = Path.Combine(_directory, $"{Guid.NewGuid():N}.docx");
using (var archive = ZipFile.Open(source, ZipArchiveMode.Create))
{
var entry = archive.CreateEntry("word/document.xml");
using var writer = new StreamWriter(entry.Open());
var controls = string.Join("", tags.Select(tag =>
$"<w:sdt><w:sdtPr><w:tag w:val=\"{tag}\"/></w:sdtPr><w:sdtContent>" +
"<w:p><w:r><w:t>Platzhalter</w:t></w:r></w:p></w:sdtContent></w:sdt>"));
writer.Write("<w:document xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\">" +
$"<w:body>{controls}</w:body></w:document>");
}
var service = new LetterTemplateService(Path.Combine(_directory, $"service-{Guid.NewGuid():N}"));
service.Import(source, "Elternbrief");
return service;
}
private static Student StudentWithContact(string? salutation) => new()
{
FirstName = "Lena",
LastName = "Beispiel",
Contacts =
[
new Contact
{
Name = "Frau Muster",
Relation = "Mutter",
LetterSalutation = salutation,
Street = "Hauptstraße 1",
PostalCode = "12345",
City = "Musterstadt",
},
],
};
public void Dispose()
{
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
}
}
+12 -3
View File
@@ -8,11 +8,20 @@ namespace LehrerApp.Desktop.Tests;
public class FakeStudents(List<Student> all) : IStudentRepository
{
private readonly Dictionary<Guid, StudentReferenceSummary> _references = [];
public Student? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
public List<Student> GetAll(bool includeInactive = false) => all;
public List<Student> GetAll(bool includeInactive = false) =>
includeInactive ? all.ToList() : all.Where(s => s.IsActive).ToList();
public List<Student> GetByGroup(Guid groupId) => all;
public void Save(Student student) { }
public void Delete(Guid id) { }
public StudentReferenceSummary GetReferenceSummary(Guid studentId) =>
_references.GetValueOrDefault(studentId, new StudentReferenceSummary(0, 0, 0, 0, 0, 0));
public void SetReferenceSummary(Guid studentId, StudentReferenceSummary summary) => _references[studentId] = summary;
public void Save(Student student) { all.RemoveAll(s => s.Id == student.Id); all.Add(student); }
public void Delete(Guid id)
{
if (GetReferenceSummary(id).HasReferences) throw new InvalidOperationException();
all.RemoveAll(s => s.Id == id);
}
}
public class FakeMemberships(List<GroupMembership> all) : IGroupMembershipRepository
@@ -0,0 +1,29 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Students;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public class GenderAvatarDisplayTests
{
[Theory]
[InlineData(Gender.M, "M")]
[InlineData(Gender.W, "W")]
[InlineData(Gender.D, "D")]
public void VerwendetEindeutigeBeschriftung(Gender gender, string expected) =>
Assert.Equal(expected, GenderAvatarDisplay.Label(gender));
[Fact]
public void UnbekanntesGeschlechtHatNeutralenFallback()
{
Assert.Equal("?", GenderAvatarDisplay.Label(null));
Assert.Contains("nicht angegeben", GenderAvatarDisplay.Tooltip(null));
}
[Theory]
[InlineData(Gender.M)]
[InlineData(Gender.W)]
[InlineData(Gender.D)]
public void AuswahlwertKannVerlustfreiZurueckgewandeltWerden(Gender gender) =>
Assert.Equal(gender, GenderAvatarDisplay.FromOption(GenderAvatarDisplay.ToOption(gender)));
}
@@ -0,0 +1,74 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class GroupRolloverDialogViewModelTests
{
[Fact]
public void Vorauswahl_NimmtNurAktiveSchuelerAmSchuljahresende()
{
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26", GradeLevel = 8 };
var fullYearStudent = Student("Ganzjahr");
var h1Student = Student("Halbjahr");
var withdrawnStudent = Student("Ausgetreten");
var inactiveStudent = Student("Inaktiv", active: false);
var membershipList = new List<GroupMembership>
{
Membership(group, fullYearStudent, MembershipPeriod.FullYear),
Membership(group, h1Student, MembershipPeriod.H1Only),
Membership(group, withdrawnStudent, MembershipPeriod.FullYear, new DateOnly(2026, 5, 1)),
Membership(group, inactiveStudent, MembershipPeriod.FullYear),
};
var groups = new FakeGroups([group]);
var memberships = new FakeMemberships(membershipList);
var schemes = new FakeSchemes();
var schoolYears = new SchoolYearService();
var vm = new GroupRolloverDialogViewModel(group,
new FakeStudents([fullYearStudent, h1Student, withdrawnStudent, inactiveStudent]),
memberships, schemes, schoolYears,
new GroupRolloverService(groups, memberships, schemes, schoolYears));
Assert.True(vm.Students.Single(s => s.FullName.Contains("Ganzjahr")).IsSelected);
Assert.False(vm.Students.Single(s => s.FullName.Contains("Halbjahr")).IsSelected);
Assert.False(vm.Students.Single(s => s.FullName.Contains("Ausgetreten")).IsSelected);
var inactive = vm.Students.Single(s => s.FullName.Contains("Inaktiv"));
Assert.False(inactive.IsSelected);
Assert.False(inactive.CanSelect);
Assert.Equal(1, vm.SelectedCount);
}
[Fact]
public void Dialog_SchlaegtNaechstesSchuljahrUndNaechsteStufeVor()
{
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26", GradeLevel = 8 };
var student = Student("Ganzjahr");
var membershipList = new List<GroupMembership> { Membership(group, student, MembershipPeriod.FullYear) };
var groups = new FakeGroups([group]);
var memberships = new FakeMemberships(membershipList);
var schemes = new FakeSchemes();
var schoolYears = new SchoolYearService();
var vm = new GroupRolloverDialogViewModel(group, new FakeStudents([student]), memberships,
schemes, schoolYears, new GroupRolloverService(groups, memberships, schemes, schoolYears));
Assert.Equal("2026/27", vm.TargetSchoolYear);
Assert.Equal(9, vm.GradeLevel);
Assert.True(vm.ArchiveSource);
}
private static Student Student(string lastName, bool active = true) =>
new() { FirstName = "Test", LastName = lastName, IsActive = active };
private static GroupMembership Membership(LearningGroup group, Student student,
MembershipPeriod period, DateOnly? leftAt = null) => new()
{
GroupId = group.Id,
StudentId = student.Id,
Period = period,
JoinedAt = new DateOnly(2025, 8, 1),
LeftAt = leftAt,
};
}
@@ -0,0 +1,50 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Students;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public class ManageStudentDialogViewModelTests
{
[Fact]
public void VerknuepfteDatenSperrenEndgueltigesLoeschenAberNichtDeaktivieren()
{
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
var students = new FakeStudents([student]);
students.SetReferenceSummary(student.Id, new StudentReferenceSummary(1, 2, 0, 0, 3, 1));
var vm = new ManageStudentDialogViewModel(students, student);
Assert.False(vm.CanDelete);
Assert.Equal(4, vm.ReferenceItems.Count);
vm.DeactivateCommand.Execute(null);
Assert.False(student.IsActive);
Assert.Equal(StudentManagementResult.Deactivated, vm.Result);
}
[Fact]
public void UnverknuepfterSchuelerKannEndgueltigGeloeschtWerden()
{
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
var students = new FakeStudents([student]);
var vm = new ManageStudentDialogViewModel(students, student);
vm.DeletePermanentlyCommand.Execute(null);
Assert.Equal(StudentManagementResult.Deleted, vm.Result);
Assert.Null(students.GetById(student.Id));
}
[Fact]
public void DeaktivierterSchuelerKannWiederAktiviertWerden()
{
var student = new Student { FirstName = "Anna", LastName = "Beispiel", IsActive = false };
var vm = new ManageStudentDialogViewModel(new FakeStudents([student]), student);
vm.ReactivateCommand.Execute(null);
Assert.True(student.IsActive);
Assert.Equal(StudentManagementResult.Reactivated, vm.Result);
}
}
@@ -24,7 +24,8 @@ public sealed class SettingsViewModelTests
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
holidays ?? new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath),
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties());
new PeriodScheduleService(tempPath), supervisionDuties ?? new FakeSupervisionDuties(),
new LetterTemplateService(tempPath));
}
[Fact]
@@ -100,7 +101,7 @@ public sealed class SettingsViewModelTests
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
new FakeSupervisionDuties());
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
vm.SelectedStateName = "Bayern";
@@ -122,7 +123,7 @@ public sealed class SettingsViewModelTests
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties());
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
vm.PeriodTimes[0].StartText = "08:00";
vm.PeriodTimes[0].EndText = "08:45";
@@ -148,7 +149,7 @@ public sealed class SettingsViewModelTests
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties());
new FakeSupervisionDuties(), new LetterTemplateService(tempPath));
vm.PeriodTimes[0].StartText = "08:45";
vm.PeriodTimes[0].EndText = "08:00";
@@ -0,0 +1,60 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Students;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public class StudentListViewModelTests
{
private readonly Student _anna = new() { FirstName = "Anna", LastName = "Beispiel", Gender = Gender.W };
private readonly Student _ben = new() { FirstName = "Ben", LastName = "Muster", Gender = Gender.M };
private readonly LearningGroup _group = new() { Name = "Mathematik 7a" };
[Fact]
public void SucheFindetSchuelerWeiterhinUeberNamen()
{
var vm = BuildViewModel();
vm.SearchText = "Anna";
Assert.Single(vm.Students);
Assert.Equal(_anna.Id, vm.Students[0].Id);
}
[Fact]
public void SucheFindetSchuelerUeberLerngruppe()
{
var vm = BuildViewModel();
vm.SearchText = "7a";
Assert.Single(vm.Students);
Assert.Equal(_ben.Id, vm.Students[0].Id);
}
[Fact]
public void LeereSucheZeigtAlleSchueler()
{
var vm = BuildViewModel();
vm.SearchText = " ";
Assert.Equal(2, vm.Students.Count);
}
[Fact]
public void ListeneintraegeZeigenAvatarAusGespeichertemGeschlecht()
{
var vm = BuildViewModel();
Assert.Equal("W", vm.Students.Single(s => s.Id == _anna.Id).AvatarLabel);
Assert.Equal("M", vm.Students.Single(s => s.Id == _ben.Id).AvatarLabel);
}
private StudentListViewModel BuildViewModel() => new(
new FakeStudents([_anna, _ben]),
new FakeGroups([_group]),
new FakeMemberships([
new GroupMembership { StudentId = _ben.Id, GroupId = _group.Id },
]));
}