Files
LehrerApp/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs
T
adminandClaude Sonnet 5 fc2d7aea3e Baustein 9: Konflikt-Review-UI (Kapitel 10)
Minimale Liste im Tab "Synchronisation" - Entitaet, Zeitpunkt, welche
Seite ConflictResolver gewaehlt hat, mit "Gesehen"-Aktion. Kein
Feld-Diff fuer v1: die Payloads sind clientseitig verschluesselt, ein
Diff wuerde ohnehin nur rohes JSON zeigen.

Neu EventQueue.MarkReviewed(id) - bisher gab es AddConflict/
GetUnreviewed/ConflictCount, aber keinen Weg, einen Konflikt als
gesehen zu markieren.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 11:37:20 +02:00

350 lines
15 KiB
C#

using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Desktop.ViewModels.Settings;
using LehrerApp.Sync;
using LehrerApp.Sync.Models;
using System.Linq;
using Xunit;
namespace LehrerApp.Desktop.Tests;
public sealed class SettingsViewModelTests
{
private static SettingsViewModel BuildViewModel(FakeSchoolHolidays? holidays = null,
FakeSupervisionDuties? supervisionDuties = null,
FakeSubjects? subjects = null, FakeCompetencyDomains? competencyDomains = null,
EventQueue? eventQueue = null)
{
// Bewusst kein "using": SchoolCalendarSettingsService liest den Pfad erst bei SetState,
// das Verzeichnis muss über die Lebensdauer des ViewModels bestehen bleiben.
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
return new SettingsViewModel(
subjects ?? new FakeSubjects([]), competencyDomains ?? new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
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 LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
eventQueue ?? TestSupport.BuildEventQueue());
}
[Fact]
public void SyncConflicts_ZeigtUnreviewedKonflikteBeimLaden()
{
var queue = TestSupport.BuildEventQueue();
var conflict = new ConflictEntry
{
LocalEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" },
RemoteEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" },
Resolution = "RemoteWon",
};
queue.AddConflict(conflict);
var vm = BuildViewModel(eventQueue: queue);
var item = Assert.Single(vm.SyncConflicts);
Assert.Equal(conflict.Id, item.Id);
Assert.Contains("anderen Gerät", item.ResolutionDisplay);
}
[Fact]
public void MarkConflictReviewed_EntferntKonfliktAusListeUndAusDerQueue()
{
var queue = TestSupport.BuildEventQueue();
var conflict = new ConflictEntry
{
LocalEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" },
RemoteEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString(), Operation = "Save" },
Resolution = "LocalWon",
};
queue.AddConflict(conflict);
var vm = BuildViewModel(eventQueue: queue);
var item = vm.SyncConflicts.Single();
vm.MarkConflictReviewedCommand.Execute(item);
Assert.Empty(vm.SyncConflicts);
Assert.Empty(queue.GetUnreviewed());
}
[Fact]
public void AddSchoolHoliday_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayName = "Sommerferien";
vm.NewHolidayStartText = "01.07.2026";
vm.NewHolidayEndText = "10.08.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Single(holidays.GetAll());
Assert.Single(vm.SchoolHolidayEntries);
}
[Fact]
public void AddSchoolHoliday_EndeVorBeginn_SetztFehlerUndSpeichertNicht()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayName = "Ungültig";
vm.NewHolidayStartText = "10.08.2026";
vm.NewHolidayEndText = "01.07.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Empty(holidays.GetAll());
Assert.NotEqual("", vm.HolidayDateError);
}
[Fact]
public void AddSchoolHoliday_FehlenderName_SetztFehlerUndSpeichertNicht()
{
var holidays = new FakeSchoolHolidays();
var vm = BuildViewModel(holidays);
vm.NewHolidayStartText = "01.07.2026";
vm.NewHolidayEndText = "10.08.2026";
vm.AddSchoolHolidayCommand.Execute(null);
Assert.Empty(holidays.GetAll());
Assert.NotEqual("", vm.HolidayNameError);
}
[Fact]
public void RemoveSchoolHoliday_EntferntEintragAusRepositoryUndListe()
{
var holidays = new FakeSchoolHolidays();
var holiday = new SchoolHoliday { Name = "Herbstferien", StartDate = new DateOnly(2026, 10, 12), EndDate = new DateOnly(2026, 10, 24) };
holidays.Add(holiday);
var vm = BuildViewModel(holidays);
vm.RemoveSchoolHolidayCommand.Execute(vm.SchoolHolidayEntries[0]);
Assert.Empty(holidays.GetAll());
Assert.Empty(vm.SchoolHolidayEntries);
}
[Fact]
public void SelectedStateName_Aendern_PersistiertUeberSchoolCalendarSettings()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-state-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var calendarSettings = new SchoolCalendarSettingsService(tempPath);
var vm = new SettingsViewModel(
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), calendarSettings, new PeriodScheduleService(tempPath),
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
vm.SelectedStateName = "Bayern";
Assert.Equal(GermanState.BY, calendarSettings.State);
}
[Fact]
public void SavePeriodTimes_GueltigeEingabe_WirdPersistiert()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var periodSchedule = new PeriodScheduleService(tempPath);
var vm = new SettingsViewModel(
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
vm.PeriodTimes[0].StartText = "08:00";
vm.PeriodTimes[0].EndText = "08:45";
vm.SavePeriodTimesCommand.Execute(null);
Assert.Equal("", vm.PeriodTimesError);
Assert.Equal(45, periodSchedule.GetDurationMinutes(1));
}
[Fact]
public void SavePeriodTimes_EndeVorBeginn_SetztFehlerUndSpeichertNicht()
{
var tempPath = System.IO.Path.Combine(
System.IO.Path.GetTempPath(), $"lehrerapp-settingsvm-periods-invalid-tests-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempPath);
var periodSchedule = new PeriodScheduleService(tempPath);
var vm = new SettingsViewModel(
new FakeSubjects([]), new FakeCompetencyDomains(), new FakeGradingKeyTemplates(),
new FakeSchemes(), new GradingService(), new BackupService(tempPath),
new DatabaseEncryptionService(), new AppLockService(tempPath),
new LiteDbContext(new MemoryStream()), new PrivacySettingsService(tempPath),
new FakeDocumentation(), new FakeStudents([]), new FakeShorthandCodes([]),
new FakeSchoolHolidays(), new SchoolCalendarSettingsService(tempPath), periodSchedule,
new FakeSupervisionDuties(), new LetterTemplateService(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue());
vm.PeriodTimes[0].StartText = "08:45";
vm.PeriodTimes[0].EndText = "08:00";
vm.SavePeriodTimesCommand.Execute(null);
Assert.NotEqual("", vm.PeriodTimesError);
Assert.Equal(0, periodSchedule.GetDurationMinutes(1));
}
[Fact]
public void AddSupervisionDuty_GueltigeEingabe_WirdGespeichertUndInListeAngezeigt()
{
var duties = new FakeSupervisionDuties();
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.NewDutyLocation = "Pausenhof";
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Single(duties.GetAll());
Assert.Single(vm.SupervisionDuties);
Assert.Equal(DayOfWeek.Monday, duties.GetAll()[0].Weekday);
Assert.Equal(2, duties.GetAll()[0].AfterPeriod);
}
[Fact]
public void AddSupervisionDuty_FehlenderOrt_SetztFehlerUndSpeichertNicht()
{
var duties = new FakeSupervisionDuties();
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Empty(duties.GetAll());
Assert.NotEqual("", vm.NewDutyError);
}
[Fact]
public void AddSupervisionDuty_PauseBereitsBelegt_ZeigtFreundlicheFehlermeldung()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(supervisionDuties: duties);
vm.NewDutyWeekdayName = "Montag";
vm.NewDutyAfterPeriod = 2;
vm.NewDutyLocation = "Bibliothek";
vm.AddSupervisionDutyCommand.Execute(null);
Assert.Single(duties.GetAll());
Assert.Equal("Für diese Pause ist bereits eine Aufsicht eingetragen.", vm.NewDutyError);
}
[Fact]
public void RemoveSupervisionDuty_EntferntEintragAusRepositoryUndListe()
{
var duties = new FakeSupervisionDuties();
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
var vm = BuildViewModel(supervisionDuties: duties);
vm.RemoveSupervisionDutyCommand.Execute(vm.SupervisionDuties[0]);
Assert.Empty(duties.GetAll());
Assert.Empty(vm.SupervisionDuties);
}
[Fact]
public void Kompetenzbereiche_Umsortieren_PersistiertSortOrder()
{
var subject = new Subject { Name = "Biologie", ShortName = "Bio" };
var repository = new FakeCompetencyDomains();
repository.Add(new CompetencyDomain
{
SubjectId = subject.Id, GradeLevel = 10, Name = "Erster Bereich", SortOrder = 0,
});
repository.Add(new CompetencyDomain
{
SubjectId = subject.Id, GradeLevel = 10, Name = "Zweiter Bereich", SortOrder = 1,
});
var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository);
vm.CatalogSubject = vm.Subjects.Single();
vm.Domains[1].MoveUpCommand.Execute(null);
Assert.Equal(["Zweiter Bereich", "Erster Bereich"], vm.Domains.Select(x => x.Name));
var stored = repository.GetBySubjectAndGrade(subject.Id, 10);
Assert.Equal(["Zweiter Bereich", "Erster Bereich"], stored.Select(x => x.Name));
Assert.Equal([0, 1], stored.Select(x => x.SortOrder));
}
[Fact]
public void Kompetenzen_Umsortieren_PersistiertSortOrder()
{
var repository = new FakeCompetencyDomains();
var domain = new CompetencyDomain
{
Name = "Bereich",
Items =
[
new CompetencyItem { Code = "A", Description = "Erste", SortOrder = 0 },
new CompetencyItem { Code = "B", Description = "Zweite", SortOrder = 1 },
],
};
repository.Add(domain);
var vm = new DomainEditItem(domain, repository);
vm.Items[1].MoveUpCommand.Execute(null);
Assert.Equal(["B", "A"], vm.Items.Select(x => x.Code));
var stored = repository.GetById(domain.Id)!;
Assert.Equal(["B", "A"], stored.Items.Select(x => x.Code));
Assert.Equal([0, 1], stored.Items.Select(x => x.SortOrder));
}
[Fact]
public void KatalogKopieren_BereitetSichereVorschauFuerZielstufeVor()
{
var subject = new Subject { Name = "Biologie", ShortName = "Bio" };
var repository = new FakeCompetencyDomains();
repository.Add(new CompetencyDomain
{
SubjectId = subject.Id,
GradeLevel = 10,
Name = "Erkenntnisgewinnung",
Code = "EG",
Items = [new CompetencyItem { Code = "EG1", Description = "Modelle verwenden" }],
});
var vm = BuildViewModel(subjects: new FakeSubjects([subject]), competencyDomains: repository);
vm.CatalogSubject = vm.Subjects.Single();
var preview = vm.PrepareCatalogCopy(11);
Assert.NotNull(preview);
Assert.Equal(11, preview.GradeLevel);
Assert.Equal(1, preview.NewDomains);
Assert.Equal(1, preview.NewCompetencies);
vm.ApplyCatalogImport(preview, CompetencyCatalogImportMode.Merge, new HashSet<string>());
var copied = repository.GetBySubjectAndGrade(subject.Id, 11);
Assert.Single(copied);
Assert.NotEqual(repository.GetBySubjectAndGrade(subject.Id, 10)[0].Id, copied[0].Id);
Assert.Equal("EG1", copied[0].Items.Single().Code);
}
}