Files
LehrerApp/LehrerApp.Desktop.Tests/SettingsViewModelTests.cs
T
adminandClaude Sonnet 5 55fba2cadb feat: lokaler MCP-Server, Phase 4 (Elternbrief-Vorlagen + Claude-Desktop-Registrierung)
Schließt die MCP-Server-Spec ab. "Worksheets" aus der Spec entsprechen
im tatsächlichen Datenmodell den .lavorlage-Elternbrief-Vorlagen
(LehrerApp.Templating) - es gibt kein separates Arbeitsblatt-Konzept mit
Fach/Klassenstufe-Metadaten. Neue Tools list_letter_templates (Read) und
render_letter (Read, liefert Base64-PDF, kein DB-Schreibzugriff).

upload_worksheet/update_worksheet bewusst nicht umgesetzt: das
Seitenlayout ist eine eigene positionsbasierte DSL mit eigenem
visuellen Editor (LehrerApp.TemplateDesigner) - ein LLM müsste sie
blind erzeugen, mit hohem Risiko für kaputte Layouts. Platzhalter-Logik
aus CreateLetterDialogViewModel nach LetterPlaceholderBuilder extrahiert,
damit Dialog und MCP-Tool nicht auseinanderdriften.

Neuer McpClientRegistrationService trägt den Bridge-Pfad in Claude
Desktops claude_desktop_config.json ein (Button in den Einstellungen,
nie automatisch), ohne bestehende Fremdeinträge zu verlieren und ohne
eine nicht lesbare Konfigurationsdatei zu überschreiben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 22:53:46 +02:00

565 lines
23 KiB
C#

using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Data;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Settings;
using LehrerApp.Sync;
using LehrerApp.Sync.Models;
using System.Linq;
using Xunit;
using LehrerApp.Templating;
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, SyncKeyStatus? syncKeyStatus = null,
SyncKeyRecoveryService? syncKeyRecovery = null, AppearanceSettingsService? appearance = null,
TrashViewModel? trashTab = 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 BackupSettingsService(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 TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildMcpSettingsService(),
TestSupport.BuildMcpClientRegistrationService(),
TestSupport.BuildWebUntisSettingsService(),
TestSupport.BuildAnnualPlanSettingsService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(),
eventQueue ?? TestSupport.BuildEventQueue(), TestSupport.BuildAppLogger(),
syncKeyStatus ?? TestSupport.BuildSyncKeyStatus(),
syncKeyRecovery ?? TestSupport.BuildSyncKeyRecoveryService(),
appearance ?? TestSupport.BuildAppearanceSettingsService(),
trashTab ?? TestSupport.BuildTrashViewModel());
}
// ── Darstellung (12.4) ────────────────────────────────────────────────────────────────────
[Fact]
public void SelectedTheme_SpiegeltDenGespeichertenStandBeimStartWider()
{
var appearance = TestSupport.BuildAppearanceSettingsService();
appearance.Save(AppTheme.Dark);
var vm = BuildViewModel(appearance: appearance);
Assert.Equal(AppTheme.Dark, vm.SelectedTheme);
Assert.Equal("Dunkel", vm.SelectedThemeName);
}
[Fact]
public void SelectedThemeName_Aendern_PersistiertUndBenachrichtigtDenCodeBehind()
{
var appearance = TestSupport.BuildAppearanceSettingsService();
var vm = BuildViewModel(appearance: appearance);
AppTheme? notified = null;
vm.OnThemeChanged = t => notified = t;
vm.SelectedThemeName = "Dunkel";
Assert.Equal(AppTheme.Dark, notified);
Assert.Equal(AppTheme.Dark, appearance.Load());
}
// ── Sync-Schlüssel: Warnung + Wiederherstellungscode (10.3.2) ────────────────────────────
//
// Der "glückliche" Redeem-Pfad (gültiger Code + Datei + Bestätigung) endet in
// AppBootstrapper.RestartApplication(), das Environment.Exit(0) aufruft - das darf ein Test
// niemals auslösen (würde den Testlauf beenden). Deshalb sind hier nur die Zweige getestet,
// die VOR diesem Aufruf zurückkehren; genau dasselbe Muster gilt bereits für
// RedeemPairingCode, das aus demselben Grund ebenfalls keinen Erfolgstest hat. Der
// eigentliche Kryptografie-/Datei-Roundtrip ist in SyncKeyRecoveryServiceTests abgedeckt.
[Fact]
public void SyncKeyWasRegenerated_SpiegeltDenBeimStartErkanntenStatusWider()
{
var vm = BuildViewModel(syncKeyStatus: new SyncKeyStatus(keyWasRegenerated: true));
Assert.True(vm.SyncKeyWasRegenerated);
}
[Fact]
public void SyncKeyWasRegenerated_NormalerStart_IstFalse()
{
var vm = BuildViewModel(syncKeyStatus: new SyncKeyStatus(keyWasRegenerated: false));
Assert.False(vm.SyncKeyWasRegenerated);
}
[Fact]
public async Task CreateRecoveryCode_SpeichernErfolgreich_ZeigtCodeUndUebergibtDateiinhalt()
{
var vm = BuildViewModel();
string? savedContent = null;
vm.OnSaveRecoveryFile = content => { savedContent = content; return Task.FromResult(true); };
await vm.CreateRecoveryCodeCommand.ExecuteAsync(null);
Assert.NotEmpty(vm.RecoveryCode);
Assert.NotNull(savedContent);
Assert.Contains("notieren", vm.RecoveryStatus);
}
[Fact]
public async Task CreateRecoveryCode_AbbruchBeimSpeichern_ZeigtKeinenCode()
{
var vm = BuildViewModel();
vm.OnSaveRecoveryFile = _ => Task.FromResult(false);
await vm.CreateRecoveryCodeCommand.ExecuteAsync(null);
Assert.Empty(vm.RecoveryCode);
Assert.Equal("Abgebrochen.", vm.RecoveryStatus);
}
[Fact]
public async Task RedeemRecoveryCode_LeererCode_FragtNieNachEinerDatei()
{
var vm = BuildViewModel();
var filePickerCalled = false;
vm.OnPickRecoveryFile = () => { filePickerCalled = true; return Task.FromResult<string?>(null); };
vm.RecoveryCodeInput = " ";
await vm.RedeemRecoveryCodeCommand.ExecuteAsync(null);
Assert.False(filePickerCalled);
Assert.Equal("Bitte Code eingeben.", vm.RecoveryStatus);
}
[Fact]
public async Task RedeemRecoveryCode_DateiauswahlAbgebrochen_TutNichts()
{
var vm = BuildViewModel();
vm.OnPickRecoveryFile = () => Task.FromResult<string?>(null);
vm.RecoveryCodeInput = "IRGENDEIN-CODE";
var exception = await Record.ExceptionAsync(() => vm.RedeemRecoveryCodeCommand.ExecuteAsync(null));
Assert.Null(exception);
}
[Fact]
public async Task RedeemRecoveryCode_FalscherCode_ZeigtFehlerUndWirftNicht()
{
var recovery = TestSupport.BuildSyncKeyRecoveryService();
var vm = BuildViewModel(syncKeyRecovery: recovery);
vm.OnPickRecoveryFile = () => Task.FromResult<string?>(recovery.CreateRecoveryFile("RICHTIGER-CODE"));
vm.OnConfirmRecoveryRestore = () => Task.FromResult(true);
vm.RecoveryCodeInput = "FALSCHER-CODE";
var exception = await Record.ExceptionAsync(() => vm.RedeemRecoveryCodeCommand.ExecuteAsync(null));
Assert.Null(exception);
Assert.Contains("passen nicht zusammen", vm.RecoveryStatus);
}
[Fact]
public async Task RedeemRecoveryCode_BestaetigungAbgelehnt_TutNichts()
{
var recovery = TestSupport.BuildSyncKeyRecoveryService();
var vm = BuildViewModel(syncKeyRecovery: recovery);
vm.OnPickRecoveryFile = () => Task.FromResult<string?>(recovery.CreateRecoveryFile("EIN-CODE"));
vm.OnConfirmRecoveryRestore = () => Task.FromResult(false);
vm.RecoveryCodeInput = "EIN-CODE";
var exception = await Record.ExceptionAsync(() => vm.RedeemRecoveryCodeCommand.ExecuteAsync(null));
Assert.Null(exception);
}
[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 ClearSyncConflicts_LeertProtokollUndQueue()
{
var queue = TestSupport.BuildEventQueue();
foreach (var resolution in new[] { "LocalWon", "RemoteWon" })
queue.AddConflict(new ConflictEntry
{
LocalEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString() },
RemoteEvent = new SyncEvent { EntityType = "Student", EntityId = Guid.NewGuid().ToString() },
Resolution = resolution,
});
var vm = BuildViewModel(eventQueue: queue);
vm.ClearSyncConflictsCommand.Execute(null);
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 async Task SaveSchoolLocation_FehlendeFelder_ZeigtAlleFeldfehler()
{
var vm = BuildViewModel();
await vm.SaveSchoolLocationCommand.ExecuteAsync(null);
Assert.NotEmpty(vm.SchoolNameError);
Assert.NotEmpty(vm.SchoolStreetError);
Assert.NotEmpty(vm.SchoolPostalCodeError);
Assert.NotEmpty(vm.SchoolCityError);
}
[Fact]
public async Task SaveSchoolLocation_UngueltigePlz_RuftKeinenServerAuf()
{
var vm = BuildViewModel();
vm.SchoolName = "Testschule";
vm.SchoolStreet = "Schulweg 1";
vm.SchoolPostalCode = "1234";
vm.SchoolCity = "Teststadt";
await vm.SaveSchoolLocationCommand.ExecuteAsync(null);
Assert.Contains("fünfstellige", vm.SchoolPostalCodeError);
Assert.Empty(vm.SchoolLocationStatus);
}
[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 BackupSettingsService(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 TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildMcpSettingsService(),
TestSupport.BuildMcpClientRegistrationService(),
TestSupport.BuildWebUntisSettingsService(),
TestSupport.BuildAnnualPlanSettingsService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
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 BackupSettingsService(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 TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildMcpSettingsService(),
TestSupport.BuildMcpClientRegistrationService(),
TestSupport.BuildWebUntisSettingsService(),
TestSupport.BuildAnnualPlanSettingsService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
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 BackupSettingsService(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 TemplateStore(tempPath), TestSupport.BuildAiSettingsService(), TestSupport.BuildAiPlanningService(),
TestSupport.BuildMcpSettingsService(),
TestSupport.BuildMcpClientRegistrationService(),
TestSupport.BuildWebUntisSettingsService(),
TestSupport.BuildAnnualPlanSettingsService(),
TestSupport.BuildSyncSettingsService(), TestSupport.BuildSyncAuthService(), TestSupport.BuildEventQueue(),
TestSupport.BuildAppLogger(), TestSupport.BuildSyncKeyStatus(), TestSupport.BuildSyncKeyRecoveryService(),
TestSupport.BuildAppearanceSettingsService(), TestSupport.BuildTrashViewModel());
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);
}
}